Published on

絶対に理解するReact Redux

Authors
  • avatar
    Name
    Kikusan
    Twitter

React Reduxとは

Reactコンポーネントに対して一か所で状態を保持しておくライブラリ

Redux Data Flow

Redux data flow diagram
  1. UIで操作
  2. Eventハンドリング
  3. ActionをStoreにDispatch
  4. 以前のStateにReducerでActionを反映
  5. StateをUIに反映
  • Store: 状態を管理する場所
  • State: アプリケーション全体の状態
  • Action: 状態をどう変化させるかを保持するオブジェクト
  • Dispatch: ActionをStoreに通知するメソッド
  • Reducer: ActionによってStateを変更するロジック

tools

開発者ツールにタブが追加される。例えばstoreを見るにはstoreにwindow.REDUX_DEVTOOLS_EXTENSION && window.REDUX_DEVTOOLS_EXTENSION()を追加する。

Redux

サンプルソース

Provider

Providerで囲った配下ではstateを使用でき、Dispatchもできる。

import { Provider } from 'react-redux';

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

Store

import { createStore } from "redux";
import allReducers from './reducers';

// store
let store = createStore(
  allReducers,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
  ); // reducerを引数にとる。

Reducer

import { combineReducers } from "redux";

// reducer
const counterReducer = (state = 0, action) => { // stateの初期値 action戻り値を引数にとる
    switch (action.type) {
      case "INCREMENT":
        return state + 1;
      case "DECREMENT":
        return state - 1;
      default:
        return state;
    }
  }

// 中略

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

export default allReducers;

Action

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

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

useSelector/useDispatch

useSelectorでstateの値を取得、useDispatchでActionを通知。

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

function App() {
  // stateの取得
  const counter = useSelector((state) => state.counter);
  const isLogin = useSelector((state) => state.isLogin);

  const dispatch = useDispatch();
  
  return (
    <div className="App">
      <h1>Hello Redux</h1>
      <h3>count: {counter}</h3>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  );
}

export default App;

Redux Toolkit

Reduxをより簡潔に記述するためのツール

サンプルソース

Provider

Providerで囲った配下ではstateを使用でき、Dispatchもできる。

    <Provider store={store}>
      <App />
    </Provider>

Store

import { configureStore } from "@reduxjs/toolkit"
import cartReducer from "./features/cart/CartSlice" // default exportに命名
import modalReducer from "./features/modal/ModalSlice"

export const store = configureStore({
  reducer: {
    cart: cartReducer,
    modal: modalReducer,
  },
})

export type RootState = ReturnType<typeof store.getState>

Slice

SliceはAction(ActionCreator)とReducerとStateを同時に定義するもの。
関連するオブジェクトを同時に管理できる。

import {createSlice} from "@reduxjs/toolkit"
import cartItems from "../../cartItems"
import { CartItemType } from "../../components/CartItem"

type cartState = {
  cartItems: CartItemType[],
  amount: number,
  total: number,  
}

// 買い物かごの初期化
const initialState: cartState = {
  cartItems: cartItems,
  amount: cartItems.reduce((summ: number, current: CartItemType) => summ + current.amount, 0),
  total: cartItems.reduce((summ: number, current: CartItemType) => summ + current.price * current.amount, 0),
}

const cartSlice = createSlice({
  name: "cart",
  initialState,
  reducers: {
    clearCart: (_) => { // actionsに配置され、action名になる。 ->  type: "cart/removeItem";
      return { cartItems: [], amount: 0, total: 0 } // 戻り値がstateになる。
    },
    removeItem: (state, action) => { // actionにはpayloadとtypeが乗っている typeは自動で決まり、payloadはactionの引数が入る
      state.cartItems = state.cartItems.filter(item => item.id !== action.payload)
    },
    increase: (state, action) => {
      const cartItem = state.cartItems.find((item) => item.id === action.payload)
      if (cartItem) {
        cartItem.amount = cartItem.amount + 1;
      }
    },
    decrease: (state, action) => {
      const cartItem = state.cartItems.find((item) => item.id === action.payload)
      if (cartItem) {
        cartItem.amount = cartItem.amount - 1;
      }
    },
    calculateTotals: (state) => {
      let amount = 0;
      let total = 0;
      state.cartItems.forEach((item) => {
        amount += item.amount
        total += item.amount * item.price;
      })
      state.amount = amount;
      state.total = total;
    },
  },
})

export const { clearCart, removeItem, increase, decrease, calculateTotals } = cartSlice.actions
export default cartSlice.reducer

useDispatch,useSelector

useSelectorでstateの値を取得、useDispatchでActionを通知。

import { useDispatch, useSelector } from "react-redux"
import { RootState } from "../store"
import CartItem from "./CartItem"
import { openModal } from "../features/modal/ModalSlice"

const CartContainer = () => {
  const { cartItems, amount, total } = useSelector((state: RootState) => state.cart)
  const dispatch = useDispatch();

  return (
    <section>
      <header>
        <h2>買い物かご</h2>
      </header>
      { amount < 1
        ? <h4 className="empty-cart">何も入っていません・・・</h4>
        : <div>
            {cartItems.map((item) => <CartItem key={item.id} {...item}/>)}
          </div>
      } 
      <footer>
        <hr />
        <div className="cart-total">
          <h4>合計{ total }</h4>
        </div>
        <button className="btn clear-btn" onClick={() => dispatch(openModal())}>全削除</button>
      </footer>
    </section>
  )
}

export default CartContainer