16
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-24

概要

  • 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;
npx parcel index.html
  • ここまでだとこんな感じです

init.gif

useStateを使った状態管理

Counterアプリ

counter.gif

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

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

reset-state.gif

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

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;
  • これで完成です
  • 動作確認してみましょう

todolist-redux.gif

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

home-redux.gif

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

まとめ

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

蛇足

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?