😽

React+Reduxの仕組みを説明

2024/06/12に公開

はじめに

実務でReduxを使用するプロジェクトに入るので、Reduxの学習をしました。
理解を深めるためにReact+ReduxでのバックエンドとのAPI通信を用いてReduxの仕組みを説明します。

解説の流れ

  1. Reduxの利点の説明
  2. ログイン機能を用いてreduxの処理の流れの説明
  3. ReduxToolkitを使用した際のコードの変化の説明
  4. ログイン状態を維持させる(おまけ)

1.Reduxの利点の説明

Reduxは状態管理のライブラリです。
アプリケーションの状態を一元管理することで、予測可能な方法で状態の更新を行うことができます。
私が考える主な利点は二つあります。

状態が一元管理されるのでpropsの受け渡しをせずに済みます

コンポーネントの階層構造が深ければ深いほどメリットがあると思います。

reduxの状態管理の流れを理解すれば状態の流れを追いやすい

  • action

状態変化を表すオブジェクトです。アクションは、何かが起こったことを示し、その結果として状態が変化することを伝えます。

  • dispatch

アクションをストア内のリデューサーに渡す機能です。
ディスパッチは、アクションを送信し、リデューサーがそれを処理して新しい状態を生成するプロセスをトリガーします。

  • store

状態を保持するオブジェクトです。
Reducerを使って状態の変化を処理します。
Reducerは、現在の状態とアクションを受け取って新しい状態に上書きする純粋な関数です。

2. ログイン機能を用いてreduxの処理の流れの説明

ここからはコードを用いてReduxの処理の流れを説明します。

以下の流れで状態変化が起こります

ログインボタンを押す

アクションクリエーターをdispatchする

reducerがアクションを受け取り、状態を更新する

dispatch

dispatchはreducerにactionの内容を通知します。
今回は以下のactionの情報をdispatcを用いてstore内のreducerに伝えます。

  • ログインのステータスがtruenになる
  • ログインしたユーザーの情報が入る(backendから取得してきた情報)

ボタンを押すとuseLoginAuthActionが実行されます。

frontend/src/hooks/users/useLoginAuthAction.tsx
import { useDispatch } from 'react-redux'
import { setLoginStatus, setCurrentUser } from 'actions/sessionActions'
import { SignInParams, User } from 'types/users/session'
import { useNavigate } from 'react-router-dom';
import { useState } from 'react';
import { client } from 'lib/api/client';


export const useLoginAuthAction = (signInParams: SignInParams) => {
  const dispatch = useDispatch();
  const navigate = useNavigate();
  const [errorMessages, setErrorMessages] = useState<String[]>([]);

  const afterLoginSuccess = (data: User) => {
    dispatch(setLoginStatus(true));
    dispatch(setCurrentUser(data));
    data.admin === false?
    navigate(`/users/${data.id}`, {state: {message: 'ログインに成功しました', type: 'success-message', condition: true}})
    :
    navigate('/', {state: {message: 'ログインに成功しました', type: 'success-message', condition: true}});
  }

  const loginAction: React.MouseEventHandler<HTMLButtonElement> = (e) => {
    client.post('login', { signInParams })
      .then(response => {
        afterLoginSuccess(response.data);
      })
      .catch(error => {
        if (error.response && error.response.status === 401) {
          setErrorMessages(error.response.data.errorMessages);
        } else {
          setErrorMessages(['予期しないエラーが発生しました']);
        }
        navigate('', { state: { message: 'ログインに失敗しました', type: 'error-message' } });
      }
    );
  }

  return { loginAction, errorMessages}
}

上のコードから大事なのはafterLoginSuccess関数のdispatchの内容です。

 const afterLoginSuccess = (data: User) => {
    dispatch(setLoginStatus(true));
    dispatch(setCurrentUser(data));
    data.admin === false?
    navigate(`/users/${data.id}`, {state: {message: 'ログインに成功しました', type: 'success-message', condition: true}})
    :
    navigate('/', {state: {message: 'ログインに成功しました', type: 'success-message', condition: true}});
  }
  • dispatch(setLoginStatus(true)):
    • setLoginStatusアクションクリエーターが呼び出され、{ type: SET_LOGIN_STATUS, payload: true }というアクションオブジェクトが生成されます。
    • このアクションオブジェクトがdispatch関数に渡され、Reduxのストアに送信されます。
  • dispatch(setCurrentUser(data)):
    • setCurrentUserアクションクリエーターが呼び出され、{ type: SET_CURRENT_USER, payload: data }というアクションオブジェクトが生成されます。
    • dataの内容はbackendから取得してきたuser情報です。
    • このアクションオブジェクトもdispatch関数に渡され、Reduxのストアに送信されます。

action

こちらは上のdispatchのコードで使用しているactionの内容です。

frontend/src/actions/sessionActions.ts
import { UserResponseData } from "types/users/response";

export const SET_LOGIN_STATUS = 'SET_LOGIN_STATUS';
export const SET_CURRENT_USER = 'SET_CURRENT_USER';

export const setLoginStatus = (status: boolean) => ({
  type: SET_LOGIN_STATUS,
  payload: status
});

export const setCurrentUser = (user: UserResponseData | {}) => ({
  type: SET_CURRENT_USER,
  payload: user
});

こちらのタイプからreducerがどの状態を上書きするか判断します。
payloadの部分には上書きしたいデータの内容が入ります。
例えば、setLoginStatusにはstatusが入り、boolean型のデータが入ります。

export const setLoginStatus = (status: boolean) => ({
  type: SET_LOGIN_STATUS,
  payload: status
});

Store

disptachされたアクションは下のコードのstoreに渡されます。
rootReducerは、combineReducersを使って複数のリデューサーをまとめたものです。
この場合、sessionというキーに対してloginReducerが割り当てられています。
rootReducer内のreducerすべてにactionを渡します。

import { combineReducers, createStore } from "redux";
import loginReducer from "./loginReducer";

const rootReducer = combineReducers({
  session: loginReducer
});

const store = createStore(rootReducer)

export type RootState = ReturnType<typeof rootReducer>
export default store;

Reducer

frontend/src/reducers/loginReducer.ts
import { initialLoginState } from 'defaults/userDefaults';
import { SET_LOGIN_STATUS, SET_CURRENT_USER } from '../actions/sessionActions';
import { User } from "types/users/session";

type LoginAction = {
  type: typeof SET_LOGIN_STATUS;
  payload: boolean;
}

type CurrentUserAction = {
  type: typeof SET_CURRENT_USER;
  payload: User;
}

type Action = LoginAction | CurrentUserAction;

const loginReducer = (state = initialLoginState, action: Action) => {
  switch (action.type) {
    case SET_LOGIN_STATUS:
      return {
        ...state,
        loginStatus: action.payload
      };
    case SET_CURRENT_USER:
      return {
        ...state,
        currentUser: action.payload
      };
    default:
      return state;
  }
};

export default loginReducer;
  • loginReducerは、渡されたアクションのtypeに基づいて状態を更新します。
  • SET_LOGIN_STATUSの場合、state.loginStatusがアクションのpayload(この場合はtrue)に更新されます。
  • SET_CURRENT_USERの場合、state.currentUserがアクションのpayload(この場合はdata.user)に更新されます。

渡されたactionタイプの中から該当のものがあれば、現在のstateからpayloadの内容に上書きします。
なければ、以前の状態のstateを渡します。
上記の流れで状態管理の更新が行われます。

3. ReduxToolkitを使用した際のコードの変化の説明

実は上のReduxのコードは現在推奨されている実装方法ではありません。

ReduxToolkitを使用した方法が推奨されているやり方です。
ただ、reduxの処理の流れを理解していた方がReduxToolkitを使用する際に理解が深まるので説明しました。
ReduxToolkitを使用したら上のコードがどれだけ省略できるかも注目していただきたいです。

ReduxToolkitをインストールします。

yarn add @reduxjs/toolkit

ReducerからSliceに変更する

ファイル名をloginReducerからloginSliceに変更後、処理を変更する。

変更前

frontend/src/reducers/loginReducer.ts
import { initialLoginState } from 'defaults/userDefaults';
import { SET_LOGIN_STATUS, SET_CURRENT_USER } from '../actions/sessionActions';
import { User } from "types/users/session";

type LoginAction = {
  type: typeof SET_LOGIN_STATUS;
  payload: boolean;
}

type CurrentUserAction = {
  type: typeof SET_CURRENT_USER;
  payload: User;
}

type Action = LoginAction | CurrentUserAction;

const loginReducer = (state = initialLoginState, action: Action) => {
  switch (action.type) {
    case SET_LOGIN_STATUS:
      return {
        ...state,
        loginStatus: action.payload
      };
    case SET_CURRENT_USER:
      return {
        ...state,
        currentUser: action.payload
      };
    default:
      return state;
  }
};

export default loginReducer;

変更後

frontend/src/reducers/loginSlice
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { initialLoginState} from "defaults/userDefaults";
import { User } from "types/users/session";

const loginSlice = createSlice({
  name: 'login',
  initialState: initialLoginState,
  reducers: {
    setLoginStatus(state, action: PayloadAction<boolean>) {
      state.loginStatus = action.payload;
    },
    setCurrentUser(state, action: PayloadAction<User>) {
      state.currentUser = action.payload;
    }
  }
});

export const { setLoginStatus, setCurrentUser} = loginSlice.actions;
export default loginSlice.reducer;

変更後のコードの解説をします。
createSliceを使用することでactionを定義する必要がなくなります。
setLoginStatusとsetCurrentUserがactionのアクションクリエータの役割を示しています。

dispatch

dispatchに渡すアクションクリエータもloginReducerからimportするものに変更します。
これによりactionファイルは削除できます。

import { useDispatch } from 'react-redux';
import { setLoginStatus, setCurrentUser } from 'reducers/loginSlice';

export const useLoginAuthAction = (signInParams: SignInParams) => {
  const dispatch = useDispatch();
  const navigate = useNavigate();
  const [errorMessages, setErrorMessages] = useState<String[]>([]);

  const afterLoginSuccess = (data: User) => {
    dispatch(setLoginStatus(true));
    dispatch(setCurrentUser(data));
    data.admin === false?
    navigate(`/users/${data.id}`, {state: {message: 'ログインに成功しました', type: 'success-message', condition: true}})
    :
    navigate('/', {state: {message: 'ログインに成功しました', type: 'success-message', condition: true}});
  }

configureStoreを使用してストアを設定

ReduxToolkitのコードに変更する。

変更前

import { combineReducers, createStore } from "redux";
import loginReducer from "./loginReducer";

const rootReducer = combineReducers({
  session: loginReducer
});

const store = createStore(rootReducer)

export type RootState = ReturnType<typeof rootReducer>
export default store;

変更後

import { configureStore } from '@reduxjs/toolkit';
import loginReducer from './loginSlice';

const store = configureStore({
  reducer: {
    session: loginReducer
  }
})

export type RootState = ReturnType<typeof store.getState>;
export default store;

ReduxToolkitのconfigureStoreを使用することで、createStoreとcombineReducersを手動で設定する必要がなくなります。
configureStoreは、デフォルトでいくつかの便利なミドルウェア(redux-thunk)を含んでおり、開発者体験を向上させます。

4. ログイン状態を維持させる(おまけ)

ログイン状態をローカルストレージに保存して、ログイン状態を維持できるようにします。
そうすることでページを更新してもログイン状態を維持できるようにします。
Reduxではredux-persistを使用することでログイン状態を維持できるようにできます。

import { configureStore, combineReducers } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import loginReducer from './loginSlice';

// key: 'root' は、永続化された状態のキーを指定します。
// storage は、使用するストレージエンジンを指定します(ここではlocalStorage)。
// whitelist は、永続化するreducerのリストを指定します(ここでは'session'のみ)。
const persistConfig = {
  key: 'root',
  storage,
  whitelist: ['session']
};

// reducerをまとめます
const rootReducer = combineReducers({
  session: loginReducer
});

// すべてのreducerの中からwhitelistで指定されたものだけ永続化するようにする
const persistedReducer = persistReducer(persistConfig, rootReducer);

const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) => getDefaultMiddleware({
    serializableCheck: {
      // Reduxのデフォルトミドルウェアは、アクションや状態がシリアライズ可能(JSONに変換可能)であることを期待しています。
      // シリアライズできないデータ(例えば、関数や特殊なオブジェクト)が含まれていると、警告やエラーが発生する可能性があります。
      // persist/PERSISTアクションはredux-persistが使用するもので、シリアライズできないデータを含むことがあるため、以下のようにシリアライズチェックから除外しています。
      ignoredActions: ['persist/PERSIST']
    }
  })
});

export type RootState = ReturnType<typeof store.getState>;
export const persistor = persistStore(store);
export default store;

おわりに

Reduxのメカニズムを例えを用いて解説しました。
ReduxToolkitを使用する際も、Reduxのメカニズムが必要なので勉強になりました。
1人でも多くの人のReduxの理解が深まれば幸いです。

参考

https://redux.js.org/

Discussion