🐷

Redux 超入門

2023/04/30に公開

今回はReduxについての記事となります。

一応僕はReactを主に使うエンジニアなのですが、reduxをあまり触ったことがないので勉強がてら記事を書くことにしました。

なので、間違っているところがあるかもしれませんので、見つけたらご指摘ください🙇‍♂️

reduxとは?

reduxとは状態管理のためのライブラリとなります。

このライブラリを使うことで、stateを一元管理できるので保守性が上がります。

reactと相性が良いため、よくreactと一緒に使われます。

また、最近はreduxを書きやすくしたredux toolkitが主に使われます。

reduxの主な登場人物

まずは、次の図を見てください。


https://redux.js.org/tutorials/essentials/part-1-overview-concepts#redux-application-data-flow

reduxの主な登場人物は、次の4つになります。

  • store:stateとreducerを保持する場所
  • action:何が起きたのかの情報を持つオブジェクト
  • dispatch:呼び出されたactionをstoreに通知する関数
  • reducer:storeから受け取ったactionと、store内のstateを元に、変更されたstateを返す純粋関数

さらに、UI,Eventhandler,Provider,Selectorと組み合わせて使います。

実際の実装例を見せると次のようになります。
(まずは素のreduxで実装していきます。)

// reducer
import { combineReducers } from "redux";

const couterReducer = (state = 0, action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + action.payload;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
};

const loginReducer = (state = false, action) => {
  switch (action.type) {
    case "LOGIN":
      return true;
    case "LOGOUT":
      return false;
    default:
      return state;
  }
};

const allReducers = combineReducers({
  counter: counterReducer,
  isLogin: loginReducer,
});

export default allReducers;

今回はログイン情報を扱うreducerと、カウンターの数値を扱うreducerを作成します。

reducer内では、actionのtypeに合わせて関数を実行します。

また、actionには、状態を更新するために必要な追加の情報(ペイロード)を含めることができます。

今回の例で言うと、ペイロードとして渡ってきた値をincrement時に使用しています。

複数のreducerを使う場合は、combineReducersというものを使用して1つのオブジェクトに統合します。

これで、counterというstateに対してはcounterReducerが、isLoginというstateにはloginReducerが適用されます。

この時に、stateも定義されるわけです。

export const increment = (number) => {
  return {
    type: "INCREMENT",
    payload: number,
  };
};

export const decrement = () => {
  return {
    type: "DECREMENT",
  };
};

export const login = () => {
  return {
    type: "LOGIN",
  };
};

export const logout = () => {
  return {
    type: "LOGOUT",
  };
};

次に、actionCreaterというものを定義します。

このあと、これを元にActionを作成することになります。

// store
import { createStore } from "redux";
import { Provider } from "react-redux";const store = createStore(
  allReducers,
);

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

次にcreateStoreという関数で、reducerを引数に渡してstoreを作成します。

そして、Providerでコンポーネント全体を囲むことで、その中で指定したstoreが使えるようになります。

import { useSelector, useDispatch } from "react-redux";
import { increment, decrement, login as loginAction, logout } from "./actions";

function App() {
  const counter = useSelector((state) => state.counter);
  const login = useSelector((state) => state.isLogin);
  const dispatch = useDispatch();
  return (
    <div className="App">
      <h1>Redux App</h1>
      <h2>Counter: {counter}</h2>
      <button onClick={() => dispatch(increment(5))}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
      <br />
      <h2>{login ? "ログイン中" : "ログインしてください"}</h2>
      <button onClick={() => dispatch(loginAction())}>ログイン</button>
      <button onClick={() => dispatch(logout())}>ログアウト</button>
    </div>
  );
}

export default App;

実際にstateを参照するにはuseSelectorという関数を使用します。

そして、reducerを使いたい時はuseDispatchという関数を使用します。

この時、actionCreaterを使用することで、storeに通知するactionを作成することができます。

actionCreaterというよく分からない単語を使っていますが、要はactionを作成するための関数です。

これで、超簡単なログイン機能とカウントアップ機能の実装が終わりました。

ここまでで、今回やった実装をまとめると次のようになります。

  • 使用するstateとそれに対応するreducerを定義する
  • reducerに対応するactionCreatorを定義する
  • storeを作成し、providerでコンポーネントを囲むことで、storeの参照が可能になる
  • useSelectorでstateを取得
  • useDispatchを使用し、dispatchを作成
  • 作成したdispatchにactionCreatorを渡して、storeに通知する
  • storeに渡ってきたactionとstoreに元々あったstateを使って、stateの値を書き換える
  • useSelectorで変更されたstateを取得

簡単にまとめるとこのようになります。

流れ的には、UI⇨Action⇨Store⇨UIとなります。

ちなみに、このようなデータの流れをFlux Flowと言います。

redux toolkit

次に、先ほどの例をredux toolkitで書き換えようと思います。

redux toolkitではSliceという新しい概念が出てきます。

これは、1つのstoreで全てのstateを管理するのではなく、Store、Reducer、ActionCreatorをまとめたSliceという単位で管理しようという考え方になります。

実際に先ほどの例を書き換えた結果は次のようになります。

import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: 0,
  reducers: {
    increment: (state, action) => {
      return state + action.payload;
    },
    decrement: (state) => {
      return state - 1;
    },
  },
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

import { createSlice } from "@reduxjs/toolkit";

const isLoginSlice = createSlice({
  name: "isLogin",
  initialState: false,
  reducers: {
    login: () => {
      return true;
    },
    logout: () => {
      return false;
    },
  },
});

export const { login, logout } = isLoginSlice.actions;
export default isLoginSlice.reducer;

まずcreateSliceを使い、それぞれのsliceを作成します。

import { Provider } from "react-redux";
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./reducers/counter";
import isLoginReducer from "./reducers/isLogin";export const store = configureStore({
  reducer: {
    counter: counterReducer,
    isLogin: isLoginReducer,
  },
});

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>

次に、configreStoreという関数を使い、storeを作成します。

あとは、先ほどと同じようにProviderにstoreをセットすることで、参照可能になります。

import "./App.css";
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement } from "./reducers/counter";
import { login, logout } from "./reducers/isLogin";

function App() {
  const counter = useSelector((state) => state.counter);
  const isLogin = useSelector((state) => state.isLogin);
  const dispatch = useDispatch();
  return (
    <div className="App">
      <h1>Redux App</h1>
      <h2>Counter: {counter}</h2>
      <button onClick={() => dispatch(increment(5))}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
      <br />
      <h2>{isLogin ? "ログイン中" : "ログインしてください"}</h2>
      <button onClick={() => dispatch(login())}>ログイン</button>
      <button onClick={() => dispatch(logout())}>ログアウト</button>
    </div>
  );
}

export default App;

残りもほとんど同じで、dispatchの中見だけcreateSliceで作成したactionCreatorを設定すればOKです。

このように、redux-toolkitを使うことで、簡単・簡潔に書くことができます。

おまけ

ちなみに、redcerの中身は純粋関数である必要があるため、副作用のある処理を書くことはできません。

なので、非同期処理などの副作用のある処理をしたい場合は、redux-thunkというライブラリを使用する必要があります。

ただ、redux-toolkitにはredux-thunkが内包されているので、新たにライブラリをインストールする必要はありません。

まとめ

今回はreduxについてまとめてきました。

reduxを使っている現場は多いかと思うので、ぜひこの機会に身につけましょう。

宣伝

0からエンジニアになるためのノウハウをブログで発信しています。
https://hinoshin-blog.com/

また、YouTubeでの動画解説も始めました。
https://www.youtube.com/channel/UCqaBUPxazAcXaGSNbky1y4g

インスタの発信も細々とやっています。
https://www.instagram.com/hinoshin_enginner/

興味がある方は、ぜひリンクをクリックして確認してみてください!

Discussion