What are production use cases for the useRef, useMemo, useCallback hooks?

ln09nv2 picture ln09nv2 · Mar 1, 2021 · Viewed 7.3k times · Source

Outside of the counter example seen in many YouTube tutorial videos, what are practical/real-world use cases for useMemo and useCallback?

Also, I've only seen an input focus example for the useRef hook.

Please share other use cases you've found for these hooks.

Answer

Ajeet Shah picture Ajeet Shah · Mar 14, 2021

useRef:

Syntax: const refObject = useRef(initialValue);

It simply returns a plain JavaScript object. Its value can be accessed and modified (mutability) as many times as you need without worrying about "rerender".

Its value will persist (won't be reset to the initialValue unlike an ordinary* object defined in your function component; it persists because useRef gives you the same object instead of creating a new one on subsequent renders) for the component lifetime.

If you write const refObject = useRef(0) and print refObject on console, you would see the log an object - { current: 0 }.

*ordinary object vs refObject, example:

function App() {
  const ordinaryObject = { current: 0 } // It will reset to {current:0} at each render
  const refObject = useRef(0) // It will persist (won't reset to the initial value) for the component lifetime
  return <>...</>
}

Few common uses, examples:

  1. To access the DOM: <div ref={myRef} />
  2. Store mutable value like instance variable (in class)
  3. A render counter
  4. A value to be used in setTimeout / setInterval without a stale closure issue.

useMemo:

Syntax: const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

It returns a memoized value. The primary purpose of this hook is "performance optimization". Use it sparingly to optimize the performance when needed.

It accepts two arguments - "create" function (which should return a value to be memoized) and "dependency" array. It will recompute the memoized value only when one of the dependencies has changed.

Few common uses, examples:

  1. Optimize expensive calculations (e.g. operations on data like sort, filter, changing format etc.) while rendering

Unmemoized example:

function App() {
  const [data, setData] = useState([.....])

  function format() {
    console.log('formatting ...') // this will print at every render
    const formattedData = []
    data.forEach(item => {
      const newItem = // ... do somthing here, formatting, sorting, filtering (by date, by text,..) etc
      if (newItem) {
        formattedData.push(newItem)
      }
    })
    return formattedData
  }

  const formattedData = format()

  return <>
    {formattedData.map(item => <div key={item.id}>
      {item.title}
    </div>)}
  </>
}

Memoized example:

function App() {
  const [data, setData] = useState([.....])

  function format() {
    console.log('formatting ...') // this will print only when data has changed
    const formattedData = []
    data.forEach(item => {
      const newItem = // ... do somthing here, formatting, sorting, filtering (by date, by text,..) etc
      if (newItem) {
        formattedData.push(newItem)
      }
    })
    return formattedData
  }

  const formattedData = useMemo(format, [data])

  return <>
    {formattedData.map(item => <div key={item.id}>
      {item.title}
    </div>)}
  <>
}

useCallback:

Syntax: const memoizedCallback = useCallback(() => { //.. do something with a & b }, [a, b])

It returns a memoized function (or callback).

It accepts two arguments - "function" and "dependency" array. It will return new i.e. re-created function only when one of the dependencies has changed, or else it will return the old i.e. memoized one.

Few common uses, examples:

  1. Passing memoized functions to child components (that are optimized with React.memo or shouldComponentUpdate using shallow equal - Object.is) to avoid unnecessary rerender of child component due to functions passed as props.

Example 1, without useCallback:

const Child = React.memo(function Child({foo}) {
  console.log('child rendering ...') // Child will rerender (because foo will be new) whenever MyApp rerenders
  return <>Child<>
})

function MyApp() {
  function foo() {
    // do something
  }
  return <Child foo={foo}/>
}

Example 1, with useCallback:

const Child = React.memo(function Child({foo}) {
  console.log('child rendering ...') // Child will NOT rerender whenever MyApp rerenders
  // But will rerender only when memoizedFoo is new (and that will happen only when useCallback's dependency would change)
  return <>Child<>
})

function MyApp() {
  function foo() {
    // do something
  }
  const memoizedFoo = useCallback(foo, [])
  return <Child foo={memoizedFoo}/>
}
  1. Passing memoized functions to as dependencies in other hooks.

Example 2, without useCallback, Bad (But eslint-plugin-react-hook would give you warning to correct it):

function MyApp() {
  function foo() {
    // do something with state or props data
  }
  useEffect(() => {
    // do something with foo
    // maybe fetch from API and then pass data to foo
    foo()
  }, [foo])
  return <>...<>
}

Example 2, with useCallback, Good:

function MyApp() {
  const memoizedFoo = useCallback(function foo() {
    // do something with state or props data
  }, [ /* related state / props */])

  useEffect(() => {
    // do something with memoizedFoo
    // maybe fetch from API and then pass data to memoizedFoo
    memoizedFoo()
  }, [memoizedFoo])
  return <>...<>
}

These hooks rules or implementations may change in the future. So, please make sure to check hooks reference in docs. Also, it is important to pay attention to eslint-plugin-react-hook warnings about dependencies. It will guide you if omit any dependency of these hooks.