Skip to content

Navigation Menu

Sign in
Sign up

Passing the optimistic value to an action #8

Unanswered
devongovett asked this question in Q&A
Discussion options

I'm thinking about patterns for actions in React Aria. A common pattern in many components is an interface like this:

interface Props<T> {
 value?: T,
 changeAction?: (v: T) => void | Promise<void>
}

Internally, we use useOptimistic to store the latest value to display to the user.

function Component<T>(props: Props<T>) {
 let [isPending, transition] = useTransition();
 let [optimisticValue, setOptimisticValue] = useOptimistic(props.value);
 let onInteraction = () => {
 transition(async () => {
 let newValue = computeNewValue();
 setOptimisticValue(newValue);
 await props.changeAction?.(newValue);
 });
 };
 // Display optimisticValue...
}

Sometimes, we need to calculate newValue based on the previous state. I think this should be based on the optimistic value, not the value prop, because the optimistic value is what the user is seeing and they will expect the updated state to reflect that. We can do that by passing a function to setOptimisticValue. Then ideally we'd pass that value to changeAction so the parent component can respond to it. However, there doesn't seem to be a good way to do this.

This does not work because the callback is not called until the next render:

let newValue;
setOptimisticValue(oldValue => {
 newValue = calculateNewValue(oldValue);
 return newValue;
});
await changeValue?.(newValue); // newValue is undefined.

This does not work because React will error that you can't trigger a re-render while rendering a different component (assuming the action sets state in the parent component):

setOptimisticValue(oldValue => {
 let newValue = calculateNewValue(oldValue);
 changeValue?.(newValue); // Error: cannot trigger a re-render while rendering a different component
 return newValue;
});

Has anyone found a good pattern for this? In the past, we've tracked the current value in a ref and used this to calculate the new value instead of using the setState callback. This feels quite hacky because you have to reset the ref on the next render to match the value prop, which seems like exactly what useOptimistic is supposed to handle. However because you can't get the actual optimistic value synchronously, there's not a good place to call the action.

You must be logged in to vote

Replies: 1 comment 1 reply

Comment options

This is also an open question for me, I mentioned it here: "Who calls useOptimistic? The caller or the design component?"

I don't see why you need the updater function here, I think that's really only needed if you have multiple events that could mutate the optimistic value - here you only have one

How if your problem different to the like button example in the docs? https://react.dev/reference/react/useOptimistic#updating-props-or-state-optimistically

They feel similar: calculate a new value based on the existing optimistic value, set the new value with setOptimisticValue, pass the new value into the change action.

You must be logged in to vote
1 reply
Comment options

devongovett Mar 21, 2026
Collaborator Author

Who calls useOptimistic? The caller or the design component?

I think it would be nice for the design component to handle this out of the box in most cases. You can always opt-out by using regular events like onChange instead of actions, and implementing your own transition.

I think that's really only needed if you have multiple events that could mutate the optimistic value

There are a few cases where this happens. One example we encountered is form reset, when many onChange events may be emitted simultaneously. A CheckboxGroup component that aggregates the state of its children will need to handle each of these synchronous onChange events and update its state accordingly. If this happens within a single event loop tick, React won't re-render in between.

We've also encountered issues with testing tools like Playwright that may emit multiple events synchronously. In general I don't like relying on React to re-render synchronously in any case (unless you use flushSync). That seems like an implementation detail that could change due to any number of factors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Q&A
Labels
None yet

AltStyle によって変換されたページ (->オリジナル) /