条件・関連リソース
- この記事のコードは react-with-redux-philosophy を 
React Native用に修正したものです。 - この記事の構成:実装 ( 
Doing) >>> 解説・説明 (Explaining) - Expo開発環境を整っていると想定します。( + シミュレータ )
 
- expo 36.0.0
 - react 16.9.0
 - react-native-paper 3.6.0
 
※ React Context API で State 管理を理解するには 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
※ uuid を React 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 Store 作成」に該当します。( Step 5 )
// src/todoContext.js import React from "react"; const TodoContext = React.createContext(null); export default TodoContext;
React.useReducer() を設定
作成した TodoContext は React.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();
  }
};
作成した TodoContext、initialTodos、todoReducer を src/globalStateExample.js に反映します。
TodoContext.Provider の value に todoReducer を渡し、Child コンポーネントをラップしておきます。
「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 を更新する設定が必要です。
「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を削除しtodosprops に変更します。
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.jsでconsole.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 API で State を管理するには、useContext()、useReducer() を使用するので、Redux の動作方法を知ることが前提になるかと思いますが、Redux より設定方法は簡単 ( RTK を使うような感じ ) でパッケージの追加も不要で便利かと。
しかし、
redux-logger のように Store の変化をリアルタイムで確認できる環境も大事かと思います。全体のコードは GitHub Repo で確認できます。
※ Redux の設定方法は ▶︎ Redux ToolkitでReduxを楽に使う〜React-Native〜
  
  
  
  

コメント