useref vs. usestate and unnecessary renders

2019-07-26

 | 

~3 min read

 | 

505 words

Today, I found a wonderful side-effect of useRef: it doesn’t re-render components when it changes.

This was ideal for my situation because I needed to keep a property around so that I could access it in an API call. Naively, I reached for the tool I know best for this in functional React components, useState. What I didn’t realize at the time was the cost I was paying in doing so.

Because I was using state to store this information, every time it changed, the entire component and all of the children components re-rendered as well.

Using the “Highlight Updates” feature in the React Chrome Dev Tools, it was pretty clear to see: useState

Here’s a simplified component showing what was going on under the hood.

function NameInput(props) {
  const { handleSubmit } = props
  const [name, setName] = useState("")
  const handleChange = (event) => {
    setName(event.target.value)
  }
  const handleSave = async () => {
    try {
      await handleSubmit(name)
    } catch (e) {
      throw new Error("Submission failed!", e)
    }
  }
  return (
    <div>
      <label hmtlFor="add-name">Add your name</label>
      <input id="add-name" onChange={handleChange} />
      <button onClick={handleSave}>Save</button>
    </div>
  )
}

Solving The Problem Through Refs

The issue is the “unnecessary” re-renders. I’m not actually showing anything different to the user based on what I’m storing in state (the input component is managing its own state), so, every time I rerendered the form was unnecessary to communicate the information to the user.

It turns out the useRef is ideal for this situation because it doesn’t subscribe to changes. Instead, per the React team: 1

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component. … Keep in mind that useRef _doesn’t_notify you when its content changes. Mutating the .current property doesn’t cause a re-render.

With that in mind, I was able to refactor my code simply by lifting out the useState and replacing the setName with a name.current.

function NameInput(props) {
  const { handleSubmit } = props
  const name = useRef("")
  const handleChange = (event) => {
    name.current = event.target.value // set the ref's .current property
  }
  const handleSave = async () => {
    try {
      await handleSubmit(name.current) // access the ref's .current property
    } catch (e) {
      throw new Error("Submission failed!", e)
    }
  }
  return (
    <div>
      <label hmtlFor="add-name">Add your name</label>
      <input id="add-name" onChange={handleChange} />
      <button onClick={handleSave}>Save</button>
    </div>
  )
}

It’s worth noting that I am mutating the value of the ref with each change. Unlike useState which is side-effect free and returns a new state object (or useReducer which makes this even more explicit).

Still, in my case, this is perfectly acceptable and the results speak for themselves. useRef

(H/t to Christian Nwamba for a useful writeup on the differences between useState and useRef and getting me started. 2 )

Resource



Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!