Expo.ioReact Context APIReact NativeReact Native PaperReact Redux todouseContextuseReducer

Reduxの代わりにReact Context APIでStateを管理してみる

italy-4882600_1920 Expo.io
スポンサーリンク

条件・関連リソース

  • この記事のコードは react-with-redux-philosophyReact Native 用に修正したものです。
  • この記事の構成:実装 ( Doing ) >>> 解説・説明 ( Explaining )
  • Expo開発環境を整っていると想定します。( + シミュレータ )
  • expo 36.0.0
  • react 16.9.0
  • react-native-paper 3.6.0

React Context APIState 管理を理解するには Redux の概念や動作方法の知識が必要です。

アプリの概要・事前準備

  • Todo アプリを作成しながら React Context API を利用して State を管理する方法をみていきます。
  • アプリの機能
    • タスクの追加
    • タスクの閲覧
    • タスクの完了・進行中の設定
    • 閲覧のフィルタリング ( すべて、完了したタスク、進行中のタスク )

▼ 完成すると以下のような動作をします。( 動画が再生されない場合は こちら から )

UI ライブラリとして、React Native Paper をインストールします。

try🐶everything global-state-mgmt-usecontext$ npm install react-native-paper

タスクを追加する際、ユニークなランダム ID を生成する uuid を利用します。
react-native-get-random-values も合わせてインストールします。

try🐶everything global-state-mgmt-usecontext$ npm install uuid
try🐶everything global-state-mgmt-usecontext$ npm install react-native-get-random-values

uuidReact Navite で使用する方法の詳細は このリンク をご参考ください。

Expo プロジェクトを作成

expo init your-project-name で新しいプロジェクトを作成し expo サーバを起動しておきます。

try🐶everything ~$ expo init global-state-mgmt-usecontext
? Choose a template: expo-template-blank
Using Yarn to install packages. You can pass --npm to use npm instead.
...

try🐶everything ~$ cd global-state-mgmt-usecontext/
try🐶everything global-state-mgmt-usecontext$ expo start

iOS Simulator を起動しておきます。

別のターミナルで、src フォルダの下に globalStateExample.js ファイルを作成します。

try🐶everything global-state-mgmt-usecontext$ mkdir src && touch src/globalStateExample.js
// src/globalStateExample.js

import React from "react";
import { View, Text, StyleSheet } from "react-native";

const GlobalStateWithReactContext = props => {
  return (
    <View style={styles.container}>
      <Text>GlobalStateWithReactContext</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center"
  }
});

export default GlobalStateWithReactContext;

App.js を以下のように修正します。

import React from "react";
import { StyleSheet, SafeAreaView } from "react-native";
import GlobalStateWithReactContext from "./src/globalStateExample";

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <GlobalStateWithReactContext />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center"
  }
});

iOS Simulator の画面上に GlobalStateWithReactContext と表示されれば OK です。

基本動作確認が終わったら、UI を作成します。

スクリーンの UI を作成

  • 対象 UI
    • 閲覧のフィルタリング ( すべて、完了したタスク、進行中のタスク )
// src/Filter.js

import React from "react";
import { View } from "react-native";
import { Button } from "react-native-paper";

export const Filter = () => {
  return (
    <View
      style={{
        alignItems: "center",
        justifyContent: "center",
        flexDirection: "row",
        marginVertical: 10
      }}
    >
      <Button onPress={() => {}}>All</Button>
      <Button onPress={() => {}}>Complete</Button>
      <Button onPress={() => {}}>Incomplete</Button>
    </View>
  );
};
  • 対象 UI
    • タスクの閲覧
    • タスクの完了・進行中の設定
// src/TodoList.js

import React from "react";
import { View } from "react-native";
import { initialTodos } from "./initialTodos";
import { TodoItem } from "./TodoItem";

export const TodoList = () => {
  return (
    <View>
      {initialTodos.map(todo => {
        return <TodoItem key={todo.id} todo={todo} />;
      })}
    </View>
  );
};
// src/TodoItem.js

import React, { useContext } from "react";
import { View, Text } from "react-native";
import { Checkbox } from "react-native-paper";

export const TodoItem = ({ todo }) => {
  return (
    <View
      style={{
        flexDirection: "row",
        paddingLeft: 40,
        alignItems: "center",
        justifyContent: "flex-start"
      }}
    >
      <Checkbox.Android
        status={todo.isComplete ? "checked" : "unchecked"}
        onPress={() => {}}
      />
      <Text>{todo.task}</Text>
    </View>
  );
};
  • 対象 UI
    • タスクの追加
// src/AddToDo.js

import React, { useState } from "react";
import { View } from "react-native";
import { TextInput, Button } from "react-native-paper";

export const AddToDo = () => {
  const [text, setText] = useState("");
  return (
    <View style={{ marginBottom: 20 }}>
      <TextInput
        label="Add ..."
        returnKeyType="done"
        value={text}
        onChangeText={text => setText(text)}
        keyboardType="default"
        style={{ margin: 5 }}
      />
      <Button mode="contained" onPress={() => {}}>
        Add Todo
      </Button>
    </View>
  );
};

作成した UI コンポーネントを src/globalStateExample.js に反映します。

import React from "react";
import { View, StyleSheet } from "react-native";
import { Filter } from "./Filter";
import { TodoList } from "./TodoList";
import { AddToDo } from "./AddToDo";

const GlobalStateWithReactContext = () => {
  return (
    <View style={styles.container}>
      <Filter />
      <TodoList />
      <AddToDo />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1
  }
});

export default GlobalStateWithReactContext;

まだ、ボタンや Checkbox をクリックしても反応しません。

これから Context Store を作成して実際にTodo リストを管理できるように設定していきます。

Context Store を作成

最初に、Redux Store に該当する React Context ( TodoContext ) を作成します。

Redux を使用する場合

Redux Store 作成」に該当します。( Step 5 )

// src/todoContext.js

import React from "react";
const TodoContext = React.createContext(null);
export default TodoContext;

React.useReducer() を設定

作成した TodoContextReact.useReducer() を利用するため、
Todo アプリの初期値 ( initialTodos ) を作成します。▶︎ initialArg

const [state, dispatch] = useReducer(reducer, initialArg, init);

// src/initialTodos.js

import "react-native-get-random-values";
import { v4 as uuidv4 } from "uuid";
import { seed } from "./utils/uuidSeed";

export const initialTodos = [
  {
    id: uuidv4({ random: seed() }),
    task: "Learn React Native",
    isComplete: true
  },
  {
    id: uuidv4({ random: seed() }),
    task: "Learn Redux",
    isComplete: true
  },
  {
    id: uuidv4({ random: seed() }),
    task: "Learn React Native Paper",
    isComplete: false
  },
  {
    id: uuidv4({ random: seed() }),
    task: "Learn React Redux",
    isComplete: true
  }
];

src/utils/uuidSeed.js を作成しておきます。

次はReducerを作成します。▶︎ reducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

// src/todoReducer.js

import "react-native-get-random-values";
import { v4 as uuidv4 } from "uuid";
import { seed } from "./utils/uuidSeed";

export const todoReducer = (state, action) => {
  switch (action.type) {
    case "DONE_TODO":
      return state.map(todo => {
        if (todo.id === action.id) {
          return { ...todo, isComplete: true };
        } else {
          return todo;
        }
      });
    case "UNDO_TODO":
      return state.map(todo => {
        if (todo.id === action.id) {
          return { ...todo, isComplete: false };
        } else {
          return todo;
        }
      });
    case "ADD_TODO":
      return state.concat({
        id: uuidv4({ random: seed() }),
        task: action.task,
        isComplete: false
      });
    default:
      throw new Error();
  }
};

作成した TodoContextinitialTodostodoReducer src/globalStateExample.js に反映します。

TodoContext.ProvidervaluetodoReducer を渡し、Child コンポーネントをラップしておきます。

Redux を使用する場合

Redux Store を React Native アプリ ( Root ) へパスする」作業に該当します。( Step 7 )

// src/globalStateExample.js

import React from "react";
import { View, StyleSheet } from "react-native";
import TodoContext from "./todoContext";
import { initialTodos } from "./initialTodos";
import { todoReducer } from "./todoReducer";
import { Filter } from "./Filter";
import { TodoList } from "./TodoList";
import { AddToDo } from "./AddToDo";

const GlobalStateWithReactContext = props => {
  const [todos, dispatchTodos] = React.useReducer(todoReducer, initialTodos);

  return (
    <TodoContext.Provider value={dispatchTodos}>
      <View style={styles.container}>
        <Filter />
        <TodoList />
        <AddToDo />
      </View>
    </TodoContext.Provider>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1
  }
});

export default GlobalStateWithReactContext;

次は、フィルタリングの設定を行います。

// src/filterReducer.js

export const filterReducer = (state, action) => {
  switch (action.type) {
    case "SHOW_ALL":
      return "SHOW_ALL";
    case "SHOW_COMPLETE":
      return "SHOW_COMPLETE";
    case "SHOW_INCOMPLETE":
      return "SHOW_INCOMPLETE";
    default:
      throw new Error();
  }
};

src/globalStateExample.js に反映します。

// src/globalStateExample.js

import React from "react";
import { View, StyleSheet } from "react-native";
import TodoContext from "./todoContext";
import { initialTodos } from "./initialTodos";
import { todoReducer } from "./todoReducer";
import { filterReducer } from "./filterReducer";
import { Filter } from "./Filter";
import { TodoList } from "./TodoList";
import { AddToDo } from "./AddToDo";

const GlobalStateWithReactContext = props => {
  const [todos, dispatchTodos] = React.useReducer(todoReducer, initialTodos);
  const [filter, dispatchFilter] = React.useReducer(filterReducer, "SHOW_ALL");

  const filteredTodos = todos.filter(todo => {
    if (filter === "SHOW_ALL") return true;
    if (filter === "SHOW_COMPLETE" && todo.isComplete) return true;
    if (filter === "SHOW_INCOMPLETE" && !todo.isComplete) return true;
    return false;
  });

  return (
    <TodoContext.Provider value={dispatchTodos}>
      <View style={styles.container}>
        <Filter dispatch={dispatchFilter} />
        <TodoList todos={filteredTodos} />
        <AddToDo />
      </View>
    </TodoContext.Provider>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1
  }
});

export default GlobalStateWithReactContext;

まだ、ボタンや Checkbox をクリックしても反応しません。

反応させるには、UI コンポーネント側でアクションクリエーターを作成しアクション毎に Context Store ( TodoContext )dispatch() して State を更新する設定が必要です。

Redux を使用する場合

React Native アプリを Redux Store へコネクトする」作業に該当します。( Step 6 )

Filter.js を修正します。

  • ボタンをクリックすると該当するアクションが実行され、各 State 値が変更されます。
// src/Filter.js

import React from "react";
import { View } from "react-native";
import { Button } from "react-native-paper";

export const Filter = ({ dispatch }) => {
  const handleShowAll = () => {
    dispatch({ type: "SHOW_ALL" });
  };

  const handleShowComplete = () => {
    dispatch({ type: "SHOW_COMPLETE" });
  };

  const handleShowIncomplete = () => {
    dispatch({ type: "SHOW_INCOMPLETE" });
  };

  return (
    <View
      style={{
        alignItems: "center",
        justifyContent: "center",
        flexDirection: "row",
        marginVertical: 10
      }}
    >
      <Button onPress={handleShowAll}>All</Button>
      <Button onPress={handleShowComplete}>Complete</Button>
      <Button onPress={handleShowIncomplete}>Incomplete</Button>
    </View>
  );
};

TodoList.js ファイルを修正します。

  • initialTodos を削除し todos props に変更します。
import React from "react";
import { View } from "react-native";
import { TodoItem } from "./TodoItem";

export const TodoList = ({ todos }) => {
  return (
    <View>
      {todos.map(todo => {
        return <TodoItem key={todo.id} todo={todo} />;
      })}
    </View>
  );
};

TodoItem.js ファイルを修正します。

  • Checkbox をクリックする度にタスクのステータスを「完了」、「進行中」に切り替えます。
import React, { useContext } from "react";
import { View, Text } from "react-native";
import { Checkbox } from "react-native-paper";
import TodoContext from "./todoContext";

export const TodoItem = ({ todo }) => {
  const dispatch = useContext(TodoContext);

  const handleChange = () => {
    dispatch({
      type: todo.isComplete ? "UNDO_TODO" : "DONE_TODO",
      id: todo.id
    });
  };

  return (
    <View
      style={{
        flexDirection: "row",
        paddingLeft: 40,
        alignItems: "center",
        justifyContent: "flex-start"
      }}
    >
      <Checkbox.Android
        status={todo.isComplete ? "checked" : "unchecked"}
        onPress={handleChange}
      />
      <Text>{todo.task}</Text>
    </View>
  );
};

Context Store ( TodoContext ) の中身が変わるので expo サーバを再起動し iOS simulator もリフレッシュしておきましょう。

表示されたタスクから、チェックを入れたり外したりして「ALL」「COMPLETE」「INCOMPLETE」 で表示リストが変われば OK です。

最後の作業はタスクを追加する設定です。

AddToDo.js ファイルを修正します。

  • 新しいタスクがあれば Store に反映します。
import React, { useState, useContext } from "react";
import { View } from "react-native";
import { TextInput, Button } from "react-native-paper";
import TodoContext from "./todoContext";

export const AddToDo = () => {
  const dispatch = useContext(TodoContext);
  const [text, setText] = useState("");

  const handleChangeText = () => {
    if (text) {
      dispatch({
        type: "ADD_TODO",
        task: text
      });
    }
    setText("");
  };
  return (
    <View style={{ marginBottom: 20 }}>
      <TextInput
        label="Add ..."
        returnKeyType="done"
        value={text}
        onChangeText={text => setText(text)}
        keyboardType="default"
        style={{ margin: 5 }}
      />
      <Button mode="contained" onPress={handleChangeText}>
        Add Todo
      </Button>
    </View>
  );
};

これで完了です!

App Test

早速、新しいタスクを追加して同じくチェックを入れたり、表示オプションを変えたりしながら動作を確認してみましょう。冒頭の動画のように動くと思います。

Context Store の Stateを確認:▶︎ todos

  • src/globalStateExample.jsconsole.log(todos) を入れてターミナルで確認しますと以下の通りです。( New Item というタスクを追加した場合 )
Array [
  Object {
    "id": "18100319-0015-4105-8206-01041010170e",
    "isComplete": true,
    "task": "Learn React Native",
  },
  Object {
    "id": "17070b05-0f07-480a-8d00-190717140611",
    "isComplete": true,
    "task": "Learn Redux",
  },
  Object {
    "id": "18050502-090d-410c-8616-010607150e0d",
    "isComplete": false,
    "task": "Learn React Native Paper",
  },
  Object {
    "id": "00070009-1603-4704-990b-140015150311",
    "isComplete": true,
    "task": "Learn React Redux",
  },
  Object {
    "id": "01051806-0b03-4701-8f02-021319170809",
    "isComplete": false,
    "task": "New Item",
  }
]

まとめ

React Context APIState を管理するには、useContext()useReducer() を使用するので、Redux の動作方法を知ることが前提になるかと思いますが、Redux より設定方法は簡単 ( RTK を使うような感じ ) でパッケージの追加も不要で便利かと。

しかし、
redux-logger のように Store の変化をリアルタイムで確認できる環境も大事かと思います。全体のコードは GitHub Repo で確認できます。

※ Redux の設定方法は ▶︎ Redux ToolkitでReduxを楽に使う〜React-Native〜

参照文献

スポンサーリンク

コメント

タイトルとURLをコピーしました