【useState/ReactRedux】Reactにおける状態管理

## 概要

- Reactにおける状態管理のしかたをReact標準で備える`useState`を使ったものとReactにおけるReduxライブラリである`ReactRedux`を使ったものを紹介します

## サンプルアプリの準備

- この後の説明をするためのサンプルを作っておきます
- 必要なライブラリを追加

```sh
yarn add react react-dom react-router-dom parcel-bundler
```

<details><summary>`index.html`を作成</summary><div>

```html:index.html
<div id="root"></div>
<script src="index.js"></script>
```

</div></details>
<details><summary>`index.js`を作成</summary><div>

```jsx:index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './src/App';

ReactDOM.render(<App />, document.getElementById('root'));
```

</div></details>
<details><summary>`src/App.js`の作成</summary><div>

```jsx:src/App.js
import React from 'react';
import Router from './routes/router';

function App() {
  return <Router />;
}

export default App;
```

</div></details>
<details><summary>`src/routes/router.js`を作成</summary><div>

```jsx: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;
```

</div></details>
<details><summary>`src/components/Home.js`を作成</summary><div>

```jsx: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;
```

</div></details>
<details><summary>`src/components/Couner.js`を作成</summary><div>

```jsx: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;
```

</div></details>
<details><summary>`src/components/TodoList.js`を作成</summary><div>

```jsx: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;
```

</div></details>

- 以下のコマンドで起動すると http://localhost:1234 でアクセスできます

```sh
npx parcel index.html
```

- ここまでだとこんな感じです

![init.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/d17714e9-3b30-8b1e-7490-00da35527bb4.gif)

## useStateを使った状態管理
### Counterアプリ

![counter.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/20adc0c4-3095-70d4-0379-9288f93ec0a8.gif)

- `＋`を押すとインクリメントされて`ー`を押すとデクリメントされるサンプルです
    - 現在の値をメモリ上で保持しておく必要があるためそこで状態管理が発生するわけですね
- `src/components/Counter.js`を以下の内容に変更します
    - 1行目で`useState`のimportが追加されているようにReactが標準で提供する`useState`を使います

```jsx: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.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/6d0ab6dc-88bc-06f2-1d23-03bece68e2c6.gif)

- TodoListアプリを作ってみます
- 現在のTodoのリストや現在の入力された値をuseStateを使って管理してみます

```jsx: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`はコンポーネント単位で宣言しています
- なのでコンポーネントが破棄されると管理していた状態もすべて破棄されてしまいます
- サンプルアプリでページ遷移して戻ってくると値がリセットされていることからも分かるかと思います

![reset-state.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/5e4e657d-bd1a-3626-fd10-3fb3ef044188.gif)

## ReactReduxを使った状態管理

- `useState`はコンポーネント単位で状態を管理していましたが、ReactReduxはアプリ全体で状態の管理をします
    - `single source of truth`と言われるように状態はアプリ全体で一箇所で管理するという考え方です
- ReactReduxを使うと、状態の管理や状態を更新するための処理がコンポーネントから切り離され役割が分離されるのが特徴です

### ReactReduxのセットアップ

- 必要なライブラリを追加します
    - 今回は`redux-toolkit`も使います
    - Reduxチームが作っているライブラリでこれを使うと実装量が大きく減少します

```sh
yarn add react-redux @reduxjs/toolkit
```

- ReactReduxの設定周りのファイルを作成します
- `src/store/index.js`を作成します

```js:src/store/index.js
import { configureStore, combineReducers } from '@reduxjs/toolkit';

export const store = configureStore({
  reducer: combineReducers({
    // この中は後で作る
  }),
});
```

- `src/App.js`に適用します

```jsx: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`を作成します

```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`に反映させます

```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`を作成します

```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`を作りました
- 最後に`src/components/Counter.js`で`useCounter`を使うように修正します

```jsx: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`からの置き換え完了です
- ページ遷移をしても状態が維持されることを確認しましょう

![counter-redux.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/25c9387f-6cc4-0eb9-d8ef-c120114c141b.gif)

### TodoListアプリ

- 同じ要領でTodoListもReactRedux化していきましょう
- まずは`src/store/todoSlice.js`を作成します

```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`に反映させます

```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`を作成します

```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`を修正します

```jsx: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;
```

- これで完成です
- 動作確認してみましょう

![todolist-redux.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/e7804ef7-3e32-c10c-6da4-d0ef0272cd9b.gif)

### ReactReduxの特徴

- `useState`の時は状態管理はコンポーネントに閉じていたためコンポーネントが破棄されると状態も破棄されていました
- ReactReduxを使うとコンポーネントの外で状態を管理するためコンポーネントが破棄されても状態を維持することができます
    - それにともなってソースコードも状態管理とコンポーネントと分離されてGoodですね
- 状態がコンポーネントの外で管理されているためどのコンポーネントからでも状態にアクセス可能にもなります
    - 試しに`src/components/Home.js`を以下のようにしてみましょう

```jsx: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コンポーネントからでも管理している状態にアクセスできることが確認できます

![home-redux.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175213/7cf5de3d-51f1-1a98-a2bc-06b9212b4b42.gif)

- どのコンポーネントからでもアクセスはできますが、あくまでメモリ上で保持しているだけなのでリロードすると値は消えてしまいます
    - 永続化したい場合はサーバに送信して保存しておく必要があります

## まとめ

- `useState`を使ったコンポーネント内での状態管理と、ReactReduxを使ったアプリ全体での状態管理を紹介しました
- ReactReduxを使うと登場人物が増えて複雑になってくるところもあるので規模や要件に応じて使い分けるとよいでしょう

## 蛇足

- 今回の例では問題は起きないが`useTodo`の中で`useSelector(selectInputText)`と`useSelector(selectTodoList)`を呼んでいるため、どちらか片方しか使わないコンポーネントがもう一方の値が更新された時にも再レンダリングされてしまう
- `useSelector`の存在意義からしてもコンポーネントで必要なものだけ`useSelector`する方がいいのかもしれない