DEV Community

loading...
Cover image for How to useRef to Fix React Performance Issues

How to useRef to Fix React Performance Issues

notsidney profile image sidney alcantara Originally published at Medium ・6 min read

And how we stopped our React Context re-rendering everything

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:

useRef() is useful for more than the ref attribute. It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.

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

The Problem

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.

Screen recording of a user selecting a cell and opening the side drawer

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:

  • We need to set this state from the table component. We’re using 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.

  • But the side drawer and table components are siblings, so they can’t directly access each other’s state.

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:

  1. 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.

  2. We were already sharing a lot of “global” data via a context located close to the root of the component tree, and we felt it made sense to add this state to that central data store.

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.

Screen recording of delay in side drawer 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’s value prop changes.

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.

The Aha Moment

We first explored a few different solutions (from Dan Abramov’s post on the issue) before settling on useRef:

  1. Split the context, i.e. create a new SideDrawerContext.
    The table would still need to consume the new context, which still updates when the side drawer opens, causing the table to re-render unnecessarily.

  2. Wrap the table component in React.memo or useMemo.
    The table would still need to call useContext to access the side drawer’s state and neither API prevents it from causing re-renders.

  3. Memoize the react-data-grid component used to render the table.
    This would have introduced more verbosity to our code. We also found it prevented necessary re-renders, requiring us to spend more time fixing or restructuring our code entirely, solely to implement the side drawer.

While reading through the Hook APIs and useMemo a few more times, I finally came across that point about useRef:

useRef() is useful for more than the ref attribute. It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.

And more importantly:

useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render.

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 Solution

  1. Keep the open and cell states in the side drawer.

  2. Create a ref to those states and store it in the context.

  3. Call the set state functions (inside the side drawer) using the ref from the table when the user clicks on a cell.

Diagram representing the preceding list

The code below is an abbreviated version of the code used on Firetable and includes the TypeScript types for the ref:

This solution proved to be the best since:

  1. The current cell and open states are stored inside the side drawer component itself, the most logical place to put it.

  2. The table component has access to its sibling’s state when it needs it.

  3. When either the current cell or open states are updated, it only triggers a re-render for the side drawer component and not any other component throughout the app.

You can see how this is used in Firetable here and here.

When to useRef

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 below and follow me on Twitter @nots_dney as I write more about what we’re building at Antler Engineering.

GitHub logo AntlerVC / firetable

Excel/Google Sheets like UI for Firebase/Firestore. No more admin portals!

Firetable: Combine the power of Firestore with the simplicity of spreadsheets

Features

  • Spreadsheet interface for viewing Firestore collections, documents, and subcollections.

    • Add, edit, and delete rows
    • Sort and filter by row values
    • Resize and rename columns
  • 27 different column types Read more

    • Basic types: Short Text, Long Text, Email, Phone, URL…
    • Custom UI pickers: Date, Checkbox, Single Select, Multi Select…
    • Uploaders: Image, File
    • Rich Editors: JSON, Code, Rich Text (HTML)
  • Powerful access controls with custom user roles Read more

  • Supercharge your database with your own scripts.

    • Action field: trigger any Cloud Function
    • Derivative field: populate cell with value derived from the rest of the row’s values
    • Aggregate field: populate cell with value aggregated from the row’s sub-table
  • Integrations with external services.

    • Connect Table uses Algolia to get a snapshot of another table’s row values
    • Connect Service uses any HTTP endpoint to get a cell value

Firetable makes it easy to use key Firebase products

Cloud Firestore Firebase Authentication Firebase Functions Firebase Hosting Firebase Storage

Live demo →


Firetable demo GIF


Getting started

To set…





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.

Discussion (9)

pic
Editor guide
Collapse
dispix profile image
Octave Raimbault

Shouldn't you be able to achieve the same design with useState instead if you use 2 providers (1 for the data, which changes often, and 1 for the functions that shouldn't update)?

This seems a better practice than using ref which brings us back to the issue of the classes era with mutable values. Whereas splitting in 2 contexts leverage closures to achieve the same thing.

Collapse
danielo515 profile image
Daniel Rodríguez Rivero

Where is useFiretableContext coming from? You don't import it anywhere neither define it. I understand the code is simplified, but using functions that are coming from "nowehere" does not help understanding.
Thanks for sharing your solution in any case.

Collapse
ricardo profile image
Ricardo Luz

That's a great article! I didn't know that change the current reference doesn't cause a content re-render, thanks for sharing this

Collapse
zarabotaet profile image
Dima

You only need normal state manager.
Take a look at effector, it provides atomic lightweight stores.

Collapse
calag4n profile image
calag4n

Great post thanks for sharing 😊.

Collapse
dgiulian profile image
Diego Giuliani

Great article, I never thought of combining context and ref that way. It's a neat solution, but one has to be careful when to use it.

Collapse
casvaniersel profile image
Cas van Iersel

Did you look into PureComponents instead of function components with Hooks? Could boost your performance in the rest of your app too.

Collapse
sshaw profile image
Skye Shaw

Very nice.

I think you should include some of the table cell's code for completeness.

Collapse
salvoravida profile image
Salvatore Ravidà

Why don't use redux, and simply store selected cell ti redux, everything will work as it should. You are just passing a callback setstate to parent components.