Development

React Reduxの仕組みを解説


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

ブログ作成者紹介

氏名:D・H
所属:開発部
入社年:2024年4月

解説の流れ

  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の情報をdispatchを用いてstore内のreducerに伝えます。
・ログインのステータスがtrueになる
・ログインしたユーザーの情報が入る(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の内容です。

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';

const persistConfig = {
  key: 'root',
  storage,
  whitelist: ['session']
};

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

const persistedReducer = persistReducer(persistConfig, rootReducer);

const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) => getDefaultMiddleware({
    serializableCheck: {
      ignoredActions: ['persist/PERSIST']
    }
  })
});

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

おわりに

本ブログでは、Reduxのメカニズムを例を用いて解説しました。
ReduxToolkitを使用する際も、Reduxのメカニズムが必要なので勉強になりました。
本ブログがReduxへの理解に繋がれば幸いです。

参考

https://redux.js.org/

わたしたちと一緒に働いていただける方を大募集しています!
興味のある方はぜひ下記リンクをチェックしてみてください!

X(旧Twitter)では社内の雰囲気を発信しています、ぜひフォローしてみてください!

関連記事

%d人のブロガーが「いいね」をつけました。