📖

状態管理ライブラリであるReact(Redux ,Recoil)とVue(Vuex)を比較する

2023/10/13に公開

今回は多くのコンポーネント間でデータの共有やコンポーネントの再利用、非同期操作の管理などさまざまな場面で活用される状態管理ライブラリについて触れていきたいと思います。

早速初めにReactのRecoilについて触れていきます

React(Recoil)

  • Recoilでは、アトム(共有状態)からセレクタ(純粋関数)を経て、Reactコンポーネントへと流れるデータフロー・グラフを作成することができる。アトムは、コンポーネントがサブスクライブすることができる状態の単位。セレクタは、この状態を同期または非同期で変換します。

  • RecoilとTypeScriptを使用した簡単なコード例を示します。この例では、Recoilのアトムとセレクタを使用して、ToDoアプリケーションのタスクリストを管理します。

// ToDoListState.ts

import { atom, selector } from 'recoil';

interface Task {
  text: string;
  isComplete: boolean;
}

// アトム(共有状態)を作成
export const todoListState = atom<Task[]>({
  key: 'todoListState',
  default: [],
});

// タスクの統計情報を計算するセレクタを作成
export const todoListStats = selector({
  key: 'todoListStats',
  get: ({ get }) => {
    const todoList = get(todoListState);
    const totalTasks = todoList.length;
    const completedTasks = todoList.filter((task) => task.isComplete).length;
    const incompletedTasks = totalTasks - completedTasks;
    return {
      totalTasks,
      completedTasks,
      incompletedTasks,
    };
  },
});

上記のコードでは、todoListStateというアトム(共有状態)と、todoListStatsというセレクタ(純粋関数)を作成しています。todoListStateはToDoリストのタスクを管理し、todoListStatsはタスクの統計情報を計算します。

次に、Reactコンポーネントでこれらの状態を使用します。

// TodoApp.js

import React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { todoListState, todoListStats } from './ToDoListState';

function TodoApp() {
  const [taskInput, setTaskInput] = React.useState('');
  const [todoList, setTodoList] = useRecoilState(todoListState);
  const stats = useRecoilValue(todoListStats);

  const addTask = () => {
    setTodoList([...todoList, { text: taskInput, isComplete: false }]);
    setTaskInput('');
  };

  return (
    <div>
      <h1>ToDo App</h1>
      <input
        type="text"
        value={taskInput}
        onChange={(e) => setTaskInput(e.target.value)}
      />
      <button onClick={addTask}>Add Task</button>
      <ul>
        {todoList.map((task, index) => (
          <li key={index}>
            <input
              type="checkbox"
              checked={task.isComplete}
              onChange={() => {
                const updatedTodoList = [...todoList];
                updatedTodoList[index] = { ...task, isComplete: !task.isComplete };
                setTodoList(updatedTodoList);
              }}
            />
            {task.text}
          </li>
        ))}
      </ul>
      <div>
        <p>Total Tasks: {stats.totalTasks}</p>
        <p>Completed Tasks: {stats.completedTasks}</p>
        <p>Incompleted Tasks: {stats.incompletedTasks}</p>
      </div>
    </div>
  );
}

export default TodoApp;

このコードでは、ToDoリストのタスクの追加とチェックの切り替えが行われています。また、タスクの統計情報も表示されます。

Recoilを使用することで、コンポーネント間で簡単に状態を共有し、セレクタを使用してデータを変換することができる。

React(Redux)

  • 一貫した動作をし、異なる環境(クライアント、サーバー、ネイティブ)で動作し、テストが容易なアプリケーションの作成を支援します。その上、ライブコード編集とタイムトラベリングデバッガを組み合わせたような、開発体験を提供します。

  • 下記のコードはReduxを使用してアクション、リデューサー、ストアを設定し、Reactコンポーネント内でReduxの状態を読み取り、アクションをディスパッチしています。

Reduxの設定

// actions.ts

import { createAction } from '@reduxjs/toolkit';

export const increment = createAction<number>('counter/increment');
export const decrement = createAction<number>('counter/decrement');

// reducers.ts

import { createReducer } from '@reduxjs/toolkit';
import { increment, decrement } from './actions';

const initialState = {
  count: 0,
};

export const counterReducer = createReducer(initialState, (builder) => {
  builder
    .addCase(increment, (state, action) => {
      state.count += action.payload;
    })
    .addCase(decrement, (state, action) => {
      state.count -= action.payload;
    });
});

// store.ts

import { configureStore } from '@reduxjs/toolkit';
import { counterReducer } from './reducers';

const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

export default store;

次に、ReactコンポーネントでReduxを使用します。

// Counter.tsx

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './actions';

const Counter = () => {
  const count = useSelector((state) => state.counter.count);
  const dispatch = useDispatch();

  return (
    <div>
      <h2>Counter: {count}</h2>
      <button onClick={() => dispatch(increment(1))}>Increment</button>
      <button onClick={() => dispatch(decrement(1))}>Decrement</button>
    </div>
  );
};

export default Counter;

アプリケーション全体をセットアップ

// App.tsx

import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import Counter from './Counter';

function App() {
  return (
    <Provider store={store}>
      <div className="App">
        <Counter />
      </div>
    </Provider>
  );
}

export default App;

Vue(Vuex)

単純なグローバルオブジェクトとは異なり、Vuexのストアはリアクティブです。また、単方向のデータフローになるので、変更される状態の追跡が明示的です。
vue-devtoolsを使用すると、getterで得られる値、mutationをcommitした履歴などが確認できます。

以下の例では、ストアを作成し、状態(state)、ゲッター(getters)、ミューテーション(mutations)、アクション(actions)を含む典型的なVuexの機能を示しています。

import { createStore } from 'vuex';

// ストアを作成
const store = createStore({
  state() {
    return {
      count: 0, // 状態の初期値
    };
  },
  getters: {
    doubleCount(state) {
      return state.count * 2; // ゲッター
    },
  },
  mutations: {
    increment(state) {
      state.count++; // ミューテーション
    },
    decrement(state) {
      state.count--;
    },
  },
  actions: {
    async incrementAsync(context) {
      // 非同期処理を含むアクション
      setTimeout(() => {
        context.commit('increment'); // ミューテーションをコミット
      }, 1000);
    },
  },
});

export default store;
  1. state: ストアの状態を表すオブジェクト。この例では、countという状態を持っています。

  2. getters: 状態から派生した値を計算するためのゲッター。doubleCountはcountを2倍に計算する例です。

  3. mutations: 同期的に状態を変更するためのミューテーション。incrementとdecrementはcountを増減させる例です。

  4. actions: 非同期処理を含むアクション。incrementAsyncは1秒後にincrementミューテーションをコミットする非同期処理を含んでいます。

このストアをVue.jsアプリケーションで使用するには、Vueコンポーネント内でstoreをインポートし、setup内で使用できます。例えば、computedプロパティを使用してゲッターを取得したり、アクションを呼び出したりできます。

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double Count: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
    <button @click="incrementAsync">Increment Async</button>
  </div>
</template>

<script>
import { computed } from 'vue';
import { useStore } from 'vuex';

export default {
  setup() {
    const store = useStore();

    // ゲッターを使用
    const count = computed(() => store.getters.doubleCount);

    // ミューテーションをコミット
    const increment = () => {
      store.commit('increment');
    };

    // アクションを呼び出し
    const incrementAsync = () => {
      store.dispatch('incrementAsync');
    };

    return {
      count,
      increment,
      incrementAsync,
    };
  },
};
</script>

最後に

  • States, Modules (Actions + Reducers), Store, Components, Router の順になるよう実装しており、大まかな構造に違いはないと思う。

  • React 本体は Flux アーキテクチャ(あるいは MVC)の View のみを担当するライブラリであり、実用的なアプリケーションを作るには周辺のライブラリを選定して組み合わせて使う必要があります。 特に Redux 用の非同期処理 Middleware(redux-thunk, redux-promise, redux-saga, redux-observable)、Router (connected-react-router) 、form との統合 (formik, redux-form) に何を使うかは話し合う必要性があると感じた

Discussion