Wednesday, December 16, 2020

React's UseRef Deep Dive

A Complete Guide to useRef

What's useRef

useRef allows you to keep a mutable value within a component, similar to useState or instance variables on a class, without triggering re-renders.

For example, this component stores the number of clicks for a button:

function RefButton () {
  const clicks = useRef(0)

  return (
    <button onClick={() => (clicks.current += 1)}>
      Clicks: {clicks.current}
    </button>
  )
}

This is how this component looks like (I added a re-render button so you can actually test it out 😄):

Interactive Example

The example below is completely interactive, try clicking the "Clicks" button and then click on "Re-render".

As you can see, if you click the "Clicks" button it doesn't do anything. However, after click on "Re-render", it gets updated with the number of clicks we did previously.

Difference with a variable

You might wonder why not just use a simple variable as the example below:

let clicks = 0;

function OutsideVariableButton() {
  return (
    <button onClick={() => (clicks += 1)}>
      Clicks: {clicks}
    </button>
  )
}

And here's an interactive example for it:

Interactive Example

TypeResultoutside variable

The button works the same way that our previous example. However, the problem arises when you have multiple instances of the same component like the example below. Try clicking just one of the buttons and then click on re-render to see the result.

Interactive Example

TypeResultoutside variableoutside variableoutside variable

As you were able to see, the clicks are not isolated. In fact, all the examples from this article uses the same button component, so if you click the button from the first example and then click on "re-render" on the second example, the count it is gonna be incremented! What a bug 🐛.

On the other hand, useRef values are completely isolated between components:

Interactive Example

TypeResultrefrefref

Difference with useState

The main difference between useState and useRef, is that useState triggers a re-render and useRef doesn't.

In the following example I added two buttons: one that updates its count with useRef and the other one with useState. I added some labels so you can identify them easily.

Interactive Example

TypeResultstateref

You'll notice that clicking on the button with useRef doesn't trigger a re-render and thus, the view isn't updated. On the other side, when you click on the button that uses useState, it will update its clicks count immediately.

Refs as a Way to Access Underlying DOM Elements

To perform imperative actions on DOM nodes, React provides a way to get a reference to them via refs. All you have to do is to assign a ref property to a node with a ref object like this:

function CustomInput() {
  const inputRef = useRef()

  return <input ref={inputRef} />
}

The way to get a DOM reference using refs works (informally 😅) as follows:

Today

React

Hey, what's up?

12:00

Could you give me a reference to this dom node?

12:00

React

Sure, I assigned it to the 'current' property of your ref.

12:00

On the first render, inputRef's value will be { current: null } and in the following renders it will have its current property assigned to the specified DOM node:

However, if you only reference inputRef inside useEffect then it'll always reference the DOM node so you don't need to worry about it being undefined.

Let's update our example to get an idea of how this works:

function AttachingToDomExample() {
  const inputRef = useRef()

  console.log("Render inputRef value:", inputRef)

  useEffect(() => console.log("useEffect inputRef value:", inputRef))

  return <input ref={inputRef} />
}

Here's the console output when rendering this component:

Render Location Value
1 Render { current: undefined }
useEffect { current: <input /> }
2 Render { current: <input /> }
useEffect { current: <input /> }
3 Render { current: <input /> }
useEffect { current: <input /> }

As you can see, if you access the inputRef inside useEffect then you don't need to worry about it being undefined because React will assign it automatically for you.

Real World Use Cases for Refs

Let's start with a simple real-world application for refs: usePrevious. This hook stores the previous value for a given state variable. It is even referenced on React's docs as a way to "get the previous props or state". Let's see it in action first:

function UsePreviousExample() {
  const [clicks, setClicks] = useState(0)
  
  const previousClicks = usePrevious(clicks)

  return (
    <div>
      <button onClick={() => setClicks(clicks + 1)}>
        Clicks: {clicks} - Before: {previousClicks}
      </button>
    </div>
  )
}

Here's the output so you can play with it:

Interactive Example

You can notice that the previousClicks variable stores the value for the previous render for a given variable. Here's its implementation:

function usePrevious(value) {
  const ref = useRef()
  useEffect(() => {
    ref.current = value
  })
  return ref.current
}

Let's analyze how it works.

Let's simulate what happens on the first render. We can remove the call to useEffect since it doesn't affect the return value on the first render:


function usePrevious(value) {
  const ref = useRef()
  return ref.current
}

On the first render it is called with a value of 0:


const previousClicks = usePrevious(0)

In this case, usePrevious will return undefined:


function usePrevious(value) {
  const ref = useRef()
  return ref.current 
}

After increase the value for count, here's how the usePrevious call will look:


const previousClicks = usePrevious(1)

Since usePrevious is called again, its effect needs to run:

useEffect(() => {
  ref.current = 0
})

After this, the usePrevious function is called again:


function usePrevious(value) {
  const ref = useRef()
  return ref.current 
}

And so on. Here's the value for each render for both variables:

Render clicks previousClicks
1 0 undefined
2 1 0
3 2 1
4 3 2

Callback Refs

Callback Refs are a different way to set refs. It gives you a fine-grain control over when refs are attached and detached because you provide a function instead of a ref variable. This function gets called every time the component mounts and unmounts.

Here's an example that shows/hides an emoji every time you click its button. The important thing here is the ref prop that we added. We use a function to log the provided ref:

const callback = (ref) => console.log("callback:", ref)

function App () {
  const [rerenders, setRerenders] = useState(0);

  return (
    <div>
      <button onClick={() => setShow(!show)}>
        {show ? "Hide" : "Show"}
      </button>
      {show && <span ref={callback}>👋</span>}
    </div>
  );
}

Here's an interactive version of the previous code (you can check the output in the console to see that I'm not lying 🙃):

Note: If you use callback refs as inline functions, it will be called twice: one with null and another one with the DOM element. This is because React needs to clear the previous ref every time the function is created. A workaround for this is to use a class method.

(Legacy) String Refs

Warning

String refs are a legacy feature and they are likely to be removed in future React versions.

The way it works is that you provide a string as a ref value like ref="exampleRef" and it automatically gets assigned to this.refs.

Note: String refs can only be used with class components.

Here's an usage example:

export default class App extends React.Component {
  render() {
    console.log(this.refs);

    return (
      <div ref="exampleRef">
        <button onClick={() => this.setState({ dummy: 0 })}>Re-render</button>
      </div>
    );
  }
}

Here's the value for this.refs across renders:

Render this.refs
1 {}
2 { exampleRef: <div>...</div> }
3 { exampleRef: <div>...</div> }
4 { exampleRef: <div>...</div> }

As you can see, on the first render this.refs.exampleRef will be undefined and on the following renders it will point out to the specified DOM node.

Conclusion

We saw what useRef is, how it differentiates with a plain old variable and state variables, and we saw real world examples that uses it. I hope that most of the content makes sense to you!

I'd love to hear your feedback. You can reach out to me on Twitter at any time :-)




from Hacker News https://ift.tt/3agfxYk

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.