条件・関連リソース
- この記事のコードは 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〜


コメント