-
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
In StrictMode, the useState() initializer function is called twice, but one of the results is discarded #20090
Comments
We're planning to make this configurable in DevTools (#19710). I agree this is pretty surprising, and unfortunately it's surprising the other way around too. For common application debugging we've found that double-logging (from double renders) is more annoying than silencing the extra render passes. But I certainly understand that the first time you discover this behavior, it's perplexing. As a temporary workaround you can just do
I don't quite understand why you find this surprising. This is (and has always been) the behavior in StrictMode, too. (E.g. here's ReactDOM 16 with StrictMode running the state initializer twice.) State initializer is part of render, and should be pure. Running it twice (and ignoring the result of one of the runs) should be completely safe, since the state initializer shouldn't mutate anything or add any listeners. Why is running it twice a problem for you? It feels like the rest of the description assumes deep knowledge of the code so it's hard to comment on, but I can only speak to the expected behavior. Maybe you can reduce the code to a specific pattern that's breaking for you? |
Here is a version that logs everything (by doing the |
The thing that immediately jumps out to me is the I think what you'd usually want to do is to split apart object creation and any side effects. E.g. |
orbitcontrols is not mine, it is a threejs primitive, i forked it for more clarity. everything has side-effects and that is something i expect react to handle simply because we don't get to decide if our dependencies are clean or not. if we pull something from npm, remark, threejs, or whatever, and we need to create a local instance (like the react docs show), it must work, or else you simply wouldn't be able to have local state in the real world. r3f for instance could close ship because all of threejs works like that. i don't really care if it creates it twice, but it seems like it then mixes up the two that it has created. or at least we would need a way to at least dispose of the one that CM has created. this bug was introduced in react-reconciler 0.26.0. the one before is not affected in concurrent mode. |
If it has side effects, it needs to be done during the commit phase (eg useLayoutEffect). |
the problem is that in almost every case you create a local instance of something, you need it in the view. here you do it with Remark: https://reactjs.org Whatever has changed since the previous reconciler, this pushes us into more boilerplate and indirection. const controls = useRef()
useLayoutEffect(() => {
controls.current = new Controls()
}, [])
return <foo controls={controls.current} /> // doesn't exist, it would need an additional forceRefresh or worse const [controls, set] = useState()
useLayoutEffect(() => {
set(new Controls())
}, [])
return controls
? <foo controls={controls.current} />
: null |
I’m not really sure what “mixes up” means here. Reducing this example to something with no third-party code would help a lot because I don’t know much about what this third-party code is supposed to be doing and I’m not familiar with the terminology. If this is truly a React bug you should be able to reproduce it with a tiny example with ReactDOM.
I would really appreciate if we could focus on the concrete issue and not be alarmist here. Doing a side effect during a constructor (like subscription) has always been a problem — for example, it’s completely broken if you use server rendering. I understand that R3F doesn’t, but this is not a new constraint overall for idiomatic React code. I’m sure we can figure out a solution here but a smaller self-contained example would make this much easier. |
before i attempt that, could you explain what it does? i have a local state variable const [controls] = useState(() => new OrbitControlsImpl(camera, gl.domElement)) why does concurrent mode create two controls? if i understood this a bit better i could try to make a reduced variant for react-dom. the debug log thing made it seem like magic, but now at least i know that there are to instances running. one has the zoom flag, the other --- for the strangest reason --- didn't get to the view part where enableZoom=false is set, and this i think is the bug in react. |
I want to clarify that the problem is not in creating instances. The problem is if the constructor of that instance has some side effects (such as setting up a DOM listener). This problem is not new. It is in fact the reason |
i understand this part. but can you explain why one control has enableZoom=true and the other doesn't? that is the bug. the view says: <object {...props} /> and props contain enableZoom=false. even if CM creates two versions of orbitcontrols, one does not get to the view and the props, it is interrupted for some reason before that. that is what i need to know before i can try a react.dom version. |
This is not specific to Concurrent Mode. Again, in Strict Mode we always run the render two times in development. The second render will not "reuse" the state initializer — both of them will create one. It's just that we'll throw away the result of one of these renders in DEV. Why do we do this? Precisely to find side effects during initialization, since that breaks other features. |
Concurrent Mode can't literally "interrupt" anything in your code. We're still living according to the rules of JS. If |
I'd like to better understand the desirable behavior. These statements are unclear to me:
What do you mean by "stuck"? What is the "view section"?
Where is that being "set"? It would help if you could write step by step what you expected, and what is the actual behavior. |
Here's something I don't understand in particular. I see you're rendering The code that's logging in |
Here is a version where I added a getter/setter for https://codesandbox.io/s/r3f-contact-shadow-forked-u9g0b?file=/src/OrbitControls.js:146-338 Object.defineProperty(this, 'enableZoom', {
get() {
return this._enableZoom
},
set(val) {
this._enableZoom = val
warn('setting enableZoom to', val)
}
}) Now let's see what happens. First, the state is initialized and the constructor runs. It has side effects (not allowed), but let's forget about that for a moment. It sets Next, the constructor runs again. The state initializer runs again. Again, side effects (not allowed), but that's not the point. This one also sets So by this point both of them have Finally, I'm seeing this assignment! OK, so this is why one of them gets I'd say it's shady that Anyway, what happens next makes sense now. We've got two listeners (because of a subscription during render, which is not allowed and has always been a problem for server rendering, so is not idiomatic), both of which are active. One of them has Hence the issue. Does this make sense? We can brainstorm some alternative solutions but I'd like to verify if I understood the issue correctly. |
This piece of code in R3F looks like This is not a supported pattern — the method name literally starts with "create". I hope you can understand that while I want to help figure out a solution here, it is tough when I can't assume that the renderer code respects the contract. Of course, any changes to React can accidentally break it if it does something as clever as reuse passed instances instead of creating them. |
By the way. I want to briefly answer the concrete question, i.e. why is it even useful to check for this. One reason is related to server rendering. By double-rendering the initialization sequence, we expose memory leaks and bugs that would have otherwise occurred only on the server, on the client. I understand that you don't personally need server rendering since R3F is client-only. But for many ReactDOM users, at some point the ability to render their app to HTML becomes an extremely important performance optimization, especially for low-end devices. We don't want to put people in a situation where they turn on server rendering, and half of their components are completely broken because they do subscriptions and other side effects during the initial render. So this is one part of the motivation. The other part is that we need to assume render can be "thrown away" for many future features. For example, consider Suspense. My impression was that you've found it fairly useful in R3F. However, Suspense relies on being able to "throw away" a rendered tree midway if some part of it is not ready, and then trying to render it again later. For this to work, you need to be able to safely initialize state, throw it away, and then initialize it again. You can't just reuse the old result because the props might have changed by the time you do a second attempt. This is why the render phase needs to be pure (including state initializers). This is not specific to Suspense. For example, if React adds built-in animation and gesture low-levell primitives, it would also be important to be able to render "start" and "future" states virtually to generate keyframes between them. This can't work if the process of rendering itself performs side effects. So this is what we tease out by doing double render. |
yes, primitive is an integral part, allows you to project an already existing thing into the react world. the only difference is that it is created beforehand, there's no real conflict here. for this particular platform imperatively created objects are a given come what will. What is happening is this:
Therefore i'd like to ask for a more reasonable means to handle local stateBlocking mode:function OrbitControls(props) {
const { camera, gl } = useThree()
const [controls] = useState(() => new OrbitControlsImpl(camera, gl.domElement))
useFrame(() => controls.update())
return <primitive object={controls} {...props} />
} Concurrent mode:function OrbitControls(props) {
const { camera, gl } = useThree()
const [controls, set] = useState()
useLayoutEffect(() => {
const temp = new OrbitControlsImpl(camera, gl.domElement)
set(temp)
return () => temp.dispose()
}, [])
useFrame(() => {
if (controls) controls.update()
})
return controls ? <primitive object={controls} {...props} /> : null
} This is cumbersome and it will hit every single time you're dealing with side-effects in local state, which may or may not be evident since you have no control over 3rd party controls. In the official react docs you use A suggestion: useEffectStateCould there be a hook that abstracts this? Something that will become the official means of dealing with local state - if it has side effects or not. With a clean-up phase, but immediate return. const controls = useEffectState(
() => new Foo(),
previous => /* cleanup */,
[conditions]
)
console.log(controls) // it's immediately available |
I think there is some misconception that I want to get to the bottom of.
React doesn't "break in the midst of render". This is technically impossible. React can't make your code stop in the middle. Instead, we need to be clear that JSX does not produce immediate function calls. When you see render() {
return <MyComponent />
} It doesn't mean that So what happens here isn't that React magically stops your code from executing. It just means that it calls your component twice: // somewhere in React
YourComponent(); // ignore the result
let result = YourComponent(); // use the result This behavior has been on in StrictMode since 2018, so by itself it is not new. And the goal of it, as I explained earlier, is precisely to catch side effects during initialization or render — which it did, as designed. (I don't know why you didn't see the issue with old versions, but if you make a CodeSandbox with the right versions that don't show the problem even in StrictMode, I can dig into this.) Does this explanation make sense? We can talk more about specific solutions or whether the "reusing" |
sure, i can, here's a version on the previous reconciler and react 16.4 in concurrent mode: https://codesandbox.io/s/r3f-contact-shadow-forked-z93o3 i get why this is no solution, even if that old reconciler worked. in a way im glad this comes up now. this must have happened a thousand times silently without anybody noticing. i think a new hook or anything that spares us the useLayoutEffect soup will go a long way in making this easier. when you see the useEffectState above, could that be a way? |
I don't think it could be a way. But it's hard to explain why because I'm not sure that we have alignment even on simpler things (e.g. whether or not your render function got interrupted). I was wondering whether my previous comment makes sense and whether you see what I meant by that. |
i think we're on the same page. though i still don't fully understand why one instance of the orbitcontrols does not get that flag then. if react does not interrupt - how can it end up without commitUpdate/applyProps. but either way, the internals are messing with my head, i better leave that to the react team. all that matters to me is that we have a sane way of dealing with local side-effect state, which currently seems only possible by jumping through hoops, rendering twice, checking against undefined. |
From what I'm seeing here, R3F is compiled in production mode (despite the development environment). For this reason, it doesn't have very important warnings (e.g. about duplicate keys), and the StrictMode double-rendering is disabled. I'm guessing you fixed this in the latest release, which is why the behavior is showing up now. |
Ah yes, that was our discussion on twitter. It is inlining react-reconciler in prod mode there. |
I think you're overestimating the complexity of internals in this particular case. If you're creating a third-party renderer, it is helpful to have the right high-level mental model. Let me try to walk through it and see if that makes sense?
Say this is your component: function YourComponent() {
return <div />
} Imagine this is React: YourComponent(); // ignore the result
let result = YourComponent();
let domNode = rendererConfig.createInstance(result.type, result.props); I think this shows why even if When you write |
ok, i get it now. and applyProps is within createInstance. that's why it ends up like that. how would i solve it, given that i must allow something like |
OK, glad we're on the same page. Now regarding solving this... I understand that you already rely on this behavior and it's frustrating. But I do want to point out that if we were talking about anything similar related to ReactDOM, we would have the same problem with server rendering. It is simply not supported to have side effects during render because it limits the kinds of features we can build into React. It breaks server rendering, error handling, and other cases.
We don't — you can never really know that in JavaScript. It is a matter of convention. For years, when people run into this with an imperative library, they would either work around it by initializing in Arguably, side effects in constructor are widely considered to be a bad practice, regardless of whether you use React or not. It's worth fixing that imperative code to separate the event subscription from the rest of the initialization. Or at least offer an opt out. Sure, we can talk more about possible workarounds, but I want to be clear that the idiomatic solution is to solve this at the Three level. |
I’m finding it difficult to continue this conversation because most of the content of my replies seems to be ignored by your replies, and we’re beginning to walk in circles. But I will try to retrace my train of thought one more time. I don’t intend to keep repeating the same things over and over though, especially not on a weekend. I trust that you mean well though.
Indeed, your particular use case is not about SSR. But the context in which React users tend to encounter this problem is usually related to SSR. I hope you can agree that React server rendering is used much wider than R3F. Therefore, it might make sense that even if we wanted to solve this somehow for R3F, a solution that would not also solve the same problem for SSR would not be sufficient. We can't raise a magic wand and make code like
If something has side effects during initialization, the idiomatic solution is to separate them (e.g. move them into a separate method). Even if you did not "write" some code, often you are able to send a pull request or refactor the imperative code. Of course, I understand you don't want to rewrite Three.js, but I also don't get the feeling that this pattern is pervasive there. From looking at a bunch of internals, so far I've only found it in the You're right not every problem can be fixed at the source. The canonical workaround then, is to move this work to the commit phase (
You are continuing the pattern of saying things "just" "need" to be done in some particular way. This is not conductive to a good technical discussion and I am asking you to please not do that. I do want to point out (again) that "objects" can't "have side effects". This doesn't have a technical meaning. You are referring to constructors with side effects. I explained how those are handled right above.
I am not sure what you mean by this. The
No, this is not what I was saying. I feel like we're going to have to stop this conversation if you continue to insist that I said something that I didn't. I hope that this comment (and the two approaches in the beginning of it) help clarify my point.
Let me once again point out that this is how React has worked for seven years for server rendering. I understand that you might not have used that, but all third-party component authors for ReactDOM eventually learn about this problem, and solve it with one of the two ways I have described in the beginning of this problem. Given the size of the React ecosystem, I think it's fair to say that this problem is solveable and has not led to any projects "dying".
React does not "invalidate" anything. If the constructor did not have a side effect, it would have been completely safe for React to recreate it. Let's focus on the root of the problem — the double rendering is only a mechanism to surface it. And as I have said repeatedly, in this comment and others, the way to solve it is either in the imperative code or by moving this code to the commit phase.
This is not a "new policy". React has existed for seven years, and for those seven years people have put side effects into Let's take your component here: Now let's run it in Node: It errors, because we're unable to calculate the initial state ( As for double-invoking of constructors, it has been on for ReactDOM users for two years in In fact, the very word "policy" is misleading. What you're describing ("just make this work for SSR") is plainly not possible because the DOM does not exist on the server. It's not our whim, but a consequence of React being able to render things on the server (which is a good feature!) Coincidentally, the libraries that don't care about server rendering (e.g. jQuery) are the ones that don't enforce this separation of side effects.
I believe this comment to be hyperbolic based on the evidence I have provided above. Let me recap:
We can keep brainstorming but like I already wrote, I'm going to need a concrete example of what doesn't work with my approach, so that I can iterate on it. However, to continue this discussion, I have to ask you to avoid hyperbolic statements like "React must do X or else", "this will kill my project", etc. I'm fairly certain we can find a solution to this that both you and I would be happy with, but this isn't conductive to finding it. Instead, let's focus on the actual problem. Thank you. |
@Aprillion as i said, this just makes a mess, because something that is available immediately is now being spread over 2 render phases and everything else has to adapt. normally a local variable is being used by other variables, hooks and the view. this now has to go in states and every step in the way you check against undefined. and that is just not what you can expect people to know. they'll run into this for ever. there has to be some sort of compromise. if something doesn't work in SSR in a non SSR app, it should cause such a problem, especially if it's not under your own control. |
@gaearon i do understand the purpose of it. these apps do not run under SSR, threejs itself can't. all of three works that way, every object they have has handlers or effects in the constructor. that is why im getting anxiety. if there is nothing for these cases, no possibility to hold a fixed state obj in a non ssr app, that endangers the project. im seeing these elaborated useLayoutEffect things spanning multiple render phases and this is no solution. i was hoping for a compromise, something that helps our projects and the users that create imperative objects all day, because that's the way it works. sometimes escape hatches like dangerouslySetInnerHTML are needed. imagine you'd take that out because it's dirty. it would collapse so many usecases. import React, { ____useEffectfulState } from 'react'
function Foo() {
const foo = ____useEffectfulState(() => new Bar(), [conditions]) |
Can you point me to concrete examples, please?
Don't forget that you can wrap Hooks into components and then the user doesn't need to write them over and over again.
The problem here is not a lack of "effectful state". The problem is a side effect in constructor. This breaks server rendering and this breaks Concurrent Mode. (Which is why Strict Mode always surfaced this problem.) If this is a blocker for R3F, it can keep using Legacy Mode. But I can't "change the math" and magically make this pattern work. It's the constraint itself (no side effects in constructor) that enables these two features (server rendering and CM). Without this constraint, neither of these two features can work. This is the point of React — the reason it's able to add features like this is because it adds some constraints, compared to something like jQuery or vanilla Three.js. It's okay if you don't use all React features. To stop going around in circles, please let's get back to this:
This is the most actionable thing you can do to help me understand the issue better. This will be much more helpful than discussing a proposal to change React. |
i mean, it's a react element now, you can do anything to it. animate it for instance. you can't animate a hook. function Foo({ open })
const [foo] = useState(() => new Foo())
const props = useSpring({ open: open ? 0 : -100 })
<a.primitive object={foo} {...props} /> // this will animate "foo.open" 60fps outside of react but this is besides the point. it could be
everything in there has side-effects: https://github.com/mrdoob/three.js/tree/dev/examples/jsm/controls many other places in threejs as well. but this is something you'll find elsewhere, too. any generic vanilla js thing. for instance i just googled "vanilla js gesture" and they all work like that, for instance: https://github.com/zingchart/zingtouch // vanilla
let zt = new ZingTouch.Region(document.body) // adds events
// react, yields two conflicting gesture handlers
function Zing({ flag }) {
const [zing] = useState(() => new ZingTouch.Region(document.body))
useMemo(() =>zing.doSomething(flag), [zing, flag])
return <div>{zing.whatever}</div> so react says, not supported. but with useLayoutEffect it just turns into a soup. function Zing({ flag }) {
const [zing, set] = useState()
useLayoutEffect(() => {
set(new ZingTouch.Region(document.body))
}, [])
useMemo(() => {
if (zing) {
zing.doSomething(flag)
}
}, [zing, flag])
return zing
? <div>{zing.whatever}</div>
: null |
You can, if it's using the React render cycle. If you're not for performance reasons (as is the case with react-spring?), then I imagine that Hook could be hooked up to whatever mechanism that react-spring is already using. What is the low-level primitive through which
Yes, but you previously said "I can't ask threejs to write 10.000 objects from scratch". You are now linking to a directory that contains literally eight modules, out of which three have already implemented
I'm curious if you see this pattern in any other places in Three.js, to back up the claim of needing to "write 10.000 objects from scratch". It seems like it's important to assess the extent of this so we can pick a good solution.
I believe I've already replied to this argument and I have nothing to add there. |
I would like to propose one last time that you provide a CodeSandbox that demonstrates something that works with your current approach but doesn't work with mine. As I understand, this |
i have explained that this has nothing to do with it. i can make an example and this would be completely off the point. the point is, i can't force three. they won't change it. 10 or 10.000 doesn't matter. it would affect their entire userbase of vanilla users that may not know what SSR frameworks do. i mean let's ask @mrdoob here ← would you please rewrite all objects with constructor side-effects (all controls, some shader based primitives, some lights, rectarea for instance, anything that adds events or writes into the global namespaces)? i can't also force "zing" to stop using events in the constructor. all im looking for is a solution for non SSR apps that doesn't force me to route something through render phases just to create an object. |
I'm not sure what you mean by this. A concrete runnable example that doesn't work with my proposed solution but works with your initial one is the most direct way for us to get to solving this issue. Assuming you're interested in solving the problem, this is the most actionable thing you can help me with. I can try to make one myself but it will take me a lot more time, since I'm not sure what property I can animate in
You are vastly overestimating my knowledge. I have a faint idea of how it works, but I don't know where that happens in the code and how exactly it is hooked up. So a concrete example would help me figure this out a lot quicker.
We can definitely try to send a PR to them that adds two methods to five files. This is not breaking semver in any way. (The default behavior can stay "connected" — we just need the ability to disconnect and later connect again.) This is not the only solution for sure, but it's worth at least assessing the amount of work here. I can send this PR myself if you don't want to get involved in this.
This is not what I'm asking for. If the side effects can be undone (e.g. some of these controls already offer a I think I've said it many times but I'll repeat: if we can't modify the underlying code, the workaround is to do this in the commit phase. I have provided one solution for commit phase that works. If you have constraints that make it a non-starter (e.g. the animation case), I need to see the problem for myself in a sandbox. There isn't much I can help with until one is provided. Thank you. |
I've had a brief chat with the Three.js maintainer who indicated that they also think the problem is only relevant to the Controls, and that they're open to changing it to a If we make this change in Three.js, it means that you'd still need to do function Controls() {
const { camera, gl } = useThree()
// OK if we remove side effects from the constructor
const [controls, setControls] = useState(() => new ObjectControls(camera, gl.domElement))
useLayoutEffect(() => {
controls.attach()
return () => controls.detach()
}, [controls])
// ...
return <primitive object={controls} />
} But you would be able to keep using You can't really avoid this layout effect because you need to clean up somehow when the component is unmounted anyway. I respect your position that code like this is a "soup" but this is how ReactDOM users bind to // Application code
function Controls() {
const controls = useControls((camera, node) => new ObjectControls(camera, node))
return <primitive object={controls} />
}
// ...
// Inside R3F:
// ...
function useControls(create) {
const { camera, gl } = useThree()
const [controls, setControls] = useState(create(camera, gl.domElement)
useLayoutEffect(() => {
controls.attach()
return () => controls.detach()
}, [controls])
return controls;
} If this is still too verbose for your taste, we could conceivably even build this whole logic into the @drcmda If we solve this on the Three.js level with an approach like this, would it be satisfactory to you? |
Just to close the loop on this. We've talked with @drcmda, and he applied my earlier proposed fix:
The fix is in pmndrs/drei@47fd6a4. I'm going to close this, but ideally as a follow-up it would be good to resolve this on the Three.js level so that all of the initialization can be moved to |
Three.js proposal: mrdoob/three.js#20575 |
it's confusing. Is there an option to disable it? |
It's hard to say what is confusing and what you want to disable. Strict Mode is opt-in, so yes, you can disable it by removing it from your app. The behavior itself is intentional. |
React version: 17.0.1
React reconciler: 0.26.0
Steps To Reproduce
Link to code example: https://codesandbox.io/s/r3f-contact-shadow-forked-iggyv?file=/src/index.js:308-745
old demo
https://codesandbox.io/s/r3f-contact-shadow-forked-e44m3?file=/src/index.js
This demo creates a local object which is supposed to live within the components lifecycle.
For some reason concurrent mode creates two versions of that object, but one is stuck in the view section.
These controls aren't allowed to zoom, yet, when you give you mousewheel - it zooms. The control clearly receives the flag.
This does not happen in blocking mode and previous reconcilers (for instance react 16.4.x and reconcilers pre 0.26
Debugging in this is almost impossible as React swallows console.logs. Some users have found out that it indeed creates two branches of local state: https://twitter.com/belinburgh/status/1319990608010874883
The state object (orbit-controls) has side-effects, it creates events, but that is and should be of no consequence.
The text was updated successfully, but these errors were encountered: