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);
ref={setElement}
andref={element => setElement(element}
in my app... The former misses updates on occasion... I have no clue why – Goodrow