okinawa

IT勉強メモ

【React】createAsyncThunk・createSliceメモ

参考

redux.js.org

www.udemy.com

公式のありがたい画像

公式のはGIF画像なので下記画像よりわかりやすい。要参照。
Redux Fundamentals, Part 6: Async Logic and Data Fetching | Redux

画像に出てくる各用語の解説

詳しくは下記に書いた。

dodosu.hatenablog.jp

Store

状態(State)を保管しておく場所。

Reducer

Reducerとはstateをどのように更新するか定義した関数。

State

状態のこと。

Reduxは、このStateを管理するためのライブラリ。

Stateが更新されると画面が再描画される。

UI

関数コンポーネントのこと。

Action

名前はActionだけど何のActionもしない。

Reducerの識別子である「tpye」と「payload(Stateの素)」を持つオブジェクト。

payload

本来のデータ部分のことらしい。

HTTPで言うと、ヘッダデータなどの付加情報をを除いたボディ部分。

APIとの通信の場合、payloadにレスポンスデータが入ってくる。

Dispatch

ReducerにActionを渡して、Reducerを呼び出す関数。ここがスタート。

dispatch(action);でReducer呼び出し。

EventHandler

そのまま。

何らかのEventと紐付ける。ボタンクリックなど。

StoreとEventHandlerの繋ぎ

Dispatchの引数にActionオブジェクトを渡して繋ぐ。

ActionオブジェクトにはReducerの識別子であるtypeプロパティがある。

typeプロパティの値でReducerと紐付ける。

createAsyncThunkとは

Redux Tool kitに含まれる非同期処理を扱うためのツール。

Stateの更新を非同期で行う場合に使う。

APIとの通信によく使われます。

ざっくり言うとこの関数の引数に、Promiseを返す非同期処理を渡すとStateを更新できる。

createSliceとセットで使う。

createAsyncThunkの構文

createAsyncThunk(
  'type',
   非同期関数
)

const addAsyncWithStatus = createAsyncThunk(
  'counter/asyncCount', // 第一引数 = type = 一意な名前なら何でもよい 
  // 第2引数 = 何らかの非同期処理↓
  async (payload) => {
    const response = await asyncCount(payload);
    return response.data;
  }
)

createAsyncThunkの引数

・第一引数:type(文字列)
Reducerを識別するための文字列。一意な名前なら何でもよいらしい。

・第二引数:payloadCreator
Promiseを返す非同期関数のこと。

createAsyncThunkの戻り値

ActionCreatorを返す。

返すActionCreatorは処理状態によって異なります。

  • pendingの時: ‘関数名.pending’
  • fulfilledの時: ‘関数名.fulfilled’
  • rejectedの時: ‘関数名.rejected’

構文のコード例で行くと下記のActioCreatorが返ってくる。

addAsyncWithStatus.pending
addAsyncWithStatus.fullfilled
addAsyncWithStatus.rejected

createSliceとは

createSlice は、Redux Toolkit の一部であり、Reduxの createReducer や createAction を組み合わせたものです。

これにより、簡単にReduxのstoreの状態を更新するためのリデューサーとアクションを作成できます。

簡単に言うと、Reducer・ActioCreator・Stateを持つオブジェクトを返す。

構文

const counter = createSlice({
  name: 'Slice名', //これがtypeの一部になる
  initialState: {state: '初期値'},
  //reducersには同期処理
  reducers: {
   /*Stateを更新する同期処理をここに書く*/ 
  },

  //extraReducersには非同期処理
  extraReducers: (builder) => {
    builder.addCase(addAsyncWithStatus.pending, (state) => {
      //State更新する処理(処理中)
    })
      .addCase(addAsyncWithStatus.fulfilled, (state, action) => {
        //State更新する処理(成功時)
        state.count = state.count + action.payload;
      })
      .addCase(addAsyncWithStatus.rejected, (state) => {
        //State更新する処理(失敗時)
      })
  }
});

引数

  • name=Sliceの名前
  • initilState=stateの初期値
  • reducers=同期処理のReducer
  • extraReducers=非同期処理のReducer

戻り値

公式より引用
createSlice | Redux Toolkit

{
    name: string,
    reducer: ReducerFunction,
    actions: Record<string, ActionCreator>,
    caseReducers: Record<string, CaseReducer>.
    getInitialState: () => State,
    reducerPath: string,
    selectSlice: Selector;
    selectors: Record<string, Selector>,
    getSelectors: (selectState: (rootState: RootState) => State) => Record<string, Selector>
    injectInto: (injectable: Injectable, config?: InjectConfig & { reducerPath?: string }) => InjectedSlice
}

とりあえずactionCreatorとReducerを返すとだけ覚えておけばOK。

actionCreatorとReducerの取得方法↓

const sliceSample = createSlice( /*省略*/);

const [ reducersオブジェクト内の関数名 ] = sliceSample.actions; // reducersのActionCreator取得
const rducerFunc =  sliceSample.reducer; // Reducer取得。これをStoreに渡す

サンプルコード

上がcounter。下がtestSlice。

非同期ボタンを押すと、0~1秒後に計算結果が返ってくるボタン。(extraReducers)

他のボタンは同期処理。(reducers)

・Example.js

import Counter from "./components/Counter";
import { Provider } from "react-redux";
// import store from "./store"
import { createSlice, createAsyncThunk, configureStore } from "@reduxjs/toolkit";

//非同期処理用の関数。0~1秒後に引数の値を返すだけ。
//createAsyncThunkにthunkされる関数
//これの戻り値がextraReducersのaction.payloadに渡される
const asyncCount = (count = 1) => {
  return new Promise((resolve) =>
    setTimeout(() => resolve({ data: count }), Math.random() * 1000)
  );
};

//ActionCreator
//createAsyncThunkはActioCreatorを返す関数
const addAsyncWithStatus = createAsyncThunk(
  'counter/asyncCount', // 第一引数=type
  async (payload) => { // 第2引数=payloadCreator=何らかの値を返す非同期処理の関数
    const response = await asyncCount(payload);
    return response.data;
  }
)

//ActionCreator
//createAsyncThunk2つめ。中身は同じ。
const addAsyncWithStatus2 = createAsyncThunk(
  'test/asyncCount',
  async (payload) => {
    const response = await asyncCount(payload);
    return response.data;
  }
)

/**Reducer&Action&Stateの生成
 * createSliceはReducerとActionCreatorとStateを含むオブジェクトを返す
 */
const counter = createSlice({
  name: 'counter',///Slice名。これがtypeの一部になる
  initialState: {//Stateの初期値
    count: 0,
    status: ''
  },
  //reducersには同期処理
  reducers: {
    add(state, { type, payload }) {
      state.count = state.count + payload;
    },
    minus(state, { type, payload }) {
      state.count = state.count - payload;
    }
  },
  //extraReducersには非同期処理
  extraReducers: (builder) => {
    builder.addCase(addAsyncWithStatus.pending, (state) => {
      state.status = 'Loading...'
    })
      .addCase(addAsyncWithStatus.fulfilled, (state, action) => {
        state.status = '取得済'
        state.count = state.count + action.payload;
      })
      .addCase(addAsyncWithStatus.rejected, (state) => {
        state.status = 'エラー'
      })
  }
});

//2つ目のcreateSlice
//Reducer&Action&Stateの生成
const testSlice = createSlice({
  name: 'test',//Slice名。これがtypeの一部
  initialState: {
    result: 1,
    status2: ''
  },
  reducers: {
    multiplication(state, { type, payload }) {
      state.result = state.result * payload;
    },
    division(state, { type, payload }) {
      state.result = state.result / payload;
    }
  },
  extraReducers: (builder) => {
    builder.addCase(addAsyncWithStatus2.pending, (state) => {
      state.status2 = "処理中";
    })
      .addCase(addAsyncWithStatus2.fulfilled, (state, action) => {
        state.status2 = "成功";
        state.result = state.result * action.payload;
      })
      .addCase(addAsyncWithStatus2.rejected, (state) => {
        state.status2 = "失敗";
      })
  }
});

//Store生成
const store =  configureStore({
  reducer: {
    counter: counter.reducer,//counter.reducerにReducerが入っている
    test: testSlice.reducer//testSlice.reducerにReducerが入っている
  }
});


// actionsにActionCreatorが入っている
// typeが counter/addとcounter/minus
const { add, minus } = counter.actions;

// typeが test/multiplicationとtest/division
const { multiplication, division } = testSlice.actions;


const Example = () => {
  return (
    <Provider store={store}>
// CounterコンポーネントにActioCreatorをすべて渡す
      <Counter actionCreator1={add} actionCreator2={minus} actionCreator3={addAsyncWithStatus}
        actionCreator4={multiplication} actionCreator5={division} actionCreator6={addAsyncWithStatus2} />
    </Provider>
  );
};

export default Example;

・Counter.js

import { useSelector, useDispatch } from "react-redux"

//ExampleコンポーネントからActioCreatorをすべて受け取り
const Counter = ({ actionCreator1, actionCreator2, actionCreator3, actionCreator4, actionCreator5, actionCreator6 }) => {
    const dispatch = useDispatch();

    const clickHandler = (e) => {
        if (e.target.name === "+2") {
            dispatch(actionCreator1(2)); // dispatch(actionCreator)でReducerを呼び出す
        } else if (e.target.name === "-2") {
            dispatch(actionCreator2(2));
        } else {
            dispatch(actionCreator3(2));
        }
    }

        const clickHandler2 = (e) => {
            if (e.target.name === "*2") {
                dispatch(actionCreator4(2));
            } else if (e.target.name === "/2") {
                dispatch(actionCreator5(2));
            } else {
                dispatch(actionCreator6(3));
            }
        }
    
    // useSelector()でStateを取得
    const status = useSelector(state => state.counter.status);
    const count = useSelector(state => state.counter.count);
    const result = useSelector(state => state.test.result);
    const status2 = useSelector(state => state.test.status2);
    return (
        <>
            <p>{count}</p>
            <h3>{status}</h3>
            <button onClick={clickHandler} name="+2">+2</button>
            <button onClick={clickHandler} name="-2">-2</button>
            <button onClick={clickHandler} name="非同期+2">非同期+2</button>
            <p>{result}</p>
            <h3>{status2}</h3>
            <button onClick={clickHandler2} name="*2">×2</button>
            <button onClick={clickHandler2} name="/2">÷2</button>
            <button onClick={clickHandler2} name="非同期*3">×3</button>

        </>
    )
}
export default Counter;

ちょっと分かりづらい所メモ(ActionCreatorとtype)

ActionCreator

Actionとはtypeとpayloadを持つだけの単なるオブジェクト。

このActionオブジェクトを返すのがActionCreatorという関数。

const actionCreator = () => {
  return {
      type: "couter/add", // typeはReducerを特定するための識別子
      paylod: 2  // payloadをReducerに渡して、これを元にStateを更新する
    }
}

■ActionCreatorの取得

同期処理のActionCreatorはcreateSliceの戻り値から取得。

非同期処理のActionCreatorはcreateAsyncThunkの戻り値から取得。

// createSliceからActionCreator関数を取得
const counter = createSlice(/*省略*/);
const { add, minus } = counter.actions;

// createAsyncThunkからActionCreator関数を取得
// 非同期処理関数の戻り値(pending/fullfilled/rejected)に応じた下記のActionCreatorを自動生成
// addAsyncWithStatus.pending
// addAsyncWithStatus.fullfilled
// addAsyncWithStatus.rejected
const addAsyncWithStatus = createAsyncThunk('type', 非同期処理関数);

reducersとextraReducersのtypeの違い

■reducersのtype(識別子)は「Slice名/Reducer名」

  • counter/add
  • counter/minus
  • test/multiplication
  • test/division

createSliceの戻り値であるActionCreatorにtypeが含まれている。

const { add, minus } = counter.actions; // typeはcounter/addとcounter/minus

■extraReducerのtype(識別子)は「createAsyncThunkの第一引数(type)/状態」

  • counter/addAsyncWithStatus/pending
  • counter/addAsyncWithStatus/fulfilled
  • counter/addAsyncWithStatus/rejected
  • test/addAsyncWithStatus2/pending
  • test/addAsyncWithStatus2/fulfilled
  • test/addAsyncWithStatus2/rejected

createAsycThunk関数の戻り値がActionCreator。

状態によってpending/fullfilled/rejectedが自動生成される。

const addAsyncWithStatus = createAsyncThunk('type', 非同期処理関数);