Redux]非同期状態処理のためのredux-toolkit分析と検討

39654 ワード

✔¥¥ААААААААААА


要するに、Reduxはstoreの役割だけでなく、非同期処理の役割も果たし、肥大化していると指摘する人が増えている.
私は開発時に非同期関連のタスクを挿入すると,「pending」,「except」,「success」などの呼び出しの結果に基づいて応答処理を行うことが非常に困難である.
特に,Reduxはこの状態に対して動作でいちいち定義する必要があるという不便があるため,正直,そのままのReduxで非同期操作を行うのが好きではない.
// const/action.js

const ACTION1_PENDING = "ACTION1_PENDING"
const ACTION1_FAIL = "ACTION1_FAIL"
const ACTION1_SUCCESS = "ACTION1_SUCCESS"

const ACTION2_PENDING = "ACTION2_PENDING"
const ACTION2_FAIL = "ACTION2_FAIL"
const ACTION2_SUCCESS = "ACTION2_SUCCESS"

const ACTION3_PENDING = "ACTION3_PENDING"
const ACTION3_FAIL = "ACTION3_FAIL"
const ACTION3_SUCCESS = "ACTION3_SUCCESS"
.
.
.
.
// action hell
それでも、初期のReduxで「mapStateToProps」を使用した場合に比べて、現在ではだいぶ良くなってきましたが、このように動作を定義するたびに不便なため、非同期操作に関連する内容が肥大化しすぎます.
公式文書でもredux-toolkitがおすすめです
redux-toolkitは動作する必要はありません.
また、非同期処理はpropsに自動的に伝達される方式も提供されるため、既存のredox thunkを用いて非同期処理を行うかsagaを用いて処理を行うよりもはるかに良好である.
  • 基本用法
  • import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
    
    const fetchUserById = createAsyncThunk(
      'users/fetchByIdStatus',
      async (userId: number, thunkAPI) => {
        const response = await userAPI.fetchById(userId)
        return response.data
      }
    )
    .
    .
    .
    const usersSlice = createSlice({
      name: 'users',
      initialState,
      reducers: {
        // standard reducer logic, with auto-generated action types per reducer
      },
      extraReducers: (builder) => {
        builder.addCase(fetchUserById.fulfilled, (state, action) => {
          // Add user to the state array
          state.entities.push(action.payload)
        })
      },
    })
    
    // inside component
    dispatch(fetchUserById(123))
    以上のように、createAsyncThunkをインポートし、パラメータを入力しながら呼び出すことで、必要なaction creatorを作成できます.(戻り関数)
    特別なことではなく、以前redux-thunkで非同期操作をしていた内容を覚えておけばいいのです.
    次に、createSlice内のオブジェクトのextrareducers部分に自動的にパラメータとして送信されるbuilderオブジェクトを使用して、async関数の実行によって付加される動作dispatchの各インスタンスの動作内容を記述するaddCaseメソッドを呼び出すことができる.
    公式ドキュメントのcreateAsyncThunkパラメータを表示します.

    a.第一パラメータ:動作タイプ



    上述したように、dispatchが非同期関数アクション作成者をdispatchに渡すと、既存のreudx-thunkのように関数が再呼び出されます.
    興味深いことに、「pending action」=>「fulfiled action or refered action」の順に自動的にdispatchされます.
    通常、redux-thunkは、dispatchオブジェクトとして関数を受信すると、アクションオブジェクトが現れるまで呼び出し続けます.
    つまり.

  • コンポーネントでは、dispatchによってオブジェクトを作成してcreateAsyncThunkを呼び出し、内部のpendingメソッドを呼び出して動作をdispatchします.

  • 次に、2番目のパラメータに渡された非同期関数を呼び出し、Promiseの解決を待ってから、PromiseオブジェクトのResultスロットの結果に基づいて完了したメソッドまたは拒否されたメソッドを呼び出して動作をデバッグします.

  • 次に、後述するextrareducreに登録されている関数を使用して保存されたインスタンスを確認し、インスタンス別にコールバック関数を実行してタスクを処理します.
  • function createThunkMiddleware(extraArgument) {
      return ({ dispatch, getState }) => next => action => {
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument);
        }
    
        return next(action);
      };
    }
    
    const thunk = createThunkMiddleware();
    thunk.withExtraArgument = createThunkMiddleware;
    
    export default thunk;

    b.2番目のパラメータ:ペイロードジェネレータ



    2番目のパラメータには、Promiseオブジェクトを返す関数が含まれます.
    このとき、この関数パラメータとしての値は以下のようになります.

  • arg:エレメント上で初めて割り当てを使用して非同期動作を作成したときに送信されるパラメータ.1つの値しか渡さないため、複数の値がある場合はオブジェクトに送信する必要があります.

  • thunkAPI:既存のredux-thunkから渡された値にいくつか追加されました.
  • --dispatch : 함수 몸체에서 무언가 작업을 한 후, 특정 액션을 또다시 dispatch할 필요성이 있을 경우 사용하면 된다.
    
    --getState : 함수 몸체에서 특정 작업을 할 때에, 현재 store의 상태를 조회해서 진행해야 할 필요성이 있을 경우 사용하면 된다.
    
    --extra : 이건 사실 사용할 일이 잘 없긴한데, 초기에 redux store을 만들면서 middleware로 해당 값을 붙여놨으면 자동으로 비동기 작업을 할 때마다 해당 데이터가 이 extra 프로퍼티의 값으로 할당되어 들어간다.
    
    //예를들어 store.js에 미들웨어로
    export const store = configureStore({
      reducer: {
        counter: counterReducer,
      },
      middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware({
          thunk: {
            extraArgument: "apiService",
            serializableCheck: false,
          },
        }),
    });
    
    이렇게 설정해뒀다면
    const incrementAsync = createAsyncThunk("counter/fetchCount", async (amount, thunkAPI) => {
      console.log(thunkAPI);
      const response = await fetchCount(amount);
      return response.data;
    });
    
    위에 보이는 두번째 인자 "thunkAPI" 의 객체 내용 중에서
    dispatch: ƒ dispatch()
    extra: "apiService"
    fulfillWithValue: ƒ (value, meta)
    getState: ƒ i()
    rejectWithValue: ƒ (value, meta)
    requestId: "UF2SrCaKAZUpbRafRisqL"
    signal: AbortSignal
    
    extra 부분에 설정해둔 값이 들어가있는 것을 확인할 수 있다.
    
    --fulfillWithValue, rejectWithValue : 스토어 업데이트를 redux store내에서 하는게 아니라, 컴포넌트에서 확인해서 사용하고 싶을 때 쓰면 된다
    
    export const incrementAsync = createAsyncThunk("counter/fetchCount", async (amount, thunkAPI) => {
      try {
        const response = await fetchCount(amount);
        return thunkAPI.fulfillWithValue(response);
      } catch (e) {
        return thunkAPI.rejectWithValue(e.response.data);
      }
    });
    
    위에처럼 설정했을 시, 컴포넌트에서 데이터를 확인해보면
    
    onClick={ () => {
                const async = dispatch(incrementAsync(incrementValue));
                console.log(async);
    }}
    //const async =
    {meta: {arg: 2, requestId: '-MN1HQ4-FtPK_bC3X0RJX', requestStatus: 'fulfilled'}
    payload: {data: 2}
    type: "counter/fetchCount/fulfilled"}
    
    이렇게 meta값이 존재하는 객체데이터가 리턴되게 된다.
    payload값만 갖고 싶다면 dispatch().unwrap() 으로 호출하면 된다. (, 이 때에는 unwrap 자체가 Promise객체를 리턴하기에 async await으로 처리해줘야 원하는 결과값을 가져올 수 있다. 안넣어주면 Promise pending을 보게될 것)
    
    --requestId : 해당 비동기 작업 호출의 고유 아이디이다. 
    前述したように,非同期動作関数生成器を用いてディスパッチを行う.
    自動dispatch pending動作を行い、2番目のパラメータに入る非同期関数を呼び出し、このプロセスの内部スロット状態に応じてメソッドを自動的に呼び出し完了または拒否するという.
    呼び出されたアクションオブジェクトには、次のタイプがあります.
    interface PendingAction<ThunkArg> {
      type: string
      payload: undefined
      meta: {
        requestId: string
        arg: ThunkArg
      }
    }
    
    interface FulfilledAction<ThunkArg, PromiseResult> {
      type: string
      payload: PromiseResult
      meta: {
        requestId: string
        arg: ThunkArg
      }
    }
    
    interface RejectedAction<ThunkArg> {
      type: string
      payload: undefined
      error: SerializedError | any
      meta: {
        requestId: string
        arg: ThunkArg
        aborted: boolean
        condition: boolean
      }
    }
    すなわち,動作が再びDispatchされるため,Dispatchされた動作をケース管理する部分が存在する,「ExtraReducers」部分である.
    // slice.js
    
    const reducer = createSlice({
      name: 'users',
      initialState,
      reducers: {},
      extraReducers: (builder) => {
        builder.addCase(fetchUserById.fulfilled, (state, action) => {})
        builder.addCase(fetchUserById.rejected, (state, action) => {})
      },
    })
    前述したように、コンストラクタオブジェクト上でaddCaseメソッドを使用して非同期操作を完了した後、2番目のパラメータの関数で実行する操作を実行できます.
    前述したように、ここでは後続の処理タスクは実行されません.
    コンポーネント内で操作を実行する場合は、ThunkAPIに存在するfulfillWithValueまたはREECT WithValueの呼び出し結果を返して、コンポーネント内でそのアクションオブジェクトのメタデータを使用できます.

    最後にRedux-toolkitで一番羨ましい部分は?


    redux-sagaは、複数のリクエストがあっても最後のリクエストしか実行できない「takeLatest」というコールバックを自動的に実現します.
    しかしredux-thunkにはこのような内容はないので少し残念です.
    redux-toolkitはDispatchのスキャンと非同期操作のスキャンを実現していることが印象的です.
    まず,非同期動作の途中でミドルウェアのように派遣を取り消す方法は以下の通りである.

    Dispatch自体がミドルウェアのように中からフィルタリングされている場合は、やるかどうかを決めてあげたいと思います。

    export const incrementAsync = createAsyncThunk(
      "counter/fetchCount",
      async (amount, thunkAPI) => {
        const response = await fetchCount(amount);
        return response.data;
      },
      {
        condition: (amount, { getState, extra }) => {
          const state = getState();
    
          if (state.counter.value === 0) {
            console.log("do nothing");
            return false;
          }
        },
      }
    );
    なお、createAsyncThunkの3番目のパラメータとして入力されるオブジェクトには、conditionプロセスに関数を登録するだけで異なるオープン値を含めることができます.
    1番目のパラメータは、非同期アクションジェネレータが呼び出されたときにパラメータとして受信される値であり、2番目のパラメータは、現在のリカバリポイントの状態値と、上述した初期登録の追加値である.
    したがって,身体中で任意の条件でfalseを返すと,ミドルウェアのように,その非同期動作オブジェクトのdispatchが失敗し,何もしない.

    キャンセル動作を受信して処理する場合は、createAsyncThunkの3番目のパラメータオブジェクトのオプションの1つである「dispatchConditionRejection:true」を設定するだけです.
    {
        condition: (amount, { getState, extra }) => {
          const state = getState();
    
          if (state.counter.value === 0) {
            console.log("do nothing");
            return false;
          }
        },
        dispatchConditionRejection: true,
      }
    
    .
    .
    .
    // createSlice 인자 객체 내부
      extraReducers: (builder) => {
    
        builder.addCase(incrementAsync.rejected, (state, action) => {
          console.log("cancel detected from extraReducers");
        });
      }, // 리젝트에서 발동하게 된다.

    すでに派遣されていて、非同期コールが行ってしまったので、キャンセルしたいです。


    このケースは、重複呼の処理に適している.
    正直、ユーザーがクリックした瞬間にUIクリックをきっぱり無効にして、結果が届くまで、そうしなければ非同期リクエストをキャンセルするロジックを作るのもいいです.△これは、ディボオンズのように最後の要求しか残っていないことを意味します.
    実際の使用状況別に分類すると.
    a.ボタンをクリックしてサーバーを呼び出したが、ユーザーが我慢できず、結果が出る前にアンインストールを終了した場合
    この場合、対応する結果値に到達する必要はありませんので、Effectを使用してキャンセルします.
    function MyComponent(props: { userId: string }) {
      const dispatch = useAppDispatch()
      React.useEffect(() => {
        const promise = dispatch(fetchUserById(props.userId))
    
        return () => {
          promise.abort() // unmount를 감지하면, Ajax 호출을 취소시킨다.
        }
      }, [props.userId])
    }
    この場合、「ThunkName/rejected」は自動的にRedux StoreにDispatchされます.
    つまり、reduceプロセスのキャンセルをextraReducersで処理したい場合は、処理に組み込むことができます.
    b.ユーザーはせっかちで、何度もボタンを押した.チェックアウトできませんか.
    チェックアウトできます.しかし個人的には、デバッガをよく使う立場では、デバッガよりもajaxリクエストを直接キャンセルする方が便利なように思います.(debonseのためにusecCalbackを使って関数を覚えるのはちょっと負担です)
    axiosによる呼び出しにより,ボタンを容赦なく押下し,複数回の呼び出しを防止する方法は以下の通りである.
    let asyncChecker = null;
    
    export const incrementAsync = createAsyncThunk("counter/fetchCount", async (amount, thunkAPI) => {
      if (!asyncChecker) {
        asyncChecker = thunkAPI.signal;
      } else {
        asyncChecker.abort();
      }
    
      const response = await axios.get("https://api.publicapis.org/entries",{ signal: asyncChecker });
      asyncChecker = null;
      return response.data;
    });
    上から見たThunkAPI信号は特別なものではなく、「New AbortController」によって生成されたインスタンスにすぎません.

    このようにして、createAsyncThunkのLexical環境で変数を作成してチェックします.
    クリックするたびに、対応する環境値を検索できます.
    この環境値にはsignalオブジェクトが含まれており、axiosリクエストのたびに信号オブジェクトが渡されます.
    複数回クリックすると、if文はasynchecker変数の内部に信号オブジェクトが存在することを確認するため、その信号オブジェクトの内部プロトタイプメソッドabortを呼び出すことによって非同期呼び出しをキャンセルします.
    このようにキャンセルすると、数百回クリックしても1回だけ結果が返ってきます.
    ネット上に飛んだリクエストをどうやってキャンセルするのか、もっと不思議ですが、どうしてもそうです.

    n/a.結論


    このように、正式なドキュメントを閲覧し、redux-toolkitの全体的な機能を確認しました.
    そのため、非同期タスクをリドスのツールとすれば、私はよく理解できる自信があります.
    「これでは、Redux Storeで非同期タスクを処理するコードが多すぎて、これらのコードを見たくありません.」
    ...
    こう見るとreact-queryはとても良いツールだと改めて実感します