createAsyncThunk:中止先前的请求

时间:2020-11-03 11:12:58

标签: javascript redux redux-toolkit

我正在使用createAsyncThunk向某些API发出异步请求。在任何给定时刻,仅一个请求应处于活动状态。 我了解,如果我有上次调用返回的Promise,可以使用提供的AbortSignal中止该请求。问题是,重击程序本身可以以某种方式“自动”中止先前的请求吗? 我正在考虑两种选择:

  • 将最后一个AbortSignal保持在该状态。似乎是错误的,因为状态应该可序列化。
  • 将最后一个Promise及其AbortSignal保留在全局变量中。似乎也是错误的,因为您知道全局变量。

有什么想法吗?谢谢。

2 个答案:

答案 0 :(得分:1)

我不知道您的特定api的工作方式,但以下是一个有效的示例,说明如何将异常中止逻辑放入动作和化简器中,它将在进行较新的提取时中止任何先前处于活动状态的伪造提取: >

import * as React from 'react';
import ReactDOM from 'react-dom';
import {
  createStore,
  applyMiddleware,
  compose,
} from 'redux';
import {
  Provider,
  useDispatch,
  useSelector,
} from 'react-redux';
import {
  createAsyncThunk,
  createSlice,
} from '@reduxjs/toolkit';

const initialState = {
  entities: [],
};
// constant value to reject with if aborted
const ABORT = 'ABORT';
// fake signal constructor
function Signal() {
  this.listener = () => undefined;
  this.abort = function () {
    this.listener();
  };
}
const fakeFetch = (signal, result, time) =>
  new Promise((resolve, reject) => {
    const timer = setTimeout(() => resolve(result), time);
    signal.listener = () => {
      clearTimeout(timer);
      reject(ABORT);
    };
  });
// will abort previous active request if there is one
const latest = (fn) => {
  let previous = false;
  return (signal, result, time) => {
    if (previous) {
      previous.abort();
    }
    previous = signal;
    return fn(signal, result, time).finally(() => {
      //reset previous
      previous = false;
    });
  };
};
// fake fetch that will abort previous active is there is one
const latestFakeFetch = latest(fakeFetch);

const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async ({ id, time }) => {
    const response = await latestFakeFetch(
      new Signal(),
      id,
      time
    );
    return response;
  }
);
const usersSlice = createSlice({
  name: 'users',
  initialState: { entities: [], loading: 'idle' },
  reducers: {},
  extraReducers: {
    [fetchUserById.fulfilled]: (state, action) => {
      state.entities.push(action.payload);
    },
    [fetchUserById.rejected]: (state, action) => {
      if (action?.error?.message === ABORT) {
        //do nothing
      }
    },
  },
});

const reducer = usersSlice.reducer;
//creating store with redux dev tools
const composeEnhancers =
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
  reducer,
  initialState,
  composeEnhancers(
    applyMiddleware(
      ({ dispatch, getState }) => (next) => (action) =>
        typeof action === 'function'
          ? action(dispatch, getState)
          : next(action)
    )
  )
);
const App = () => {
  const dispatch = useDispatch();
  React.useEffect(() => {
    //this will be aborted as soon as the next request is made
    dispatch(
      fetchUserById({ id: 'will abort', time: 200 })
    );
    dispatch(fetchUserById({ id: 'ok', time: 100 }));
  }, [dispatch]);
  return 'hello';
};

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

如果您仅需要在承诺是最新请求的承诺时解决该承诺,而无需中止或取消正在进行的承诺(如果不是最新的则忽略解析),则可以执行以下操作:

const REPLACED_BY_NEWER = 'REPLACED_BY_NEWER';
const resolveLatest = (fn) => {
  const shared = {};
  return (...args) => {
    //set shared.current to a unique object reference
    const current = {};
    shared.current = current;
    fn(...args).then((resolve) => {
      //see if object reference has changed
      //  if so it was replaced by a newer one
      if (shared.current !== current) {
        return Promise.reject(REPLACED_BY_NEWER);
      }
      return resolve;
    });
  };
};

如何使用它在this答案中得到了证明

答案 1 :(得分:0)

基于@HMR回答,我可以将其组合在一起,但这非常复杂。

下面的函数创建执行实际工作的“内部”异步thunk,并创建委托给内部的“外部”异步thunk,并中止以前的调度(如果有)。

内部thunk的有效载荷创建器还包装为:1)等待有效载荷创建器的上一次调用完成,2)如果在等待期间操作被中止,则跳过调用真实的有效载荷创建器(以及API调用)

import { createAsyncThunk, AsyncThunk, AsyncThunkPayloadCreator, unwrapResult } from '@reduxjs/toolkit';

export function createNonConcurrentAsyncThunk<Returned, ThunkArg>(
  typePrefix: string,
  payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg>,
  options?: Parameters<typeof createAsyncThunk>[2]
): AsyncThunk<Returned, ThunkArg, unknown> {
  let pending: {
    payloadPromise?: Promise<unknown>;
    actionAbort?: () => void;
  } = {};

  const wrappedPayloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg> = (arg, thunkAPI) => {
    const run = () => {
      if (thunkAPI.signal.aborted) {
        return thunkAPI.rejectWithValue({name: 'AbortError', message: 'Aborted'});
      }
      const promise = Promise.resolve(payloadCreator(arg, thunkAPI)).finally(() => {
        if (pending.payloadPromise === promise) {
          pending.payloadPromise = null;
        }
      });
      return pending.payloadPromise = promise;
    }

    if (pending.payloadPromise) {
      return pending.payloadPromise = pending.payloadPromise.then(run, run); // don't use finally(), replace result
    } else {
      return run();
    }
  };

  const internalThunk = createAsyncThunk(typePrefix + '-protected', wrappedPayloadCreator);

  return createAsyncThunk<Returned, ThunkArg>(
    typePrefix,
    async (arg, thunkAPI) => {
      if (pending.actionAbort) {
        pending.actionAbort();
      }
      const internalPromise = thunkAPI.dispatch(internalThunk(arg));
      const abort = internalPromise.abort;
      pending.actionAbort = abort;
      return internalPromise
        .then(unwrapResult)
        .finally(() => {
          if (pending.actionAbort === abort) {
            pending.actionAbort = null;
          }
        });
    },
    options
  );
}