概要
- Reactにおける状態管理のしかたをReact標準で備える
useState
を使ったものとReactにおけるReduxライブラリであるReactRedux
を使ったものを紹介します
サンプルアプリの準備
- この後の説明をするためのサンプルを作っておきます
- 必要なライブラリを追加
yarn add react react-dom react-router-dom parcel-bundler
`index.html`を作成
index.html
<div id="root"></div>
<script src="index.js"></script>
`index.js`を作成
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './src/App';
ReactDOM.render(<App />, document.getElementById('root'));
`src/App.js`の作成
src/App.js
import React from 'react';
import Router from './routes/router';
function App() {
return <Router />;
}
export default App;
`src/routes/router.js`を作成
src/routes/router.js
import React from 'react';
import { BrowserRouter, Route } from 'react-router-dom';
import Home from '../components/Home';
import Counter from '../components/Counter';
import TodoList from '../components/TodoList';
function Router() {
return (
<BrowserRouter>
<div>
<Route path="/" exact>
<Home />
</Route>
<Route path="/counter" exact>
<Counter />
</Route>
<Route path="/todo-list" exact>
<TodoList />
</Route>
</div>
</BrowserRouter>
);
}
export default Router;
`src/components/Home.js`を作成
src/components/Home.js
import React from 'react';
import { Link } from 'react-router-dom';
function Home() {
return (
<div>
<h1>Home</h1>
<p>
<Link to="/counter">Counterへ</Link>
<Link to="/todo-list">TodoListへ</Link>
</p>
</div>
);
}
export default Home;
`src/components/Couner.js`を作成
src/components/Counter.js
import React from 'react';
import { Link } from 'react-router-dom';
function Counter() {
return (
<div>
<h1>Counter</h1>
<p>
<Link to="/">Homeへ</Link>
<Link to="/todo-list">TodoListへ</Link>
</p>
</div>
);
}
export default Counter;
`src/components/TodoList.js`を作成
src/components/TodoList.js
import React from 'react';
import { Link } from 'react-router-dom';
function TodoList() {
return (
<div>
<h1>TodoList</h1>
<p>
<Link to="/">Homeへ</Link>
<Link to="/counter">Counterへ</Link>
</p>
</div>
);
}
export default TodoList;
- 以下のコマンドで起動すると http://localhost:1234 でアクセスできます
npx parcel index.html
- ここまでだとこんな感じです
useStateを使った状態管理
Counterアプリ
-
+
を押すとインクリメントされてー
を押すとデクリメントされるサンプルです- 現在の値をメモリ上で保持しておく必要があるためそこで状態管理が発生するわけですね
-
src/components/Counter.js
を以下の内容に変更します- 1行目で
useState
のimportが追加されているようにReactが標準で提供するuseState
を使います
- 1行目で
src/components/Counter.js
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
function Counter() {
// countは現在のカウントの値、setCountはcountを更新する関数、useStateの引数は初期値
const [count, setCount] = useState(0);
// countを+1する関数
const increment = () => setCount(count + 1);
// countを-1する関数
const decrement = () => setCount(count - 1);
return (
<div>
<h1>Counter</h1>
<p>{count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>ー</button>
<p>
<Link to="/">Homeへ</Link>
<Link to="/todo-list">TodoListへ</Link>
</p>
</div>
);
}
export default Counter;
- ボタンを押すとそれぞれ
increment
/decrement
関数を呼び出しています-
increment
/decrement
の中ではsetCount
を実行しています -
useState
で生成したsetXxx
を実行し値が更新されると、最新の値で自動的に再描画が走るというのが特徴です
-
-
useState
の動きをなんとなくつかんでもらえたのではないでしょうか?
TodoListアプリ
- TodoListアプリを作ってみます
- 現在のTodoのリストや現在の入力された値をuseStateを使って管理してみます
src/components/TodoList.js
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
function TodoList() {
// todoListを管理するstateを宣言
const [todoList, setTodoList] = useState([]);
// 入力内容を管理するstateを宣言
const [inputText, setInputText] = useState('');
// 入力内容が変化をstateにsetする関数
const onChangeText = event => setInputText(event.target.value);
// 入力内容をTodoListに追加して入力域を空にする関数
const onClickAdd = () => {
setTodoList([...todoList, { text: inputText }]);
setInputText('');
};
return (
<div>
<h1>TodoList</h1>
<input onChange={onChangeText} value={inputText} />
<button onClick={onClickAdd}>追加</button>
{todoList.map((todo, i) => (
<p key={i}>{todo.text}</p>
))}
<p>
<Link to="/">Homeへ</Link>
<Link to="/counter">Counterへ</Link>
</p>
</div>
);
}
export default TodoList;
- Stateを複数管理したい場合はこのように
useState
を必要なだけ宣言すればよいということがわかるかと思います -
useState
の使い方に慣れてきましたか?
useStateの特徴
-
useState
はコンポーネント単位で宣言しています - なのでコンポーネントが破棄されると管理していた状態もすべて破棄されてしまいます
- サンプルアプリでページ遷移して戻ってくると値がリセットされていることからも分かるかと思います
ReactReduxを使った状態管理
-
useState
はコンポーネント単位で状態を管理していましたが、ReactReduxはアプリ全体で状態の管理をします-
single source of truth
と言われるように状態はアプリ全体で一箇所で管理するという考え方です
-
- ReactReduxを使うと、状態の管理や状態を更新するための処理がコンポーネントから切り離され役割が分離されるのが特徴です
ReactReduxのセットアップ
- 必要なライブラリを追加します
- 今回は
redux-toolkit
も使います - Reduxチームが作っているライブラリでこれを使うと実装量が大きく減少します
- 今回は
yarn add react-redux @reduxjs/toolkit
- ReactReduxの設定周りのファイルを作成します
-
src/store/index.js
を作成します
src/store/index.js
import { configureStore, combineReducers } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: combineReducers({
// この中は後で作る
}),
});
-
src/App.js
に適用します
src/App.js
import React from 'react';
import { Provider } from 'react-redux';
import Router from './routes/router';
import { store } from './store';
function App() {
return (
<Provider store={store}>
<Router />
</Provider>
);
}
export default App;
- これで準備OKです
Counterアプリ
-
useState
を使ったCounterアプリをReactRedux化していきます - まずはCounter周りの状態管理と状態更新を定義するファイルを作成します
-
src/store/counterSlice.js
を作成します
src/store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
export const counterSlice = createSlice({
name: 'counter',
// Stateの初期値を設定
initialState: {
count: 0,
},
// 状態を更新する関数を定義する場所
reducers: {
// countを+1する処理
increment(state) {
return { count: state.count + 1 };
},
// countを-1する処理
decrement(state) {
return { count: state.count - 1 };
},
},
});
// reducersに定義した処理を呼び出すActionをexportする
export const { increment, decrement } = counterSlice.actions;
// 現在のcountの値を取得するためのSelectorをexportする
export const selectCount = ({ counter }) => counter.count;
// お作法としてdefault exportでreducerをexport
export default counterSlice.reducer;
-
useState
のパターンでCounter.js
に定義されていた状態管理と状態更新の処理がこちらに移動してきました- 書き方は独特ですがひとつひとつの処理は見れば分かると思います
- 次に今作ったファイルを
src/store/index.js
に反映させます
src/store/index.js
import { configureStore, combineReducers } from '@reduxjs/toolkit';
// importを追加
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: combineReducers({
// 定義を追加
counter: counterReducer,
}),
});
- 次に
counterSlice
に定義したものをコンポーネントが使いやすいようにワンクッション挟むファイルを作ります- このファイルでやってることを直接コンポーネントで書いても動きます
-
src/hooks/useCounter.js
を作成します
src/hooks/useCounter.js
import { useSelector, useDispatch } from 'react-redux';
// 現在のcountを取得するためのselectCount、countを更新するためのincrementとdecrementをimport
import { selectCount, increment, decrement } from '../store/counterSlice';
function useCounter() {
const dispatch = useDispatch();
// count,increment, decrementをすぐに使える状態で返す
return {
count: useSelector(selectCount),
increment: () => dispatch(increment()),
decrement: () => dispatch(decrement()),
};
}
export default useCounter;
-
counterSlice
でexportしたものはSelect系はuseSelector
を、Action系はuseDispatch
を通さないと使うことができません- コンポーネント側にReactReduxを意識したコードを増やしたくないのでそれを隠蔽するために
useCounter.js
を作りました
- コンポーネント側にReactReduxを意識したコードを増やしたくないのでそれを隠蔽するために
- 最後に
src/components/Counter.js
でuseCounter
を使うように修正します
src/components/Counter.js
import React from 'react';
import { Link } from 'react-router-dom';
// importを追加
import useCounter from '../hooks/useCounter';
function Counter() {
// 以下の3行が不要になった
// const [count, setCount] = useState(0);
// const increment = () => setCount(count + 1);
// const decrement = () => setCount(count - 1);
// useCounterから値を取得
const { count, increment, decrement } = useCounter();
// 以下修正なし
return (
<div>
<h1>Counter</h1>
<p>{count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>ー</button>
<p>
<Link to="/">Homeへ</Link>
<Link to="/todo-list">TodoListへ</Link>
</p>
</div>
);
}
export default Counter;
- 状態管理や状態更新をしていたコードが全て不要になり
useCounter
から取得するものに置き換わりました - これで
useState
からの置き換え完了です - ページ遷移をしても状態が維持されることを確認しましょう
TodoListアプリ
- 同じ要領でTodoListもReactRedux化していきましょう
- まずは
src/store/todoSlice.js
を作成します
src/store/todoSlice.js
import { createSlice } from '@reduxjs/toolkit';
// Slice
export const todoSlice = createSlice({
name: 'todo',
// Stateの初期値を設定
initialState: {
inputText: '',
todoList: [],
},
// 状態を更新する関数を定義する場所
reducers: {
changeText(state, action) {
return {
// returnしたものがStateにセットされるため変更がないものはそのままになるように`...state`をセットしている
...state,
// 引数で渡された値はaction.payloadで取得できる
inputText: action.payload.inputText,
};
},
add(state, action) {
return {
...state,
todoList: [...state.todoList, { text: action.payload.text }],
};
},
},
});
// reducersに定義した処理を呼び出すActionをexportする
export const { changeText, add } = todoSlice.actions;
// 現在のcountの値を取得するためのSelectorをexportする
export const selectInputText = ({ todo }) => todo.inputText;
export const selectTodoList = ({ todo }) => todo.todoList;
// お作法としてdefault exportでreducerをexport
export default todoSlice.reducer;
- Counterの時との違いは管理する状態が複数あることと、Actionの実行時に引数を受け取ることです
-
reducers
の中でreturnする時に変更しない値も返す必要があるので...state
を一番上に入れておく -
reducers
の中の関数で第2引数で受け取るaction
からaction.payload
でAction実行時に渡された引数を取得できる
-
-
src/store/index.js
に反映させます
src/store/index.js
import { configureStore, combineReducers } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
// importを追加
import todoReducer from './todoSlice';
export const store = configureStore({
reducer: combineReducers({
counter: counterReducer,
// 定義を追加
todo: todoReducer,
}),
});
- 続いてコンポーネントが使いやすいようにワンクッション挟む
useTodo
を作成します -
src/hooks/useTodo.js
を作成します
src/hooks/useTodo.js
import { useSelector, useDispatch } from 'react-redux';
import { selectTodoList, selectInputText, changeText, add } from '../store/todoSlice';
function useTodo() {
const dispatch = useDispatch();
return {
inputText: useSelector(selectInputText),
todoList: useSelector(selectTodoList),
changeText: (inputText) => dispatch(changeText({ inputText })),
add: (text) => dispatch(add({ text })),
};
}
export default useTodo;
- 最後に
useTodo
をコンポーネントに適用します -
src/components/TodoList.js
を修正します
src/components/TodoList.js
import React from 'react';
import { Link } from 'react-router-dom';
// importを追加
import useTodo from '../hooks/useTodo';
function TodoList() {
// useTodoから値を取得
const { inputText, todoList, changeText, add } = useTodo();
// 呼び出す関数をchangeTextに変更
const onChangeText = event => changeText(event.target.value);
const onClickAdd = () => {
// 呼び出す関数をaddに変更
add(inputText);
// 呼び出す関数をchangeTextに変更
changeText('');
};
// 以下修正なし
return (
<div>
<h1>TodoList</h1>
<input onChange={onChangeText} value={inputText} />
<button onClick={onClickAdd}>追加</button>
{todoList.map((todo, i) => (
<p key={i}>{todo.text}</p>
))}
<p>
<Link to="/">Homeへ</Link>
<Link to="/counter">Counterへ</Link>
</p>
</div>
);
}
export default TodoList;
- これで完成です
- 動作確認してみましょう
ReactReduxの特徴
-
useState
の時は状態管理はコンポーネントに閉じていたためコンポーネントが破棄されると状態も破棄されていました - ReactReduxを使うとコンポーネントの外で状態を管理するためコンポーネントが破棄されても状態を維持することができます
- それにともなってソースコードも状態管理とコンポーネントと分離されてGoodですね
- 状態がコンポーネントの外で管理されているためどのコンポーネントからでも状態にアクセス可能にもなります
- 試しに
src/components/Home.js
を以下のようにしてみましょう
- 試しに
src/components/Home.js
import React from 'react';
import { Link } from 'react-router-dom';
// importを追加
import useCounter from '../hooks/useCounter';
import useTodo from '../hooks/useTodo';
function Home() {
// 値を取得
const { count } = useCounter();
const { todoList } = useTodo();
return (
<div>
<h1>Home</h1>
<p>現在のCounterの値: {count}</p>
<p>現在のTodoListの件数: {todoList.length}</p>
<p>
<Link to="/counter">Counterへ</Link>
<Link to="/todo-list">TodoListへ</Link>
</p>
</div>
);
}
export default Home;
- Homeコンポーネントからでも管理している状態にアクセスできることが確認できます
- どのコンポーネントからでもアクセスはできますが、あくまでメモリ上で保持しているだけなのでリロードすると値は消えてしまいます
- 永続化したい場合はサーバに送信して保存しておく必要があります
まとめ
-
useState
を使ったコンポーネント内での状態管理と、ReactReduxを使ったアプリ全体での状態管理を紹介しました - ReactReduxを使うと登場人物が増えて複雑になってくるところもあるので規模や要件に応じて使い分けるとよいでしょう
蛇足
- 今回の例では問題は起きないが
useTodo
の中でuseSelector(selectInputText)
とuseSelector(selectTodoList)
を呼んでいるため、どちらか片方しか使わないコンポーネントがもう一方の値が更新された時にも再レンダリングされてしまう -
useSelector
の存在意義からしてもコンポーネントで必要なものだけuseSelector
する方がいいのかもしれない