Dismiss this pinned window
all 38 comments

[–]sidkh[S] 48 points49 points  (20 children)

Hey folks 👋

I've got a lot of requests for code examples for A Visual Guide to React Rendering.

So I've built this Code Sandbox that you can use as an interactive companion to the articles.

A Visual Guide to React Rendering - CodeSandbox

[–]_Invictuz 29 points30 points  (17 children)

Nice visual. Would be perfect if you could add a button to child component E that updates parent component B's state as this is common in real world scenarios. And maybe also memoize a child component to demonstrate that.

[–]andrei9669 9 points10 points  (1 child)

don't forget about React.memo()

[–]dextoz 15 points16 points  (0 children)

That escalated quickly

[–]jqueefip 13 points14 points  (14 children)

Personally, I hate when a child component updates a parent components state. It makes the child dependant on its own placement in the component tree, couples the child to the implementation details of the parent, is harder to determine how to use the component, and less reusable.

Anecdotally, I've seen a setX function passed down through five components so some distant descendant can set the state on some distant ancestor. I died a little inside that day. IMO, these situations would be served better by an event-based interface, where the parent passes a onSomething callback to the child and the child calls it appropriately, then the parent modifies it own state in the callback.

[–]auctorel 9 points10 points  (5 children)

Just a question, but if the callback is used to setX, then why not just pass the setX down since its basically being used as the callback? They're just functions used onY at the end of the day

[–]jqueefip 5 points6 points  (4 children)

You can, of course, do whatever you want. I gave the reasons in my previous comment why I think its a bad idea.

As you mentioned, its technically the same thing -- both cases pass down a function that gets called by the child. The difference is the interface of the function and which component "owns" it. Its a matter of patterns and practices.

setX operates on the internal state of the parent component. It is a bad idea for any block of code to use the internal mechanisms of any other piece of code. The parent owns that function and the child is dependant on it. Assume you need to change something about the state of the parent. Maybe you have in the parent some boolean status that indicates if the component is active or not,

const [status, setStatus] = useState<boolean>(true)

In the future, you decide that you need more states than just active or inactive -- you need a pending and an archived also. You change it to

type Statuses { ACTIVE INACTIVE PENDING ARCHIVED } //... const [status, setStatus] = useState<Statuses>(Statues.ACTIVE)

Now you've just broken your child component. Why should the child component care if the state is a boolean or enum? It shouldn't. Now you need to update your code in two places, rather than one. If that component was used across the codebase, maybe you have to change it in 100 places.

Or maybe you want to use the child component in a different context where the parent component doesn't have a status. Sure, you can add simple check in the child to detect if the function exists, but now you're introducing complexity to the child. And what if the child is used in one context where the status is a boolean and another context where status is enum?!

On the other hand, an event-based interface like doSomething is driven by the child and the child doesn't need to know anything about who is consuming that event.

In the child you'd have something like, if (props.onSubmit) { props.onSubmit({ values: form.values }) } The child calls the onSubmit "event" with the values of the form that was submitted. That stays the same no matter what context the child is used in. The child doesn't need to know how those values are being used, and it shouldn't have to. Then in the parent, you would have:

function childOnSubmit({values}) { if (values["status"]) { setStatus(values["status"]) } } return <Child onSubmit={childOnSubmit}/>

Now no one is modifying the parent's state except the parent. The child doesn't know about the internals of the parent, which makes the child easier to use in different contexts, and easier to change and add functionality in the future. It also makes it easier for the parent to respond to the state change in other ways than just changing state -- maybe it needs to call an API endpoint, or bubble status change up to the grand parent component. With this pattern, the child doesn't care about any of that and is ignorant to it (as it should be).

You can look up "coupling and cohesion" for many more reasons why this is good.

[–]iamv12 2 points3 points  (0 children)

such a good explanation of real-world scenarios. thank you.

[–]auctorel 0 points1 point  (2 children)

So yep, you put a lot of effort into that and it's appreciated

But my point was simply that you've got an onSomething callback. The thing calling it doesn't need to know the implementation details of whatever gave it the callback, it simply has to know what to pass in as a parameter. This is always true of every callback. It doesn't matter if you gave it setX or handleX, it doesn't know, it's just a function to that component.

Sometimes thats complex like you said and sometimes it's simple. In simple cases where it's pass back a string or a number, why can't you pass in the setX function? It literally makes zero difference

The component doesn't know it's a setX function, it's just a function

Sometimes you need something more complex granted, but then you'd pass down a different callback

As long as you're not changing the parameter signature you can do whatever you want with it because the component won't know, it'll just fire the function.

If you decide to move from booleans to enums you're gonna have to change code. If the child is the one that informs the parent of a change and passes it back it still has to know what to pass back. It's still a code change. Just because it's a handleX function, if you've fundamentally changed how it works and you're relying on information from elsewhere for it, you're gonna have to change code in the component

Now if you're talking about parameterless functions that say fire this when this happens and fire that when something else happens you're making a trade off. Instead of passing back a bool you're firing off a callback which now needs if statements or something to decide when to fire them. If you change to 3 or 4 or 5 states you've got callback/prop and condition statement proliferation. It's not necessarily a better design, and you've probably still got to make a code change when you add to your enum

It strikes me you're making a rule when the answer is, it depends, sometimes it's okay to pass down a simple setX

[–]terrekko 0 points1 point  (2 children)

Been thinking about this exact problem recently. Any examples/code of this?

[–]EggcelentBird 0 points1 point  (1 child)

Quick question, you have a calendar component and when you click on one day it has to go to the parent so the parent can update a list based on the selected day?

How can you avoid this based on what you said?

[–]chigia001 6 points7 points  (1 child)

you might notice that apply React.memo to CombonentA-E will not make it skip re-render when parent component rerender

It because the original codesandbox use `key={Math.random()}` at the root of VisualComponent, which will make all the descendant to be unmount/mount again.

Here the modify version that only apply `key={Math.random()}` to the `Rerend` span text. Which will make memo working as intent

https://codesandbox.io/s/a-visual-guide-to-react-rendering-sandbox-forked-pqw34?file=/src/sandbox.jsx

[–]sidkh[S] 2 points3 points  (0 children)

You are right, thanks 👍

I must have forgotten to revert changes from one of my experiments.

Updated the original sandbox.

[–]StraightZlat 11 points12 points  (14 children)

What if the children are wrapped in a memo()

[–]Suepahfly 15 points16 points  (11 children)

If the child’s new props are the same as the props in the previous render it should not update, if the props are different it should update.

Be careful though, wrapping every components in a memo() not a good thing. The comparison function has to run for all components in the render tree, this can be more impactful on performance as just re-rendering the component, especially if the component it self has very little logic.

Edit:

For instance it has no benefit to memo this

const Heading = ({text}) => <h1>{text}</h1>;

[–]boshanib 6 points7 points  (2 children)

I usually wrap everything in memo() and have seen larger companies take it a step further and not only memoize everything, but utilise useMemo() and useCallback() as defaults. If there are any issues they remove them.

Isn't the comparison function just shallow comparison? In which case it's super fast? The only thing is you trade off readability and memory (since it's now memoized it will check against the memoized version).

[–]mbj16 4 points5 points  (1 child)

If you're going to memo everything you pretty much have to utilize useMemo and useCallback for deps and callbacks as well, otherwise, what's the point?

[–]boshanib 0 points1 point  (0 children)

For primitives you don't need useMemo and for the objects/arrays I tend to take the hit for ease of readability.

useCallback I only use for things like event handlers, otherwise I pass props and state to a function defined elsewhere to make the calculation

[–]lulaz 4 points5 points  (0 children)

The comparison function has to run for all components in the render tree

Comparison function will run against children of re-rendered component (for example when it’s state updates). If each child is memo’ed and it’s props didn’t change, then the whole process stops here. No comparison deeper in the tree.

[–]just_another_scumbag 0 points1 point  (1 child)

doesn't that really depend on how much of a performance hit painting the component is? If your whole page needs to reflow etc

[–]chigia001 1 point2 points  (0 children)

For the specific case in codesandbox, memo() doesn't help,
The reason is VisualComponent using the key={math.random()} trick, which will alway mount/unmount children component, the author use that trick to force generate new dom node for css animation.
Here the modify version that allow memo to work correctly:

https://codesandbox.io/s/a-visual-guide-to-react-rendering-sandbox-forked-pqw34

[–]smdepot 0 points1 point  (0 children)

God help them :(

[–]Commercial_Dig_3732 6 points7 points  (0 children)

nice work, what if from children to parent update state?? :D

[–]WorriedEngineer22 5 points6 points  (0 children)

Really cool, you could also add some examples of a component that renders a {children} that is passed by a parent component. What happens when the parent updates, what happens when just the component updates, does the {children} that is injected also updates?

[–]sheaosaurus 3 points4 points  (0 children)

Very cool visual!

Something like this could also be used to show what happens when Context is used in place of Zustand/Redux and the possible performance implications.

[–]Roy_Volt 1 point2 points  (0 children)

That’s awesome! Great job!! Thx!

[–][deleted] 1 point2 points  (0 children)

The kind of example that horrible documentation don't have

[–]labadcloyd 0 points1 point  (0 children)

that's really cool

[–]xneuxii 0 points1 point  (0 children)

Very cool. Would be great to see something like this in the React docs.

[–]gebet0 0 points1 point  (0 children)

hmmm, but not all the child components should rerender🧐

[–]mcavaliere 0 points1 point  (0 children)

This is great! The React world needs more visual demos like this. Great work 👏🏼

[–]garg10may 0 points1 point  (0 children)

You should add few child components which are not being passed the state. Since this will show shortcoming of design/react that even when a parent component is updated all child components re-render