-
Notifications
You must be signed in to change notification settings - Fork 45.9k
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
Batching makes it difficult to perform imperative actions like focus #18402
Comments
I started wondering if RFCs for changes to React isn't a better place to write this issue. Let me know if you agree and I will close this one and write one there. |
If anything, we are going to batch more by default, not batch less. Batching is important for multiple reasons, including drastically improving performance for bubbling events, and to avoid inconsistent trees. If you need to make a specific call unbatched, you can do this manually: ReactDOM.flushSync(() => {
// this setState won't be batched
setState(something)
}) |
Is there a way to apply We constantly observe bugs caused by the same code behaving differently in two places because at one place batching is happening and at the other, it isn't. I am not saying disabling batching by default, I believe in general this is better for most projects but I can clearly see that in our project it is introducing bugs for the past year. This is strange because our software is almost bug-free. However, batching is probably our biggest contributor to bugs right now. |
No. (And you'd need to apply it to setState calls, not event handlers.)
This is interesting. This behavior has been there for over five years, and this is the first time I'm hearing this worded so strongly. (Usually people learn about batching once, learn not to read from If it's the inconsistency that gets you, you could apply |
I guess if you really want to do it, maybe something like this could work: const origSetState = React.Component.prototype.setState
React.Component.prototype.setState = function() {
ReactDOM.flushSync(() => {
origSetState.apply(this, arguments);
})
} and you can probably come up with something similar for Hooks (or use your own Needless to say, this is terrible for performance and will cause issues both for third-party components and for the future you who will be debugging this. |
I am currently writing an example to show you the cases we are hitting and why bugs are appearing. Will get back to you soon. |
Here is the example: https://codesandbox.io/s/cranky-sanderson-7kdcx. Now let me explain:
The main problem is that Note that if you can't understand what are the use cases of having access to a ref after updating the state I can go into bigger details and real-world use cases in our app. I hope that you understood me. Let me emphasize that our app manages focus a lot and React isn't good at handling focus. Maybe if a good solution for focusing exists this kind of problems can be fixed with an abstraction and not by disabling batched updates but until then I have tried many things and haven't found a solution. |
By the way, using |
Thanks for a clear example.
That's a good callout. It's fair to say React doesn't really give you the tools to do it very well. We've been doing some work in that area but it's behind an experimental flag and is not ready to be a stable API yet. This is definitely something that's on our minds but we won't have a solution in the very near future. In class-based code, you could do this to work around the problem: setState({ isVisible: true }, () => {
this.inputRef.curent.focus()
}) That's the idiomatic workaround in classes. We don't (yet?) have something similar exposed for Hooks because that exact API won't work. In particular, it would "see" old props and state which would be highly confusing. Currently, in function components the only way you can get "notified" about commits is with useLayoutEffect(() => {
if (isTwoFactorAuthenticationVisible) {
inputRef.current.focus();
}
}, [isTwoFactorAuthenticationVisible]); That would solve your problem: https://codesandbox.io/s/laughing-fast-9785u. However, I agree this solution isn't ideal because the desire to focus might in some cases have more to do with which action triggered it rather than which state changed. I think there are at least two opportunities for RFC design here:
I don't think the solution is to "revert back" to a paradigm where each setState is atomic. We think this would be a regression. Instead, we think this needs to be "fixed forward" by finding the right abstractions that work with the React model. In the meantime, I hope the workarounds above are reasonable. |
I reopened this for discussion with a different title. |
Here's a small abstraction you could build in userland today: const perform = useCommand(command => {
if (command === "focusInput") {
inputRef.current.focus();
}
});
// ...
setState(...)
perform("focusInput'); https://codesandbox.io/s/blissful-elgamal-umn3p In this example, Unlike my earlier example (https://codesandbox.io/s/laughing-fast-9785u), note that this only sets the focus on every click, even if the state didn't change. This seems to match your intent of correlating this with the user action rather than with the state change. Don't know if this is sufficient for this use case but maybe at least for a subset? |
cc @sophiebits I think you had something similar for |
@astoilkov Can you tell me more about your use cases that don't have to do with focus? You mentioned measurements. Some simplified but relatively realistic code would help here. |
Let me give you an example not related to focus. I will describe it as making a code example will be complicated. BTW if you don't understand the example I can make a GIF showcasing the use case in our app.
function openTab() {
// call Redux or other global store for changing the file state
// access the Files Sidebar ref to the fileB item and call `scrollIntoView()`
} Regarding the RFC: I read it few months ago when I was struggling with finding a good implementation for calling I agree the solution isn't to revert back. Your comments made me think. I will reflect on our conversation and I will write here again. Thank you for the time spent considering this. |
https://codesandbox.io/s/objective-rgb-lb0cu I made a new example so we can discuss a more real-world scenario. Our app has global shortcuts for opening tabs. Opening a tab should focus the editor. How do we focus the newly created editor? After we know how to do it, how do we make it work when batching updates? I am aware that we can again use |
I started understanding why you wanted me to give you an example for getting measurements. Because We don't have an example with function openTab() {
// change state
// this is harder
const rect = elementRef.current.getBoundingClientRect()
} This can be solved with a similar technique to function openTab() {
// change state
nextLayoutEffect(() =>
const rect = elementRef.current.getBoundingClientRect()
})
} |
We chatted about this with @sebmarkbage and he noticed these examples tend to be similar to our internal notion of “update queues”. So maybe that’s something we could expose as a lower level concept. It seems like they often have a particular thing in common: you want “the last one to win”. For example:
However these queues are separate. You don’t want scrollIntoView to “overwrite” the focus operation. Note that declarative attributes like autoFocus suffers from a flaw where instead of the last operation winning, we have the last sibling winning. That doesn’t really make sense. It’s the last operation that matters. We were thinking there could something be: import { queueFocus } from 'react-dom'
const onClick = () => {
queueFocus(inputRef)
setState()
}) Note we’re passing the ref itself. So it doesn’t matter that React hasn’t set it yet. It will read from mutable object when the time comes. If it’s a more generic concept that’s not limited to focus then it could get more powerful. But also more verbose. // singleton
export const useScrollQueue = createEffectQueue()
export const useFocusQueue = createEffectQueue()
// app
import { useFocusQueue } from 'somewhere'
// ...
const queueFocus = useFocusQueue()
const onClick = () => {
queueFocus(inputRef)
setState()
}) Although then the question becomes where do we keep those queues and how to get third party components to agree on using them. So perhaps they should be built-in after all. This is why we want to understand the full breadth of the use cases. Are they all “fire and forget”, are they all “last one wins”, and how many common distinct ones there are. Maybe this could be an RFC. |
With focus specifically @trueadm said there’s even more pitfalls. That there are cases to consider about blur event handlers causing other focus, or focus causing other focus. Cascading effects. And that sometimes the browser needs to paint first. I don’t have all the background knowledge to talk about this but it’s something you would need to research if you plan to write an RFC. |
Hmm...does I have some ideas for solutions. Give me some time to implement and think about them. |
I haven't tested this fully but what do you think about this: import { useMemo, MutableRefObject, useRef, useCallback } from 'react'
import useForceUpdate from 'use-force-update'
export default function useElementRef<T extends HTMLElement | null>(): MutableRefObject<T> & {
queueFocus(): void
queueScrollIntoView(arg?: boolean | ScrollIntoViewOptions | undefined): void
} {
const nextLayoutEffect = useNextLayoutEffect()
const value = useMemo(() => {
const callback: MutableRefObject<T> & {
queueFocus(): void
queueScrollIntoView(arg?: boolean | ScrollIntoViewOptions | undefined): void
} = (element: T): void => {
callback.current = element
}
callback.current = null!
callback.queueFocus = (): void => {
nextLayoutEffect(() => {
callback.current?.focus()
})
}
callback.queueScrollIntoView = (arg?: boolean | ScrollIntoViewOptions): void => {
nextLayoutEffect(() => {
callback.current?.scrollIntoView(arg)
})
}
return callback
}, [nextLayoutEffect])
return value
}
function useNextLayoutEffect(): (callback: () => void) => void {
const callbacks = useRef<(() => void)[]>([])
const forceUpdate = useForceUpdate()
return useCallback(
(callback: () => void) => {
callbacks.current.push(callback)
forceUpdate()
},
[forceUpdate],
)
} Usage: const inputRef = useElementRef()
function openTab() {
// change state
inputRef.queueFocus()
} |
That's a good point! |
I want to explain why I am searching for alternatives to
Unfortunately, my solution doesn't solve these problems. It solves two smaller problems:
|
ESLint can help here. Same as with other patterns that aren't recommended. |
'no-restricted-syntax': [
'error',
{
selector: 'MemberExpression[property.name="focus"][object.property.name="current"]',
message: `Don't call focus() directly on an element. Use queueFocus() instead.`,
},
] |
Based on our discussion I wrote a document outlining the benefits and disadvantages of all the possible solutions you and I came up with. Based on that document I implemented one of the solutions in our project (a variation of the things we have discussed). If the experiment is stable for some period of time I will post the solution here. |
@astoilkov I wanted to follow up on this discussion, how did your experiment turn out? Would you mind to share that document here so we can all learn from your experience? |
@callmetwan Yes. We have been testing the solution in our production app for a few months and everything looks great. I am working on an open-source repo and will share it when it's done(2-3 weeks from now). |
Here is the solution we have been using in production for a few months without it causing a single bug – https://gist.github.com/astoilkov/5d7b493634586a48d2f1c336349af9d8. I tried to open-source a repo but there are a lot of questions I will need to answer first. P.S. The gist doesn't provide implementations for |
How do you prevent batching for Redux props updates? |
@astoilkov It looks like you'll need to consider "remounting" behavior before upgrading to React 18 (at least when using strict mode). Related blog section. In particular, I suppose you'd need to set |
Yep. You are right. I will need to make some changes in order to support React 18. I've started working on a general-purpose solution as well. However, I haven't tested it in React 18 which is something that I'm planning when we decide to upgrade to React 18. |
React version: 16.9.5
Steps To Reproduce
ReactDOM.unstable_batchedUpdates = callback => callback()
Reasoning
I recognize that this may not be classified as bug because it isn't a documented feature but I have tried to search for a different solution but to no avail. Fixing this behavior can open a new way of using React. I tried writing on Stack Overflow and writing to @gaearon.
I have a number of arguments which support the disabling of batched updates in event handlers and in effects initialization. If anybody is willing to read a document and consider this scenario I am willing to write an RFC.
The text was updated successfully, but these errors were encountered: