条件・関連リソース
- この記事のコードは 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 ) を作成します。
// 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
コンポーネントをラップしておきます。
// 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
を更新する設定が必要です。
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.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〜
コメント