useReducer's dispatch should return a promise which resolves once its action has been delivered
See original GitHub issue(This is a spinoff from this thread.)
It’s sometimes useful to be able to dispatch an action from within an async function, wait for the action to transform the state, and then use the resulting state to determine possible further async work to do. For this purpose it’s possible to define a useNext
hook which returns a promise of the next value:
function useNext(value) {
const valueRef = useRef(value);
const resolvesRef = useRef([]);
useEffect(() => {
if (valueRef.current !== value) {
for (const resolve of resolvesRef.current) {
resolve(value);
}
resolvesRef.current = [];
valueRef.current = value;
}
}, [value]);
return () => new Promise(resolve => {
resolvesRef.current.push(resolve);
});
}
and use it like so:
const nextState = useNext(state);
useEffect(() => {
fetchStuff(state);
}, []);
async function fetchStuff(state) {
dispatch({ type: 'START_LOADING' });
let data = await xhr.post('/api/data');
dispatch({ type: 'RECEIVE_DATA', data });
// get the new state after the action has taken effect
state = await nextState();
if (!state.needsMoreData) return;
data = await xhr.post('/api/more-data');
dispatch({ type: 'RECEIVE_MORE_DATA', data });
}
This is all well and good, but useNext
has a fundamental limitation: it only resolves promises when the state changes… so if dispatching an action resulted in the same state (thus causing useReducer
to bail out), our async function would hang waiting for an update that wasn’t coming.
What we really want here is a way to obtain the state after the last dispatch has taken effect, whether or not it resulted in the state changing. Currently I’m not aware of a foolproof way to implement this in userland (happy to be corrected on this point). But it seems like it could be a very useful feature of useReducer
’s dispatch
function itself to return a promise of the state resulting from reducing by the action. Then we could rewrite the preceding example as
useEffect(() => {
fetchStuff(state);
}, []);
async function fetchStuff(state) {
dispatch({ type: 'START_LOADING' });
let data = await xhr.post('/api/data');
state = await dispatch({ type: 'RECEIVE_DATA', data });
if (!state.needsMoreData) return;
data = await xhr.post('/api/more-data');
dispatch({ type: 'RECEIVE_MORE_DATA', data });
}
EDIT
Thinking about this a little more, the promise returned from dispatch
doesn’t need to carry the next state, because there are other situations where you want to obtain the latest state too and we can already solve that with a simple ref. The narrowly-defined problem is: we need to be able to wait until after a dispatch()
has taken affect. So dispatch
could just return a Promise<void>
:
const stateRef = useRef(state);
useEffect(() => {
stateRef.current = state;
}, [state]);
useEffect(() => {
fetchStuff();
}, []);
async function fetchStuff() {
dispatch({ type: 'START_LOADING' });
let data = await xhr.post('/api/data');
// can look at current state here too
if (!stateRef.current.shouldReceiveData) return;
await dispatch({ type: 'RECEIVE_DATA', data });
if (!stateRef.current.needsMoreData) return;
data = await xhr.post('/api/more-data');
dispatch({ type: 'RECEIVE_MORE_DATA', data });
}
Issue Analytics
- State:
- Created 4 years ago
- Reactions:101
- Comments:54 (1 by maintainers)
yes, I’m confused right now. The Docs don’t help. Is dispatch asynchronous or synchronous? How do I know when it has finished affecting the state? with setState I have a call back… What do I do with dispatch to manage the order of execution before this feature request is included?
It’s 2020, the world is ending next year, why is this thread dead, if there’s one last accomplishment of mankind, it should be returning of the promise here!