

Refs are a seldom-used feature in React. If youโve read the official React guide, theyโre introduced as an โescape hatchโ out of the typical React data flow, with a warning to use them sparingly, and theyโre primarily billed as the correct way to access a componentโs underlying DOM element.
But alongside the concept of Hooks, the React team introduced the useRef Hook, which extends this functionality:
is useful for more than theuseRef()
attribute. Itโs handy for keeping any mutable value around similar to how youโd use instance fields in classes.ref
While I overlooked this point when the new Hook APIs launched, it proved to be surprisingly useful.
๐ Click here to skip to the solution and code snippets
Iโm a software engineer working on Firetable, an open-source React app that combines a spreadsheet UI with the full power of Firestore and Firebase. One of its key features is the side drawer, a form-like UI to edit a single row, that slides over the main table.
When the user clicks on a cell in the table, the side drawer can be opened to edit that cellโs corresponding row. In other words, what we render in the side drawer is dependent on the currently selected rowโโโthis should be stored in state.
The most logical place to put this state is within the side drawer component itself because when the user selects a different cell, it should only affect the side drawer. However:
react-data-grid
to render the table itself, and it accepts a callback prop thatโs called whenever the user selects a cell. Currently, itโs the only way to respond to that event.Reactโs recommendation is to lift this state to the componentsโ closest common ancestor, in this case,
TablePage
. But we decided against moving the state here because:TablePage
didnโt contain any state and was primarily a container for the table and side drawer components, neither of which received any props. We preferred to keep it this way.Side note: even if we put the state in
TablePage
, we would have run into the same problem below anyway.The problem was whenever the user selected a cell or opened the side drawer, the update to this global context would cause the entire app to re-render. This included the main table component, which could have dozens of cells displayed at a time, each with its own editor component. This would result in a render time of around 650 ms(!), long enough to see a visible delay in the side drawerโs open animation.
Notice the delay between clicking the open button and when the side drawer animates toย open
The reason behind this is a key feature of contextโโโthe very reason why itโs better to use in React as opposed to global JavaScript variables:
All consumers that are descendants of a Provider will re-render whenever the Providerโsprop changes.value
While this Hook into Reactโs state and lifecycle has served us well so far, it seems we had now shot ourselves in the foot.
We first explored a few different solutions (from Dan Abramovโs post on the issue) before settling on
useRef
:SideDrawerContext
.React.memo
or useMemo
.useContext
to access the side drawerโs state and neither API prevents it from causing re-renders.react-data-grid
component used to render the table.While reading through the Hook APIs and
useMemo
a few more times, I finally came across that point about useRef
:is useful for more than theuseRef()
attribute. Itโs handy for keeping any mutable value around similar to how youโd use instance fields in classes.ref
And more importantly:
doesnโt notify you when its content changes. Mutating theยuseRef
property doesnโt cause a re-render..current
And thatโs when it hit me:
We didnโt need to store the side drawerโs stateโโโwe only needed a reference to the function that sets thatย state.
The code below is an abbreviated version of the code used on Firetable and includes the TypeScript types for the ref:
Side note: since function components run the entire function body on re-render, whenever the
cell
or open
state updates (and causes a re-render), sideDrawerRef
always has the latest value inย .current
.This solution proved to be the best since:
You can see how this is used in Firetable here and here.
This doesnโt mean you should go ahead and use this pattern for everything you build, though. Itโs best used for when you need to access or update another componentโs state at specific times, but your component doesnโt depend or render based on that state. Reactโs core concepts of lifting state up and one-way data flow are enough to cover most app architectures anyway.
Thanks for reading! You can find out more about Firetable here and follow me on Twitter @nots_dney as I write more about what weโre buildingย at Antler Engineering.
If youโre launching a product and are hungry to build your next company,
Antler would love to hear from you. Weโre accepting applications all
across the world! Apply here.
Also published at https://medium.com/better-programming/how-to-useref-to-fix-react-performance-issues-4d92a8120c09
Create your free account to unlock your custom reading experience.