🏗️

Redux / Redux Toolkitで状態管理

2021/06/18に公開

Reduxとは

fluxを拡張した状態管理フレームワーク。
Reactとよくセットで扱われる。


引用元:ABC of Redux - DEV Community(https://dev.to/radiumsharma06/abc-of-redux-5461)
よく見るフロー図。

UI

ユーザーの入力(View)から値を入力し、Actionsに伝える。

Actions

typeとその内容を持ったオブジェクト。

{
  type: 'ADD_USER',
  contents: {
    name: 'taro',
    email: 'taro.example.com'
  }
}

const addUser = () => {
  return {
    // ADD_USER
  };
}

Dispatcher

生成されたActionsをStoreに送る

dispatch(addUser(contents));

Store

Store, Stateはアプリケーションに1つのみ存在する。

  • 状態(State)を保持する
  • StateにアクセスするためのgetStateを提供する
  • Stateを更新するためのdispatch(action)を提供する
  • リスナーを登録するためのsubscribe(linstner)を提供する

State

アプリケーションの状態。
今回の場合、ユーザー情報(名前、メールアドレス)を保持する。

{
  users: [
    {
      contents: {
        name: 'foo',
        email: 'foo@example.com'
      }
    },
    {
      contents: {
        name: 'bar',
        email: 'bar@example.com'
      }
    }
  ]
}

Reducer

ActionsとStateから、新しいStateを生成して返すメソッド。
Reducerは元のStateを更新しない純粋関数でなければいけない。

function userApp(state = initialState, action) {
  switch (action.type) {
    case ADD_USER:
      return Object.assign({}, state, {
        todos: [
          {
            contents: action.contents,
          }
        ]
      })
    ]
    default:
      return state
  }
}

Reducerが肥大化しないように、子のReducerを作成できる。

function users(state = [], action) {
  switch (action.type) {
    case ADD_USER:
      return [
        ...state,
        {
          text: action.contents,
        }
      ]
    default:
      return state
  }
}

function userApp(state = {}, action) {
  return {
    users: users(state.user, action)
  }
}

Redux Toolkitとは

Reduxは小さい構成でもコード量が多く敬遠されがちだが、
Redux Toolkitを使うことでより少ないコード量で実装することが可能になる。

今回は作業の都合上Typescriptで実装。

インストールするパッケージは以下。

redux
react-redux
redux-logger
@reduxjs/toolkit
@types/react-redux
@types/redux-logger

Slice / createSlice

reducerとactionを同時に定義ができる。

pages/store/user/slice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

export type UserState = {
  name: string;
  email: string;
  isSignIn: boolean;
};

export const initialState: UserState = {
  name: '',
  email: '',
  isSignIn: false,
};

const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    updateUser: (state, action: PayloadAction<UserState>) => ({
      ...state,
      name: action.payload.name,
      email: action.payload.email,
      isSignIn: action.payload.isSignIn,
    }),
  },
});

export default userSlice;

Store

Store部分の実装。

pages/store/store.ts
import { Store, combineReducers } from 'redux';
import { logger } from 'redux-logger';
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';

import userSlice, { initialState as userState } from './user/slice';

const rootReducer = combineReducers({
  user: userSlice.reducer,
});

const preloadedState = () => {
  return {
    user: userState,
  };
};

export type StoreState = ReturnType<typeof preloadedState>;

export type ReduxStore = Store<StoreState>;

const store = () => {
  const middlewareList = [...getDefaultMiddleware(), logger];

  return configureStore({
    reducer: rootReducer,
    middleware: middlewareList,
    devTools: process.env.NODE_ENV !== 'production',
    preloadedState: preloadedState(),
  });
};

export default store;

Custom App

Storeを使うためにCustom Appを作成。
Next.jsの場合、pagesのルートに作るだけでOK。

pages/_app.ts
import React from 'react';
import { AppProps } from 'next/app';
import { Provider } from 'react-redux';
import store from '../store/store';

const MyApp = ({ Component, pageProps }: AppProps) => {
  return (
    <Provider store={store()}>
      <Component {...pageProps} />
    </Provider>
  );
};

export default MyApp;

PageComponent

ページコンポーネントに追加。

pages/redux_user.ts
import React from 'react';
import { useDispatch } from 'react-redux';
import { userUserState } from '../store/user/selectors';
import userSlice from '../store/user/slice';

import Layout from '../components/Layout'
import Link from 'next/link'

const reduxUser: React.FC = () => {
  const dispatch = useDispatch();
  const state = userUserState().user;

  const onClickUpdateUser = () => {
    dispatch(userSlice.actions.updateUser({
      name: 'foo',
      email: 'foo@example.com',
      isSignIn: true,
    }));
  };
  
  return (
    <Layout
      title="Firebase Auth">
      <h1>Redux User</h1>
      <div>
        <button type="button" onClick={onClickUpdateUser}>UPDATE USER</button>
        <ul>
          <li>name: {state.name}</li>
          <li>email: {state.email}</li>
          <li>isSignIn: {state.isSignIn ? 'COMPLETED' : ''}</li>
        </ul>
      </div>
      <ul>
        <li>
          <Link href="/">
            <a>Go home</a>
          </Link>
        </li>
      </ul>
    </Layout>
  );

}

export default reduxUser;

[UPDATE USER]ボタンを押すと..

updateUserイベントが叩かれてviewが更新されるはず。

Discussion