10

Is it safe to use the setter function of a useState hook as a callback ref function? Will this cause trouble with Suspense or other upcoming React changes? If "yes, this is OK", that's cool! If "no" why not? If "maybe" then when is it OK vs. not?

I'm asking because one of my components requires three refs to be mounted before it can call a DOM API. Two of those required refs are "normal" refs assigned in the same component via a JSX ref prop. The other ref will be assigned, via React context, in a deeply-nested component at some later time. I needed a way to force a re-render of the parent component after all three refs were mounted, and to force a useEffect cleanup when any of the refs are unmounted.

Originally I wrote my own callback ref handler which called a useState setter that I stored in a context provider. But then I realized that the useState setter did everything that my own callback ref did. Is it safe to just use the setter instead of writing my own callback ref function? Or is there a better and/or safer way to do what I'm trying to do?

I tried Googling for "useState" "callback ref" (and other similar keyword variations) but results weren't helpful, other than @theKashey's excellent use-callback-ref package which I will definitely use elsewhere (e.g. when I need to pass a callback ref to a component that expects a RefObject, or when I need both a callback and to use a ref locally) but in this case all the callback needs to do is set a state variable when the ref changes, so Anton's package seems like overkill here.

A simplified example is below and at https://codesandbox.io/s/dreamy-shockley-5dc74.

import * as React from 'react';
import { useState, forwardRef, useEffect, createContext, useContext, useMemo } from 'react';
import { render } from 'react-dom';

const Child = forwardRef((props, ref) => {
  return <div ref={ref}>This is a regular child component</div>;
});

const refContext = createContext();
const ContextUsingChild = props => {
  const { setValue } = useContext(refContext);
  return <div ref={setValue}>This child uses context</div>;
};

const Parent = () => {
  const [child1, setChild1] = useState(null);
  const [child2, setChild2] = useState(null);
  const [child3, setChild3] = useState(null);

  useEffect(() => {
    if (child1 && child2) {
      console.log(`Child 1 text: ${child1.innerText}`);
      console.log(`Child 2 text: ${child2.innerText}`);
      console.log(`Child 3 text: ${child3.innerText}`);
    } else {
      console.log(`Child 1: ${child1 ? '' : 'not '}mounted`);
      console.log(`Child 2: ${child2 ? '' : 'not '}mounted`);
      console.log(`Child 3: ${child3 ? '' : 'not '}mounted`);
      console.log(`In a real app, would run a cleanup function here`);
    }
  }, [child1, child2, child3]);

  const value = useMemo(() => ({ setValue: setChild3 }), []);

  return (
    <refContext.Provider value={value}>
      <div className="App">
        This is text in the parent component
        <Child ref={setChild1} />
        <Child ref={setChild2} />
        <ContextUsingChild />
      </div>
    </refContext.Provider>
  );
};

const rootElement = document.getElementById('root');
render(<Parent />, rootElement);
Justin Grant
  • 41,265
  • 11
  • 110
  • 185
  • Not sure if you already got your answer. I'm currently also wondering the same thing. Libraries like react-popper use useState setters as callback refs as well... https://popper.js.org/react-popper/v2/#example – eMontielG Jul 10 '20 at 01:22

1 Answers1

-1

useState "setter" functions maintain the same reference over render cycles, so those should be safe to use. It is even mentioned that they can be omitted on dependency arrays:

(The identity of the setCount function is guaranteed to be stable so it’s safe to omit.)

You could pass the setter function directly:

<refContext.Provider value={setChild3}>

...and then read it:

const ContextUsingChild = props => {
  const setChild3 = useContext(refContext);
  return <div ref={setChild3}>This child uses context</div>;
};
Nihil
  • 37
  • 1
  • 7