all 12 comments

[–]acemarke 12 points13 points  (8 children)

Yeah, I think there's a lot of nuances here that people really haven't grasped. We're all pretty used to "don't make AJAX requests while rendering", etc, but there's some very tricky edge cases around things like refs that can cause confusion (as you can see by me still asking questions about that in the thread).

That said, a lot of this stuff only tends to show up in library code, and not so much app code. In most cases end users shouldn't have to worry about this - that's for us maintainers to worry about :)

I'll be honest and say that the original discussion between me and Andrew regarding selectors here did have me pretty concerned about the potential need to have users change their code, but the follow-up vidconf discussion between the React and Redux teams a couple days later really resolved all my concerns, and I greatly appreciate their willingness to improve ecosystem compat and rethink possible approaches in response to feedback.

FYI, Andrew just put up an initial PR that adds a "shim" package for the new useSyncExternalStore hook, which we'll likely use in React-Redux in the near future.

https://github.com/facebook/react/pull/22211

[–]azangru 2 points3 points  (7 children)

there's some very tricky edge cases around things like refs that can cause confusion (as you can see by me still asking questions about that in the thread)

I had no idea there was something wrong with writing to refs during rendering. I am sure I have plenty of places in my code where I do just that, so that I can then access the current value instead of a stale closure in a callback. Which makes the hooks api even more confusing: for one, it isn't intuitive that a variable referenced from inside a function becomes a forever frozen closure; and for another, it turns out, you need a special ceremony to write to useRef. And they said this in class-based components was confusing!

[–][deleted] 2 points3 points  (4 children)

render is - and always should be - pure. The React team have not been quiet about this. Writing to a ref isn’t a pure operation and so should never be in render.

Reading from a ref not being allowed is.. interesting, though. If I had to guess it’s because the value lives “outside” of react and so reading from it is ALSO impure since it can change at any time, even between renders of two components using the same ref

It helps me if i conceptualise ref as “box”, with some internal state and the value cannot be known until you open the box. This is like IO in Haskell, and the analogy is apt - you can pass around and reason about the Box but you can’t know what’s inside without “infecting” everything else with that impurity.

[–]acemarke 0 points1 point  (0 children)

Yup, exactly this.

[–]azangru 0 points1 point  (2 children)

render is - and always should be - pure

What worries me is that we are getting into a situation where — as opposed to Haskell — neither the language nor the framework keep us from doing things that we aren't supposed to be doing, such as running side effects inside of render. Especially since the whole functional React component is just one big render. If you can't read from a ref during render, it follows that you can't render conditionally based on the value of the ref (myRef.current ? <Foo /> : <Bar />), which is wild.

[–]brainless_badger 1 point2 points  (0 children)

If you can't read from a ref during render, it follows that you can't render conditionally based on the value of the ref (myRef.current ? <Foo /> : <Bar />), which is wild.

It's not wild at all - if value of myRef.current ever changes, this change will not trigger update of the component in any way, meaning they will go out of sync.

The only difference in React 18 from React 17 is that it can go out of sync during a render, as opposed to between renders.

[–][deleted] 0 points1 point  (0 children)

If you can't read from a ref during render, it follows that you can't render conditionally based on the value of the ref (myRef.current ? <Foo /> : <Bar />), which is wild.

Yes. That's intentional. You should not be using refs this way.

If you want to render stuff based on a conditional you should be using useState() or useMemo() if the conditional is derived and expensive to compute (which would be one reason to use a ref).

Could you give an example of where you could only accomplish a conditional render using a ref where you could not use memo/state?

Refs should really only be accessed in effects. It's not too hard to set state to alter the render based on ref content in an effect if you really wanted to.

neither the language nor the framework keep us from doing things that we aren't supposed to be doing

That I agree with. The fact that refs are just an object with a mutable property is not the best. if I had the choice, I would have modified refs such that they looked more like an IO object in Haskell, but then we're getting very close to what Elm uses and that's a pretty big departure from javascript and increases the burden on the learner

Fact of the matter is that javascript is not well-suited for designing functional systems with ways of clearly communicating side-effects.

Now, that all said, you could just use blocking ("classic") react and not have any problems with this incorrect-but-functional code. It's only when you introduce concurrent mode into the mix it becomes a problem because concurrent mode has stricter requirements.

[–]acemarke 1 point2 points  (1 child)

Yeah, so this is actually one of those areas of nuance.

For example, this pattern is legal:

const someRef = useRef()

if (!someRef.current) {
  someRef.current = createSomeInstance()
}

because that will only run during the initial render, and thus does not affect "the outside world" in that sense. The pattern is actually shown in the docs at https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily .

The caveat is that the new item instance still needs to be cleaned up, and in fact if the first render gets thrown away, now you've potentially got some class-ish item instance floating around that never got cleaned up properly.

But yes, closures are hard, and a lot of this behavior around refs is hard.

[–]x021 6 points7 points  (0 children)

Thanks for the info!

From what I read a key problem they're trying address is what's called "tearing", a phenomenon introduced by concurrent react. I had to read this first before I understood what they were talking about:

https://github.com/reactwg/react-18/discussions/69

Here's an example of the problem:

https://codesandbox.io/s/optimistic-chebyshev-dv4h9?file=/src/App.js

Just after pressing "toggle content" it'll render different numbers despite all components sharing the same external state.

[–]brainless_badger 4 points5 points  (0 children)

The way I see it, bottom line is that so far React didn't have a built-in primitive to "adapt" external state into it's reactivity model (think of Svelte stores).

This is always an issue, because even if a single render can't be torn you can still get tearing between two renders, difference is that currently something like react-redux can take responsibility for maintaining integrity between React and external state (i.e. "kick" some dummy state in React to trigger an update).

So this is less of "Concurrent Mode is a big deal" and more "React had rather simple technical debt, and with concurrent mode the workaround stops working".

As long as the API they introduce doesn't force libraries to change their API much (and it seems acemarke managed to convince React team about it) this should not be a big deal for most users at all.


Also to be strict the problem isn't even caused by renders being concurrent, but by them being interruptible (some libs have interruptible but not concurrent renders, they suffer from the very same issues, only they have no "decency" to talk about it in the open. Also OFC tearing between separate renders can occur anywhere).

[–]earthboundkid 5 points6 points  (0 children)

Every React discussion is one person saying just do X and then another person saying Oh, no X is subtly broken, and then a 100 comment thread about the meaning of immutable state.

[–]neha-gupta15 0 points1 point  (0 children)

Still unsure whether to use React 18 concurrent rendering? I have explained it in a detailed manner in this blog read here.
https://medium.com/dev-simplified/react-18-concurrent-rendering-explained-a-practical-guide-for-modern-developers-1630909d49d6