React Callback Refs
Tags: react
Published: 2023-10-28 11:44:03

---
title: React Callback Refs
published: true
description: 
tags: #react 
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-10-27 20:52 +0000
---

React refs are used when you want to update a value between renders but do not want the update to trigger rerendering. Here's one of the examples: `useIsFirstRender` returns true only if it is on the first render.
```ts
function useIsFirstRender(): boolean {
    const isFirstRender = useRef(true);
    
    useEffect(() => {
        isFirstRender.current = false;

        return () => {
            isFirstRender.current = true;
        }
    }, []);

    return isFirstRender.current;
}
```
Making `isFirstRender` a ref object would be better than making it a state, because modifying a state could potentially trigger other side effects unnecessarily during rerenders. 

Now let's take a look at another example of `useRef`. We need to customize a hook `useFocus`. `useFocus` returns a ref object to be assigned to an input element and a boolean `isFocus` indicating if the input is currently being focused.
```ts
function useFocus<T extends HTMLElement>(): [Ref<T>, boolean];

function App() {
  const [ref, isFocused] = useFocus<HTMLInputElement>()
  return <div>
    <input ref={ref}/>
    {isFocused && <p>focused</p>}
  </div>
}
```
At the first glance, you might be thinking to use a `useEffect` block, so we can add `focus` & `blur` event listeners from the input in the `useEffect`, and remove the listeners in a cleanup function. However, this would not work out well in the case that we have two inputs in the same component and the `ref` could potentially be switched from one input the other input. Let's visualise what I mean:
```ts
export function App() {
    const [buttonLabel, setButtonLabel] = useState(0);
    const toggle = () => setButtonLabel(buttonLabel ? 0 : 1);
    const [ref, isFocused] = useFocus<HTMLInputElement>()

    return <div>
                <input ref={buttonLabel ? null: ref}/>
                <input ref={buttonLabel ? ref : null} />
                <button onClick={toggle}>Toggle input</button>
                {isFocused && <p>focused</p>}
    </div>
}
```
As you can see, when the ref changes, we need a way to force the `useEffect` cleanup function to run, otherwise it will cause the problem where the event listeners retain in the previous input. 

## Introducing Callback Refs
Here is when [ref callbacks](https://react.dev/reference/react-dom/components/common#ref-callback) become useful. 
```ts
<div ref={(node) => console.log(node)} />
```
By default ref callbacks are invoked when you pass in a new ref callback. Basically this happens in the following scenarios:
1. The component mounts
2. The component unmounts
3. The component rerenders

Now back to our example, here's the solution with ref callbacks:
```ts
  const refCallback = useCallback((node: T) => {
    if(inputRef.current) {
      inputRef.current.removeEventListener('focus', handleFocus);
      inputRef.current.removeEventListener('blur', handleBlur);   
    }

    if(node) {
      node.addEventListener('focus', handleFocus);
      node.addEventListener('blur', handleBlur);
    }

    inputRef.current = node;
  }, [])
```
We wrap the ref callback with `useCallback` to avoid the callback being triggered every time the component rerenders. E.g, when the user focuses/unfocuses on the input. Now when the ref changes, node changes from the old input to undefined, then takes the new input. The logic sequence goes:
1. Remove the listeners from the old input
2. `inputRef.current` becomes undefiend
3. Attach the listeners to the new input
4. Assign the new input to `inputRef.current`

Here is the full solution:
```ts
import React, { Ref, useState, useRef, useCallback } from 'react'

export function useFocus<T extends HTMLElement>(): [Ref<T>, boolean] {
  const [isFocused, setFocused] = useState(false);
  const input = useRef<T>();

  const handleOnFocus = useCallback(() => setFocused(true), []);
  const handleOnBlur = useCallback(() => setFocused(false), []);

  const inpurRefCallback = useCallback((node: T) => {
    if (input.current) {
      // cleanup listeners on previous ref
      input.current.removeEventListener('focus', handleOnFocus);
      input.current.removeEventListener('blur', handleOnBlur);
    }

    if (node) {
      // Add listeners on new ref
      node.addEventListener('focus', handleOnFocus);
      node.addEventListener('blur', handleOnBlur);
    }

    // save new ref
    input.current = node;
  }, [])

  return [inpurRefCallback, isFocused]
}
```

Ref callbacks would be a perfect usage in the custom hooks like `useFocus` and `useHover` etc. It gives you more control over your implementation when the `node` changes. 

I hope you have learnt something from this post. Feel free to post any questions in the comment section below.

## Bonus
There's also a solution if you really want to use `useEffect` to solve the problem in the example. Here's the code:

```ts
export function useFocus<T extends HTMLElement>(): [Ref<T | undefined>, boolean] {
  const [isFocused, setFocused] = useState(false)
  const ref = useRef<T>()
  useEffect(() => {
    const currentElement = ref.current
      if (!currentElement)
      return

    // initialize the focus state when currentElement changes.
    setFocused(document.activeElement === currentElement)
    
    const onFocus = () => setFocused(true)
    const onBlur = () => setFocused(false)

    currentElement.addEventListener('focus', onFocus)
    currentElement.addEventListener('blur', onBlur)

    return () => {
      currentElement.removeEventListener('focus', onFocus)
      currentElement.removeEventListener('blur', onBlur)
    }
  }, [ref.current]) // now we can pass a dependency array to get much better performance.

  return [ref, isFocused]
}
```

However, I personally would not recommend this approach because when you specify `ref.current` in the useEffect's dependency array, how side effect updates immedicately becomes unintuitive. For example, with the above code on the first render:

- useEffect gets triggered, and it double sets focused to false 
- attach the event listeners 

When the user focuses on the input for the first time:

- useEffect gets triggered again since `ref.current` has changed from undefined to the input, and it double sets focused to true

When the user focuses on the input after the first time:

- useEffect won't be triggered again

The problem is that the value of ref in the deps array is not in sync with the one in the useEffect's body.