(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 });
}
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 });
}
Would use. Any easy hacks to implement this?
Could put a “refresh” counter in state and watch for it to change with useNext, but this may cause unnecessary rerenders ?
If you're willing to pollute your actions, could you add a callback to the action and call it from your reducer?
For example (haven't actually tried this, but...), wrap your reducer
and dispatch
using something like:
function useReducerWithResolvedDispatch(reducer, initialState, init) {
const reducerWithResolve = useCallback(
(state, action) => {
nextState = reducer(state, action)
if (action._resolveDispatch)
action._resolveDispatch(nextState)
return nextState
},
[reducer]
)
const store = useReducer(reducerWithResolve, initialState, init)
const [state, dispatch] = store
dispatch.resolved = useCallback(
(action) => (new Promise((resolve, reject) => {
action._resolveDispatch = resolve
dispatch(action)
})),
[dispatch]
)
return store
}
And then later:
...
let nextState = await dispatch.resolved({ 'type': 'FOO', ... })
If you're willing to pollute your actions, could you add a callback to the action and call it from your reducer?
A reducer must be a pure function with no side effects; it may be called multiple times, and its having been called is no guarantee that the new state it produced was actually used.
A reducer must be a pure function with no side effects; it may be called multiple times, and its having been called is no guarantee that the new state it produced was actually used.
Yeah, I hear ya. But, to enhance my understanding: What do you mean by a reducer may be "called multiple times" (multiple times for different actions prior to render, multiple times for the same action, or...)? And when would you consider a state to be "used"? When it's "seen" during a render?
How about the following? When requested via dispatch.resolved()
, we add a unique id to the action, record the ids of reduced actions in the state, and then notify the waiter in an effect. We track ids we've already seen, and lazily cleanup the state via future dispatches.
function useReducerWithResolvedDispatch(reducer, initialState, init) {
const r = useRef({
lastId: 0,
resolve: {},
resolved: [],
state: null,
})
const _init = useCallback(
([state, included]) => ([init ? init(state) : state, included]),
[init]
)
const _reducer = useCallback(
([state, includes], [aId, resolved, action]) => {
if (resolved.length)
includes = includes.filter((i) => !resolved.includes(i))
if (aId)
includes = [...includes, aId]
return [reducer(state, action), includes]
},
[reducer]
)
const [[state, includes], dispatch] = useReducer(
_reducer, [initialState, []], _init)
const _dispatch = useCallback(
(action) => {
dispatch([0, r.current.resolved, action])
},
[dispatch]
)
_dispatch.resolved = useCallback(
(action) => (new Promise((resolve, reject) => {
const aId = ++r.current.lastId
r.current.resolve[aId] = resolve
dispatch([aId, r.current.resolved, action])
})),
[dispatch]
)
useEffect(() => {
r.current.state = state
},
[state]
)
useEffect(() => {
for (const aId of includes) {
if (r.current.resolve[aId]) {
r.current.resolve[aId](r.current.state)
delete r.current.resolve[aId]
}
}
r.current.resolved = includes
},
[includes]
)
return useMemo(() => ([state, _dispatch]), [state, _dispatch])
}
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?
obviously dispatching is async, there is no way to know when the state has been updated.
If it works the same way as redux then it makes sense that it's async. I don't see anywhere in the hooks doc that actually clearly state that dispatch is actually async though. I assume it is since it looks like it's supposed to work similarly to redux (heck, redux is called out in the docs).
But, I certainly don't consider it "obvious".
Why was this closed? AFAIK - the state change is immediate, and the changes to the DOM are made apparently in the microtask queue. So after a Promise.resolve, the new state and DOM changes are complete. But it seems crazy to rely on this. Returning a Promise for when the state change has completed AND the rerender resulting from it is complete makes sense to me.
If I have to throw my actions 'into the void' and only depend on React to rerun my functions for me, and I can't hook upon a Promise for 're-rendering is complete', then I am unnecessarily coupled to React.
@deanius it wasn’t closed.
Instead of returning a promise, could it not also accept a callback - kind of like how setState
in classes work, I don't really have a feeling over which would be nicer, but I expect the use-case is similar to the class setState
use-case, I.e. do this action, and then run this callback that may call a function on props
@johnjesse - Promises have a contract, unlike callbacks, and protect the code that performs step A from even knowing about a function to do step B. Modern APIs should use Promises without a compelling reason not to—it makes for simpler code all around, that's why I didn't propose that even though some React APIs still accept callbacks (ReactDOM.render I'm looking at you!)
I'm using useReducer to manipulate my Context, I need to know when the Dispatch is finished and the actions has been completed. I believe returning a promise is a clean way to achieve this.
Any update on this, one way would be to use the useEffect on the state variable that is being changed by the action. This way we can know when the state finished updating
There a lot of different solutions in user-lands however, most of them blocking UI thread.
I think something like this - https://codesandbox.io/s/use-reducer-promisify-dispatch-ffnm3
Could not block UI thread and pretty simple to use
Any updates? It could be really helpful to await dispatchs to perform any given task without using a lot of useEffect that doesn't really trigger on any order in particular.
@pelotom I may be missing something, but wouldn't the nextState() promise in your example get triggered by any state change? It need not necessarily be due to the effect of the action 'RECEIVE_DATA' on the state.
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();
@pelotom I may be missing something, but wouldn't the nextState() promise in your example get triggered by any state change? It need not necessarily be due to the effect of the action 'DISPATCH_DATA' on the state.
Yes, it would resolve in response to any change to that particular piece of state. It's all around not a great solution, hence this issue.
(For the people who doesn't want to use any libraries but want to solve these kind of issues.)
How about using useMemo()
instead of useEffect()
?
const stateRef = useRef(state);
useMemo(() => { //useMemo runs its callback DURING rendering.
stateRef.current = state;
}, [state]);
useEffect(() => {
fetchStuff();
}, []);
async function fetchStuff() {
dispatch({ type: 'START_LOADING' });
let data = await xhr.post('/api/data');
dispatch({ type: 'RECEIVE_DATA', data }); //it will triggers useMemo.
if (!stateRef.current.needsMoreData) return;
data = await xhr.post('/api/more-data');
dispatch({ type: 'RECEIVE_MORE_DATA', data });
}
useEffect()
runs after rendering, but useMemo()
runs during rendering.
dispatch({ type: RECEIVE_DATA', data })
updates the state,useMemo()
will updates stateRef.current
,if (!stateRef.current.needsMoreData) return;
)We can make it into a simple custom hook.
Here's the codesandbox for above code including custom hook.
Although useMemo()
may not an API for the usage like this, but it works.
However, it seems really useful if dispatch returns a Promise with an updated state.
Hi everyone, what is the current status of this FeatureRequest? Does React core team (@aweary ) thinks that we need to implement with behavior? Should someone work on this?
I personally really miss being able to call getState() (similar to redux-thunk middleware) or being able to await next state in return of dispatch.
This feature would really help to create more clean and independent logic inside single function.
(For the people who doesn't want to use any libraries but want to solve these kind of issues.)
How about usinguseMemo()
instead ofuseEffect()
?
I don't have a thorough understanding of the react internals, but the main problem I see with this is that it's not concurrency safe. The render
and commit
phases are separate.
The fact that a component is being rendered does not guarantee that this particular render is going to be committed. It may be paused, re-rendered because of new data, and then committed. In the mean-time the data you stored in a ref may not match what's visible on screen.
The team that I work with prefer to minimise external dependencies, so we don't use redux, etc.
We're using useReducer
and wrote a dead-simple wrapper around it that hides the weirdness of useEffect
and the render / commit phases.
It's effectively a drop-in replacement for useReducer
https://codesandbox.io/s/new-frost-ll1y9 - shows an async incrementer, with cancellation.
Dead simple data fetching example here:
const reducer = (prevState, action) => {
switch(action.type) {
case "START_FETCHING": {
return { ...prevState, isFetching: true }
}
case "DONE_FETCHING": {
return { ...prevState, isFetching: false, myData: action.payload }
}
default: {
return prevState
}
}
}
const callMyApi = async (newState, action) => {
switch(action.type) {
case "START_FETCHING": {
const response = await window.fetch("api/mydata")
const myData = await response.json()
return doneFetching(myData)
}
default: {
return // Nothing more to do here
}
}
}
effects
property like so:const startFetching = () => ({
type: "START_FETCHING",
payload: undefined,
effects: fetchingMyDataSideEffect
})
const doneFetching = (myData) => ({
type: "DONE_FETCHING",
payload: myData,
// no need to add `effects` here, unless there are more side effects that need to run
})
useReducerWithEffects
const [state, dispatch] = useReducerWithEffects(reducer, { isFetching: false, myData: null })
EDIT:
Worth noting- there is a third argument to useReducerWithEffects
:
window.fetch
which handles error response status codes, credentials: "include"
and a few other cross-cutting concerns that we face when making API requests.You also get the added benefit, that most of your side effect functions can be unit tested very easily using async
, which is supported by jest
and other test frameworks.
Just:
You may be able to add another layer of abstraction to get even better testability.
We find that this is a lot less burdensome than calling act(...)
a bunch of times in your React component tests. In most cases now, we'll need act
at most one, maaaaybe two times if a component for some reason has internal state that is deemed important enough to test against.
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!
@m-adilshaikh this is likely not gonna be supported.
@a-eid Sure, useReducer
ought to be deprecated entirely, it's incompatible and based on fairy dust design philosophy. A dead load that practically serves nothing but neo-puritan egos.
@m-adilshaikh I don't think so, I do use it in my apps ( with the context api .)
@a-eid That's what I am referring to, everyone uses it, myself included. That's why I have come to the revelation, and I am ripping it out just about now. It's a tyrannical method forced on the ecosystem through soft power.
I've tried out two solutions for this. Both of them rely on reduce
doing a bit of extra work. The use case looks like this:
endpoint1
, receive result1
, then dispatch action update1
with payload result1
update1
endpoint2
, using information from updated stateThe service call looks like this:
async function callApi(dispatch) {
const result1 = await doServiceCall();
const callback = async (newState) => {
const result2 = await doServiceCall();
dispatch({type: 'updateAfterEndpoint2', result: result2});
}
dispatch({type: 'updateAfterEndpoint1', result: result1, callback});
}
Reducer looks like this:
function reduce(state, action) {
switch(action.type) {
case 'updateAfterEndpoint1':
state = "new state";
action.callback(state)
return state;
...
}
}
As you can see, reducer calls action.callback(state)
.
I don't like this solution because it is really ugly syntactically.
The service call looks like this:
function makeResolver() {
let _resolve;
const promise = new Promise(resolve => _resolve = resolve);
return [promise, _resolve];
}
async function callApi(dispatch) {
const result1 = await doServiceCall();
const [promise, resolve] = makeResolver();
dispatch({type: 'updateAfterEndpoint1', result: result1, resolve});
const newState = await promise;
const result2 = await doServiceCall();
dispatch({type: 'updateAfterEndpoint2', result: result2});
}
Reducer looks like this (it's actually exactly the same as before):
function reduce(state, action) {
switch(action.type) {
case 'updateAfterEndpoint1':
state = "new state";
action.resolve(state)
return state;
...
}
}
As you can see, reducer calls action.resolve(state)
.
What i like about this solution is, that it will be easy to refactor, should useReducer
introduce the Promise API one day:
- const [promise, resolve] = makeResolver();
- dispatch({type: 'updateAfterEndpoint1', result: result1, resolve});
- const newState = await promise;
+ const newState = await dispatch({type: 'updateAfterEndpoint1', result: result1});
function reduce(state, action) {
switch(action.type) {
case 'updateAfterEndpoint1':
state = "new state";
- action.resolve(state)
return state;
...
}
}
While the approach with the promise is nicer syntactically, it is also more "correct" in terms of code flow:
With approach 1, the code is called _as part of the execution stack of the reducer_. Therefore it can happen that _we accidentally trigger a dispatch inside a dispatch_. See here.
The output shows that dispatch 2 happens while dispatch 1 is still ongoing:
REDUCER: start
update after endpoint 1 – undefined
update after endpoint 2 – undefined
REDUCER: end
With approach 2 there is a significant difference. See here.
The output shows that dispatch 2 happens _after_ dispatch 1:
REDUCER: start
update after endpoint 1 – undefined
REDUCER: end
update after endpoint 2 – undefined
Thank you Dear @pelotom, actually, the lack of functional component is this issue, I wanna be sure the re-render is finished and then I wanna do something, So really the dispatch
need to have a promise returned.
Or instead, just like this.setState
it could have a callback argument:
this.setState(~~~, callbackFunction)
It's just an idea, the dispatch
function could have a callback argument:
const [state, dispatch] = useReducer(developerReducer, initialState);
dispatch(
{ type: 'SOME_ACTION' },
callbackFunction
);
we need to be able to wait until after a dispatch() has taken affect.
I think it will be good to have it perhaps with a different hook. My last implementation to achieve this, was to use a reducer wrapper on the useReduce something like:
const promiseReducer = (reducer, pState, initialState=() =>
({...initialState, dispatcherPromise: Promise.resolve('Initial State')}) ) => {
const [state, dispatch] = useReducer(reducer, pState, initialState);
const pDispatch = async (params) => {
params.data.dispatcherPromise = Promise.resolve(nanoid());
dispatch(params);
return await state.dispatcherPromise;
};
return [state, pDispatch];
}
I use the params.data.dispatcherPromise here for demo purposes you should move it in the actual component's effects functions. You will guarantee state update because an extra state property patch (dispatcherPromise) can use nanoid or an equivalent randomizer to create unique identifiers that will trigger the state update. The iniialState function is to initialize the state property so you won't see an error the first time on the state.dispatcherPromise. Then for the implementation part:
const [state, dispatch] = useServiceContext(sContext);
const makeUpdate = (itemQty) => {
dispatch({type: 'changeQuantity', data:{uid, qty:itemQty}})
.then(data => {
console.log('dispatch promise resolved with:', data);
}
)
};
I use a custom hook to get the state/dispatcher with the previously mentioned reducer, not shown here as it is beyond the point. So far with I've tried I haven't seen an issue.
I subscribed to this issue a while back and forgot all about it until this latest message. As a workaround for the original use case I had for wanting a promise from dispatch
I've been using this library: https://github.com/conorhastings/use-reducer-with-side-effects. It basically allows you to pair a side effect based on the state from the reducer. Not exactly the same ask, but depending on your situation it may satisfy your requirements. It was originally written a couple months after this issue was raised, but has been around for a little over a year now. I only started using it very recently, but so far it's working fine.
@stuckj yes that's the tradeoff, as this library you mentioned does. I do a similar thing. You can keep the state pure but have the payload with the side effect. In my approach above I used a state prop as the promise holder but it's better to use a payload parameter instead. It's easier to implement, because the reducer is executed immediately with the original dispatch call, so the promise property is populated right then and it becomes available to the caller.
Does react development team consider adding this feature in the future at all?
Most helpful comment
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?