🍖

【入門編】Reduxによる状態管理の仕組みを理解する

2021/03/21に公開

まず結論から

Reduxを理解する上では、この図解(引用元:Redux Application Data Flow)を理解できれば90%はOK。

  1. ユーザーの入力から、(ActionCreatorが)Action(アクション)を作成する
  2. Store(状態を管理するところ)へActionをDispatch(送信)する
  3. DispatchされたActionをReducer(状態を変更するところ)へ渡す
  4. Reducerが作成した新しいStateをStoreが保存する
  5. Storeからデータを参照してView(画面)に描画する

「ここが間違ってる!」や「もっとこんな仕組みが使われてるよ!」等のツッコミがあれば、どしどし貰えると助かります!(そのために書いてるみたいなところも多いので!)

Reduxとは

Reduxは、Reactが扱うUIのstate(状態)を管理をするためのフレームワークです。
image.png

Reactではstateの管理するデータフローにFluxを提案していますが、ReduxはFluxの概念を拡張してより扱いやすく設計されています。
React以外にもAngularJSやjQueryなどと併用して使用することもできますが、Reactと使用するのがベターです。

Redux - GitHub

Reduxの3原則(Three Principles)

Reduxの基本設計は以下の3つの原則に基づいて設計されています。
順を追って見ていきましょう。

Single source of truth(ソースは1つだけ)

この原則は以下のような要約できます。

  • アプリケーション全体の状態(State)はツリーの形で1つのオブジェクトで作られ、1つのストアに保存される
  • Stateが保存しやすいので、ユニバーサルアプリケーションがつくりやすい
  • Stateが1つだから、デバッグしやすい、開発しやすい

以下の図は、Redux DevToolsで使ってサンプルアプリケーションのStateの構成を示しているものです。
図解

つまり、以下のようにStoreを複数作成することはできません。

const store = createStore(sample);
const store2 = createStore(sample);

State is read-only(状態は読み取り専用)

この原則は、

  • 状態を変更する手段は、変更内容をもったActionオブジェクトを発行して実行するだけ
  • ビューやコールバックが状態を直接変更させることはできない
  • 変更は一つずつ順番に行なわれる
  • Actionはオブジェクトなので、保存可能であり、テストしやすい

ということを示してます。

Mutations are written as pure functions(変更はすべてpureな関数で書かれる)

そして、これが最後の原則です。

  • アクションがどのように状態を変更するかを「Reducer」で行う
  • 「Reducer」は状態とアクションを受けて、新しい状態を返す関数である
  • 現在のStateオブジェクトを変更することはせずに、新しいStateオブジェクトを作って返すというのがポイント
  • 開発するときは、最初はアプリケーションで一つのReducerを用意して、巨大化してきたらReducerを分割していく

State is read-only

全体構造

ここで、改めて全体構造(引用元:
Redux. From twitter hype to production)を確認してみましょう。
今回の図は、はじめに見せた図よりもより抽象化したものです。
新しい要素もありますが、このあとに解説にしますのでここでは気にしないでください。

redux図解

  1. ユーザーの入力から、ActionCreatorがActionを作成する
  2. StoreへActionをDispatch(送信)する
  3. DispatchされたActionをReducerへ渡す
  4. Reducerが作成した新しいStateをStoreが保存する
  5. Storeからデータを参照してViewに描画する

では、具体的なアプリケーションを例に出して解説していきたいのですが、
その前にざっくり構成要素の説明をしておきます。

構成要素

構成要素は、以下の7つです。
1つずつ見ていきましょう。

  • View
  • ActionCreator
  • Action
  • Middleware
  • State
  • Reducer
  • Store

View: ユーザーの見る画面

Viewとは、実際にユーザーに表示される画面のことを指します。
ユーザーがボタンをクリックしたり、テキストを入力することでイベントが発行されるので、この画面へイベントが処理の起点となります。

redux_fig3.png

ActionCreator: アクションを生成する

ActionCreatorは、その名の通り、アクションを生成する処理を行います。
図解されている部分では、青いダイアログで示されているActionsに対応しています。

redux_fig4.png

Action: ユーザーの入力したアクション

Actionとは、ユーザーが入力したアクションのことを指します。
例えば、ボタンをクリックしたり、テキストを入力することを指しています。
他にもあるボタンをクリックしたときに複数の処理が発行される場合、単一のユーザー入力に対して複数のアクションが発行されることもあります。

redux_fig5.png

Middleware: アクションをウォッチして、副作用を取り込む

Middlewareとは、アクションは発行されることを監視して、ある特定の処理に対して副作用を取り込む処理を行います。
例えば、ボタンをクリックした際にサーバーへAPIを叩きに行く処理が発生する場合は、このMiddlewareがAPIへの接続の処理を担っています。
逆に、ただ、テキストを入力するだけなどのシンプルな処理、つまり、Viewの状態を変更するだけでサーバーとのやり取りを行わない・副作用を含まないアクションでは、Middlewareでの処理を介しません。

redux_fig6.png

Reducer: アプリケーションの状態を書き換える事が唯一できる

Reducerとは、アプリケーションの状態を書き換える事が唯一できるものです。
Reduxの基本設計の1つであるSingle source of truth(ソースは1つだけ)でもあるようにStateを書き換える事ができるのはこのReducerのみです。
アクションから処理を伝搬する役割を持っています。

redux_fig7.png

State: アプリケーションの状態

Stateとは、アプリケーションの状態を示しています。
例えば、チェックボックスをチェックしているとか、APIを叩いてデータを取得できたなどの状態を指します。

redux_fig8.png

Store: アプリケーションの状態を保存する

Storeとは、アプリケーションの状態を保存する場所です。
Stateを格納しており、ReducerからStateを変更された場合、Storeに保存されているStateが書き換わります。

redux_fig9.png

処理の流れ

では、具体的にタスク管理TODOアプリを例に処理の流れを見ていきましょう。

こちらがタスク管理TODOアプリです。

ユーザーの入力により、Actionを発行する

TODOリストの項目を追加するために「筋トレをする」と入力して、「追加」のボタンをクリックしてタスクを追加するユースケースでみてみましょう。

「筋トレをする」と入力

まず、タスクを追加するというアクションを定義します。

actionType.js
export const ADD_TODO = "ADD_TODO";
actions.js
export const addTodo = content => ({
  type: ADD_TODO,
  payload: {
    id: ++nextTodoId,
    content
  }
});

そして、ここに定義したアクションをViewからdispatch(送信)するために、InputWithButtonに処理を追加します。(UIコンポーネント自体にstateを持たせる+ドメイン知識を持たせるのは、場合によってはアンチパターンですが、ここでは処理の流れを理解してもらうためにあえてこのカタチで説明します。)

InputWithButton

InputWithButton.js
class InputWithButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = { input: "" };
  }

  updateInput = (input) => {
    this.setState({ input });
  };

  handleAddTodo = () => {
    this.props.addTodo(this.state.input);
    this.setState({ input: "" });
  };

  render() {
    return (
      <div>
        <input
          onChange={(e) => this.updateInput(e.target.value)}
          value={this.state.input}
        />



        <button className="add-todo" onClick={this.handleAddTodo}>
          追加
        </button>
      </div>
    );
  }
}

ここでは、inputへ入力したテキストは、

this.state = { input: "" };

このようにローカルのStateに格納されて、追加ボタンを押したタイミングでActionがdispatchされます。

<button className="add-todo" onClick={this.handleAddTodo}>
  追加
</button>

また、handleAddTodoはactionをdispatchするのと同時にinputを空にする処理もしています。

handleAddTodo = () => {
this.props.addTodo(this.state.input);
this.setState({ input: "" });
};

ADD_TODOは予め定義されていたアクションです。
ここで、アクションを生成するのがActionCreatorです。

伝搬(dispatch)されたActionをReducerへ渡し、ReducerがStateを更新してStoreに保存する

ReducerはdispatchされたActionを受け取り、そのActionによってStoreに保存されているStoreを書き換える処理を実行します。

redux/reducer/todos.js
const initialState = {
  allIds: [],
  byIds: {}
};

export default function(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO: {
      const { id, content } = action.payload;
      return {
        ...state,
        allIds: [...state.allIds, id],
        byIds: {
          ...state.byIds,
          [id]: {
            content,
            completed: false
          }
        }
      };
    }
    ...

allIdsとはすべてのタスクを示しており、ADD_TODOというアクションがdispatchされると、そのallIdsというstateの中に、新しく作成したTODOタスクが追加されます。

Storeからデータを参照してViewに描画する

新しくTODOタスクを追加すると、Viewにもその変更が反映されます。

新しくタスクが追加された状態

ここでは、TodoListというコンポーネントがStoreに保存されているStateを監視しているため、即座に反映されます。

TodoList

TodoList.js
const TodoList = ({ todos }) => (
  <ul className="todo-list">
    {todos && todos.length
      ? todos.map((todo, index) => {
          return <TodoItem key={`todo-${todo.id}`} todo={todo} />;
        })
      : "タスクがありません。"}
  </ul>
);

const mapStateToProps = (state) => {
  const { visibilityFilter } = state;
  const todos = getTodosByVisibilityFilter(state, visibilityFilter);
  return { todos };
};
export default connect(mapStateToProps)(TodoList);

getTodosByVisivikityFilterの定義元へ移動してみます。
ここではじめてselectorというものがでてきましたが、
これはstoreのstateを取得する際に加工・選択(select)してからviewへデータを投げてくれる人です。
今回の場合は、フィルターを「すべて・完了・未完了」によって取得するデータを整形するという処理をしてます。

selector.js
export const getTodosByVisibilityFilter = (store, visibilityFilter) => {
  const allTodos = getTodos(store);
  switch (visibilityFilter) {
    case VISIBILITY_FILTERS.COMPLETED:
      return allTodos.filter(todo => todo.completed);
    case VISIBILITY_FILTERS.INCOMPLETE:
      return allTodos.filter(todo => !todo.completed);
    case VISIBILITY_FILTERS.ALL:
    default:
      return allTodos;
  }
};

なので、おおもとのstoreからデータを持ってくる処理とはこのgetTodosが行っています。

selector.js
export const getTodos = store =>
getTodoList(store).map(id => getTodoById(store, id));

これで、todoタスクを追加した際に行われている一連の流れを理解することができました。

今回は、サーバーとのやり取りを行わないシンプルなアプリケーションだったので、Middlewareなどは介在しないシンプルな処理になりましたが、実際にサービスをつくるとなった際にはもっと複雑な設計が求められるでしょう。
これについては次回の記事で。

少しでも、Reduxを学ぶ人の手助けができたら嬉しいです!
アドバイスや質問などのあればtwitterのDMやコメントでもいいので是非是非お待ちしてますー!

Discussion