Using the Hidden Attribute with React
Every time a React component mounts, updates or is unmounted, React executes two phases:
- A render phase
- A commit phase
There is a cause-effect relation between these two. The render phase is a pure, side-effect free phase, in which React collects updates, or attempts to set state on components.
In the future you’ll be able to bail on render phases.
The commit phase is responsible for running side-effects, and committing changes to the DOM. After React commits to the DOM, the Browser’s job is to apply CSS styles, layout calculations, paint pixels and compose your elements into layers.
That’s a lot of work!
Luckily, there is a DOM attribute called hidden
, which you can use to get React to commit changes to the DOM, while also telling the browser not to run its own work, just yet.
It is equivalent to display: none
, in fact, setting a display property on hidden elements overrides the attribute!
You may think that visibility:none
should work the same way, but this is a CSS property, which means the browser it’s actually execute its work chain.
A demonstration
Even though MDN recommends not to use hidden to hide panels, it is a great way to show how to take advantage of this attribute.
In this demo, there are three tabs:
- The Landing tab, simply displays a message.
- The Bitcoin tab, loads data from CoinDesk and shows it in a Victory Chart.
- The Pokémon tab, loads Pokémon from PokeApi and shows their sprites.
Each tab is a separate JavaScript bundle to React's lazy. Since the Bitcoin tab uses Victory Charts, the bundle is heavy and will most likely load last. The demo uses Parcel to bundle the whole application with zero configuration. The screenshots below are taken under Fast 3G and 6x slowdown on CPU.
After each bundle is loaded, it is parsed, then the actual component is mounted and its own life cycles methods are executed. On mount, the Landing tab does nothing special, however, in the Pokemon tab, fire type Pokémon are queried, and in the Bitcoin tab a request for the BTC/SEK rates during the last month is created.
All of this is done before the user has switched tabs. Under normal network and CPU conditions, it all happens as they read the Landing tab message.
In the Pokémon tab, elements with an even index order load their Pokémon sprite as CSS background-image
, while the ones with an odd index load the sprite as an img
HTML element.
To see that the Browser is not actually spending resources in rendering hidden elements. You can observe that React commits its work to the DOM, because sprites for Pokémon with odd indexes are fetched, since these are specified in the src
attribute for the img
element.
Notice that the Bitcoin data is also fetched.
In contrast, sprites for Pokémon with even indexes are not fetched, because these are used as a CSS background-image
property and the hidden attribute tells the browser to not run it, just yet.
As soon as you switch to the Pokémon tab, the even indexed images are fetched. If you are in the demo page, open the network tab in your Browser’s developer tools to see it yourself.
What if you switch tabs quickly? You’ll either see the fallback, or simply see the rendered component.
Details of Implementation
In the demo, the main App component looks like this:
It loads the Tabs component with three Tab children, the label
prop on each will be used to generate the control button that activates the Tab.
There is an advantage to passing a React component as label prop. Read more about it here.
Each Tab itself has children, which are Suspense React components. In case you have not used lazy and Suspense before, these are defined like so:
Now the implementation of the Tabs component. It controls which Tab children is shown, and it also generates the controls to switch between tabs.
To generate the controls the React Top Level API is quite convenient. For instance, React.Children.map
allows us to pass children as first argument, and a function to map over each child, as second argument.
The mapping function creates a button
HTML element that when clicked sets the child as current tab. The button
also shows the label
prop passed to Tab.
It is not absolutely necessary, but these controls are memo’d with empty dependencies, since they do not change when the current tab selection changes.
Another React top level API is React.Children.toArray
, which returns children as an array. In this case, to generate the Tabs content, map over the array, wrapping all children with a div
HTML element, where the hidden
attribute equals the strict inequality result of index and current.
It is generally a bad practice to use the index as key for React elements, but for this demonstration it is good enough. Good practice would be to use a unique id
prop, or the title
prop, if it could be guaranteed to be unique.
The Tab component simply renders the title
prop and its children.
Rendering the children, begins the suspenseful loading of each tab. When these resolve, React renders and commits each to the DOM. However, only the current selection is displayed, the rest are hidden
.
Counter Approach
Another approach is to hide each tab with a Boolean flag:
Unfortunately, this actually prevents the tabs from being loaded, rendered and committed to the DOM.
With this implementation, switching tabs triggers the fallback on Suspense, and makes users wait until the lazy loading resolves, it then renders and commits to the DOM.
After that, mount life cycle is executed and data is fetched. Finally, when the network requests resolve, the component is updated, triggering render and commit once again.
Moving away from a tab, will cause the component to be unmounted. If you navigate back, it will need to be mounted, and updated once again. Caching the network response might be helpful here.
To get around these issues, one could pass the hidden flag to the component and return null while it is hidden. This would allow life cycles to occur, but it requires coupling the tab management with your component’s logic.
In summary, the hidden
HTML attribute helps you to:
- Create a better user experience.
- Reduce the amount of API calls executed.
- Abstract away tab management logic.
- Commit work to the Browser, but save resources if its not relevant yet.
The code for this demo lives in this repository.
Happy hacking!