This article was originally posted to dev.to and has been copied here without modification.
What would the following lines of code do when React is rendering a component? Would they cause Counter
to get re-mounted or re-rendered?
If you said that the first two will re-render Counter
while the third will cause a re-mount, then you are correct! You can verify this for yourself with this codesandbox link. The "Basic" section shows all three cases mentioned above.
To provide some context on Counter
, it's a simple component that holds an internal count (with the useState
hook) for the number of times it has been pressed:
From this component, the most basic use case would simply be passing in the name as a prop as follows:
This is probably the most common and intuitive case. When React receives new props for a component, it will re-render the component. This results in any internal useState
hooks maintaining their internal data – which in our case means the count remains the same.
The next case is less intuitive:
At first glance, there appears to be two separate components that are being used in order to render counters; each counter associated with a different name. This could lead one to believe that both counters will go through a mount and unmount process when switching between them. However, that is not the case.
Since these are both the same component type, React actually sees this as identical to the first case. Under the hood, React uses a Virtual DOM reconciler based on a Fiber Architecture that determines how to update components (re-rendering, mounting, unmounting, etc). This reconciler uses the type of the component and the props in order to determine what lifecycle operations to take. In this case, both branches of the ternary use the same component type, but different props. This causes the reconciler to re-render the component and simply change the props passed in.
"Of course this case is the same! The code is functionally equivalent! Why are you telling me this?"
- someone who's too impatient to continue reading
Consider an application with tabs. You may have the same components that stay within the same locations across tabs. Since the components line up within the Virtual DOM hierarchy between tab transitions, this can unexpectedly cause the same re-rendering behavior to occur.
Alright, back to the intuitive. To tie it all together, the reason why this case re-mounts is quite simply due to the change in component types. On the left branch we have a Counter
component while on the right branch we have a p
element. As mentioned above, React's reconciler uses these component types in order to determine what operations to take. Since the types are different when you switch branches, it will unmount the component that was mounted and mount the component that was unmounted.
This unmounting process throws away any data saved within the component's state. Likewise, the mounting process causes component state to initialize with default values (e.g. the initial value passed into a useState
hook). This is what causes our count state to reset to 0
whenever switching between branches.
Well, there are a few real world cases where you may want to specifically have re-rendering or re-mounting behavior. Let's continue to use the Counter
component and build upon it.
Let's say that we have a web app that allows you to manage multiple users. Each of these users has a Counter
component and allows you to save their respective counts. You may write the user component like:
And with this User
component, you set up a tabs component that shows one user at a time.
The problem that will occur here is that the Counter
component's state won't reset between users. This means that when you switch between the tabs, the count will stay the same and you may accidentally save the wrong count for a given user. Extrapolating this out from a simple counter, your app may cause you to save sensitive data to the wrong user – which is a severe breach of security.
So, "how do I fix this?"
Well, the solution is a useEffect
hook. We want to listen for changes to props within the Counter
component in order to reset the state manually ourselves:
All that we've added here is a simple useEffect
hook that runs every time the name
prop changes for the component. This causes the internal count
to get reset and our 'sensitive' data to avoid leaking into other users.
You can confirm this for yourself by heading to the same codesandbox link as before and checking out the "Replicating Re-Mounting" section. Although it is defined the exact same way as the first case from the "Basic" section, it acts most similarly to the third case with its re-mounting.
Ok, now let's take the original Counter
component in a different route. This time, let's assume that we have a Counter
that only exists on one tab out of many. We may want to replicate the re-rendering functionality in order to save data when you switch back-and-forth between tabs. That way, as a user, you can work in multiple tabs without losing any data.
What I've described is basically caching the data outside of the component's state in order to prevent it from resetting. You can approach this with a variety of methods: from Redux, to React Context, to a simple cache object external from the component.
For our example, we'll do a simple cache just to show the basics. To start, we want to define a cache for us to use and a way for us to update that cache:
Now we want a way to update the cache when the component’s name
prop changes (so that we cache data for each user):
This useEffect
will also run during mount and likewise the cleanup function will run during unmount.
But wait! This code has a problem. When the cleanup function is created, count
is captured within a closure and it’ll save the wrong data into the cache. If we try to fix this by adding count
as a dependency for the hook, then it’ll cause the page to crash due to a circular reference.
To solve this issue, we can use the useRef
hook in order to use its mutative current
field:
Now the cleanup function for the useEffect
will always use the most up-to-date data for count
when setting the cache's value. This is the approach used within the codesandbox link from before for the "Replicating Re-Rendering" section.
This post was born from the mistakes that my colleagues and I have made in the past. I hope this has helped you understand React a little better and I welcome you to share anything you've learned from prior mistakes!
Finally, if you've noticed any issues above, please let me know.