createAsyncThunk:中止之前的请求
createAsyncThunk: abort previous request
我正在使用 createAsyncThunk
向某些 API 发出异步请求。在任何给定时刻,只有一个请求应该处于活动状态。
我知道如果我从上一次调用中返回了 Promise,则可以使用提供的 AbortSignal
中止请求。问题是,thunk 本身能否以某种方式“自动”中止先前的请求?
我在考虑两个选择:
- 保持状态中的最后一个 AbortSignal。似乎是错误的,因为状态应该是可序列化的。
- 将最后一个 Promise 及其 AbortSignal 保留在全局变量中。似乎也是错误的,因为,你知道,全局变量。
有什么想法吗?谢谢。
我不知道你的具体 api 是如何工作的,但下面是一个工作示例,说明如何将中止逻辑放入 action 和 reducer 中,它会在更新时中止任何以前活动的假提取提取完成:
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;
});
};
};
答案
中演示了如何使用它
根据@HMR 的回答,我可以把它放在一起,但它很复杂。
以下函数创建执行实际工作的“内部”异步 thunk,以及委托给内部异步 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
);
}
我正在使用 createAsyncThunk
向某些 API 发出异步请求。在任何给定时刻,只有一个请求应该处于活动状态。
我知道如果我从上一次调用中返回了 Promise,则可以使用提供的 AbortSignal
中止请求。问题是,thunk 本身能否以某种方式“自动”中止先前的请求?
我在考虑两个选择:
- 保持状态中的最后一个 AbortSignal。似乎是错误的,因为状态应该是可序列化的。
- 将最后一个 Promise 及其 AbortSignal 保留在全局变量中。似乎也是错误的,因为,你知道,全局变量。
有什么想法吗?谢谢。
我不知道你的具体 api 是如何工作的,但下面是一个工作示例,说明如何将中止逻辑放入 action 和 reducer 中,它会在更新时中止任何以前活动的假提取提取完成:
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;
});
};
};
根据@HMR 的回答,我可以把它放在一起,但它很复杂。
以下函数创建执行实际工作的“内部”异步 thunk,以及委托给内部异步 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
);
}