【React】 Reduxで非同期に値を更新

Reduxの特徴の一つは、素のReactでは困難な非同期処理でのステート更新が可能なことです。

特に、@reduxjs/toolkitを用いると楽に実装することができます。

非同期な更新

とりあえず事前準備として、普通の更新を用意しておきます。

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

const initialState = {
    value: 0,
};

export const counterSlice = createSlice({
    name: 'counter',
    initialState,
    reducers: {
        increment: (state) => {
            state.value += 1;
        },
        decrement: (state) => {
            state.value -= 1;
        },
        incrementByAmount: (state, action) => {
            state.value += action.payload;
        },
    },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export const selectCount = (state) => state.counter.value;

export default counterSlice.reducer;

createAsyncThunkで作成

非同期に値を更新するためには、まずはcreateAsyncThunkを用いて、更新用の処理を作成します。

counterApi.js
export const fetchCount = (amount) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve({ data: amount });
        }, 1000);
    });
}
counterSlice.js
export const incrementAsync = createAsyncThunk(
    // slice名/適当な名前 でok
    'counter/fetchCount',

    // 処理の本体
    async (amount) => {
        const response = await fetchCount(amount);
        return response.data;
    }
);

作成と同時にexportして、dispatchで呼び出す準備をしておきましょう。

このときにreturnで返した値が、actionの値として入ってきます。

reducerに登録

createAsyncThunkで作成したものは、createSlice内で、extraReducersで処理を登録します。

const initialState = {
    value: 0,
    status: "idle"
};

export const counterSlice = createSlice({
    name: 'counter',
    initialState,
    reducers: {
        // ...
    },
    extraReducers: (builder) => {
        builder
            .addCase(incrementAsync.pending, (state) => {
                state.status = "loading";
            })
            .addCase(incrementAsync.fulfilled, (state, action) => {
                state.status = "idle";
                state.value += action.payload;
            });
    },
});

// ついでに追加したstateをexport
export const selectUpdateStatus = (state) => state.counter.status;

builder.addCaseが登場しました。これは、指定したAsyncThunkの状態が変化した際に、どのような処理を行うかを登録するためのものです。感覚はaddEventListenerと似ています。

状態には3つあります。

状態
.pending実行は開始したが、まだ非同期処理が終わっていない状態
.fulfilled実行が正常に完了した (resolveされた)
.rejected実行が失敗した (rejectされた)

もちろん、addCaseを増やすだけで、別のAsyncThunkを追加できます。

dispatchする

処理ができたら、dispatchを使用して非同期処理を開始します。

import { useSelector, useDispatch } from 'react-redux';
import { increment, incrementAsync, selectCount } from './features/counter/counterSlice';

export default function App() {
    const count = useSelector(selectCount);
    const status = useSelector(selectUpdateStatus);

    const dispatch = useDispatch();

    const handleIncrementByAmountAsync = () => {
        // 他と同じように呼ぶだけ
        dispatch(incrementAsync(5));
    }

    return (
        <>
        <p>{ count }</p>
        <p>{ status }</p>
        <button onClick={handleIncrementByAmountAsync}>+5 (async)</button>
        </>
    );
}

そうすると、一定時間後に数値が加算されることがわかります。また、非同期処理中に値が更新されても問題なく値が変化することがわかります。

まとめ

@reduxjs/toolkitを用いると、簡単にReduxで値の非同期な更新が可能になります。また、同期的な更新と混ぜても問題ありません。

非同期更新は非常に頻繁に行うので、書き方をしっかり把握しておきましょう。

react redux async thumb

役に立ったらシェアしよう!