-
Notifications
You must be signed in to change notification settings - Fork 47.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
useReducer's dispatch should return a promise which resolves once its action has been delivered #15344
Comments
dispatch
should return a promise for obtaining the resulting next stateuseReducer
's dispatch
should return a promise for obtaining the resulting next state
useReducer
's dispatch
should return a promise for obtaining the resulting next state
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
And then later:
|
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
|
yes, I'm confused right now. |
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 |
@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. |
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(); |
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.) 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 });
}
We can make it into a simple custom hook. Although However, it seems really useful if dispatch returns a Promise with an updated state. |
I've tried out two solutions for this. Both of them rely on
Using a callback (not recommended) stackblitzThe service call looks like this:
Reducer looks like this:
As you can see, reducer calls I don't like this solution because it is really ugly syntactically. Using promise (not sure if recommended, but better) stackblitzThe service call looks like this:
Reducer looks like this (it's actually exactly the same as before):
As you can see, reducer calls What i like about this solution is, that it will be easy to refactor, should - 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;
...
}
} NoteWhile 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:
With approach 2 there is a significant difference. See here. The output shows that dispatch 2 happens after dispatch 1:
|
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 Or instead, just like this.setState(~~~, callbackFunction) It's just an idea, the const [state, dispatch] = useReducer(developerReducer, initialState);
dispatch(
{ type: 'SOME_ACTION' },
callbackFunction
); |
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:
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:
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 |
@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? |
I keep encountering situations where this promise or callback would be very helpful. Does anyone know any strong reason why this would not be implemented? |
+1 |
May the force be with |
Im curious on the reasoning for the React team not prioritizing this. Using a I should be able to listen for the reducer to finish and focus the input within the function scope where I call dispatch rather than creating some complicated confusing useEffect magic code that is prone to overreach for the purpose of input focus at least. I could break reducer rules and call a callback/make a side effect, which would be an anti-pattern, but would be easier-to-reason-about code than (Other than FP ideology, I wonder what the other reasons for avoiding effects in reducers are. Will it necessarily result in a a reasonable bug risk? More research needed there on my part) |
Why does it matter when an action was delivered? What's that mean - it was applied to the store by the reducer? The effect of the action is an updated store. The only event a component should be interested in, due to an action, is that the store was updated, and it can use a selector to react to that event. |
Sometimes responding to changes on the entire store will make sense. Other times it wont. In my case, I want to know when the store is updated with my item in a very specific scenario so I can do a thing related to the updated item. I want my code to be declarative and read nicely. So With the latter, I need to make extra sure |
I'm not on the React team, but I can think of at least a few possible reasons off the top of my head:
There's excellent reasons for this, and it's not just "ideology". In Concurrent Rendering, React can and will start rendering components, only to pause and throw away the results. Because of that, React expects that the rendering process truly is "pure" with no side effects. React calls your reducers at a couple points in the lifecycle and rendering process. It apparently does sometimes try to call it immediately when the update is queued to see if there's a chance of bailing out early. Assuming it keeps going, then it will normally get called during the rendering phase. So, if you start putting side effects in reducers, you will almost definitely see unexpected behavior because those effects are running even if the component isn't getting updated at that point. Note that I'm not saying that the feature being asked for in this thread is bad, impossible, or unwanted. I'm just observing that the React team has limited resources and other major priorities atm, and that proposed workarounds that involve side effects in reducer functions are likely going to cause problems in real apps. |
@markerikson that is a very useful explanation, thanks for taking the time to write it. I think I have another solution to my specific problem that is less convoluted than a useEffect. (Munging ui (focus) state with data state as a tradeoff). If I were to pollute my reducer with a side effect and there was no downside to redundant calls to that side effect, is there still a risk of issues with your understanding? Just curious where rules can be bent. Not meaning to call all of FP 'ideology' , as I actually quite like it. When blindly followed, anything start to feel that way though :) |
@mmnoo that's sort of what I was saying. Think of it is less as "rules", and think of it as "how is this going to potentially change behavior in my application?". I can't give specifics, because I don't know your app and I don't know what side effects you're considering doing here. If the "side effect" is logging to the console or writing to localStorage, then while it's technically "breaking the rules", it's meaningless in the grand scheme and won't cause bugs. If it's, say, making an So, try to think about behavior based on the stated way React is going to execute things, not "am I breaking the rules". |
Sorry, I know this is frustrating. (It's a known pain point with Hooks.) The truth is we don't know what a good solution looks like for this problem. We don't think the proposed solutions we've seen so far (here and in many other places) solve the issue very well. There are several problems with the proposed solution. Something like If the issue is "I just want a setState callback to do something imperative", then Hooks don't offer that—but if the only problem is that React delays DOM update, you can force it to happen sync. import { flushSync } from 'react-dom'
flushSync(() => {
setTabVisible(true)
}) // flushes DOM update
tabRef.current.scrollIntoView(); // updated by this point This isn't quite the same but solves a number of cases. This doesn't let you read the latest state value. But allowing to read it is also not enough, unfortunately. It also needs to be able to read fresh props which come from parent component. Or possibly other state. That's easy with classes because they're mutable, but you can't do it with Hooks without managing refs yourself. So it's not clear how a React API would do this. You could of course always call the reducer yourself. dispatch(action)
let nextLikelyState = reducer(state, action)
// ... Not super pretty but you have full control. So — we'd like to fix this, if we knew how to do it well. |
flushSync is not a solution I had considered or was aware of, thanks for the pointer! I think it will serve my needs well enough, though it seems a bit heavy-handed to force the update to happen synchronously. Would an async version be possible? I reckon that on its own would cover most of the need, since the tools available for selecting and passing around state are already pretty good. // Asynchronous version of flushSync. Not sure about the name. awaitFlush() perhaps?
await flushAsync(() => {
setTabVisible(true)
}) // flushes DOM update
tabRef.current.scrollIntoView(); // updated by this point The above suggestion is bare-bones, but I think you could do quite a lot with it. As an example: function usePromiseReducer(reducer, initialState, initializer) {
const [state, dispatch] = useReducer(reducer, initialState, initializer);
const ref = useRef(state);
// Keep track of the committed state. Layout effects happen before flushAsync resolves.
useLayoutEffect(() => {
ref.current = state;
});
const wrappedDispatch = useCallback(async (action) => {
// Wait for the next committed (or skipped) component render
await flushAsync(() => dispatch(action));
// Return the latest state, whether it was updated or not.
// The reducer may have processed multiple actions before we get here.
return ref.current;
}, [dispatch]);
return [state, wrappedDispatch];
} |
I have come up with work around this problem, This might help
|
No that's basically an anti-pattern in React as things currently stand. Our painful experience is that useEffect currently does not always run to completion as any re-render can interrupt the running instance. That's difficult to reproduce and your simple example may not have enough changing variables or your callback running long enough to see it but it happens and dramatically reduces the usability of useEffect. You basically can only use useEffect in cases where you are idempotent and certain that useEffect will keep getting called until whatever it is doing completes. |
Following up on @gaearon last suggestion, a potential workaround for synchronous actions would be to wrap the // const reducer = ...;
// const initialState = ...;
const useCustomReducer = (reducer, initialState) => {
const [state, dispatch] = React.useReducer(reducer, initialState);
return [
state,
(action) => {
dispatch(action);
return reducer(state, action);
},
];
};
const someComponent = () => {
const [state, dispatch] = useCustomReducer(reducer, initialState);
const someFunction = () => {
const result = dispatch({ type: 'DO_SOMETHING', payload: 'some value' });
console.log(`Result from my dispatch obtained immediately: ${result}`);
console.log(`This is the sate without the change caused by the latest dispatch: ${state}`);
};
return <button onClick={someFunction}></button>;
}; |
@juanpreciado I believe this is a flawed approach to this problem. You are using the |
The following is my attempt at solving this problem. I utilize an Internally, the way it works is that whenever a dispatch is called it stores the useReducerPromise.js: import { useReducer, useMemo } from "react";
import EventEmitter from "eventemitter3";
const events = new EventEmitter();
const waiters = new WeakMap();
events.on("reduced", function (action, newState) {
const resolver = waiters.get(action);
if (resolver) {
waiters.delete(action);
resolver(newState);
}
});
function reducerWrap(reducer) {
return function (state, action) {
const reduced = reducer(state, action);
events.emit("reduced", action, reduced);
return reduced;
};
}
function dispatchWrap(dispatch) {
return function (action) {
const p = new Promise(function (resolve, reject) {
waiters.set(action, resolve);
});
dispatch(action);
return p;
};
}
export default function useReducerPromise(reducer, ...reducerArgs) {
const wrappedReducer = useMemo(() => reducerWrap(reducer), [reducer]);
const [state, dispatch] = useReducer(wrappedReducer, ...reducerArgs);
const wrappedDispatch = useMemo(() => dispatchWrap(dispatch), [dispatch]);
return [state, wrappedDispatch];
} |
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment! |
bump |
I'm not sure how Redux works under the hood, but if Redux can return a Promise with the dispatch function what's preventing the React team from implementing it the same way? Example of awaiting dispatch with redux
Is it because Redux uses classes as opposed the useReducer being a hook? |
@anolan23 no, this only works with Redux because you can configure a Redux store to use the "thunk" middleware. Since Redux middleware wrap around the See these resources:
|
I see many blog posts addressing asynchronous implementations of this hook, and I came here searching the issues trying to understand why From my understanding, the dispatch function is called asynchronously, but may be called repeatedly. This is fine, as the dispatch function may indeed to called from different contexts (e.g. inside a However, this does not resolve many use cases, where the state depends on asynchronous executions. For example, a component has a form with server validations. Currently, this requires a reducer and some hacky Something like : const reducer = async (setState, action) => {
switch (action.type) {
case 'foo':
const foo = await somethingFoo();
setState(state => ({ ...state, foo }));
break;
}
}
...
const [ state, dispatch ] = useAsyncReducer(reducer, initialState);
...
await dispatch({ type: 'foo' });
// the reducer has returned at this point Note that the reducer now takes a function similar to |
I thought I would leave my solution: if I want my dispatch to "accept callbacks" then I add a queue of callbacks to my state. // example of an action with a callback
const doSomethingWithCallback = (payload, callback) => {
dispatch({ type: DO_SOMETHING, payload })
dispatch({ type: ADD_CALLBACK, payload: callback })
} Then elsewhere, usually where the useReducer is called const [state, dispatch] = useReducer(reducer, initialState)
useEffect(() => {
if (isEmpty(state.scheduledCallbacks)) {
return
}
state.scheduledCallbacks.forEach((callback) => {
callback(state)
})
dispatch({ type: ResourceActionType.CLEAR_CALLBACKS })
}, [state.scheduledCallbacks]) I have also considered pulling this into my useReducer implementation directly, so that all my reducers can do this generically... |
(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:and use it like so:
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 causinguseReducer
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
'sdispatch
function itself to return a promise of the state resulting from reducing by the action. Then we could rewrite the preceding example asEDIT
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 adispatch()
has taken affect. Sodispatch
could just return aPromise<void>
:The text was updated successfully, but these errors were encountered: