Photo by Thomas Tastet (Unsplash)
NOTE: I do recommend using these pieces of code sparse. After all, it is an anti-pattern. And if you are a beginner, you should probably read this on how to change the value of child components by lifting state up first...
How to modify the state of a child component?
As we learn to use React we are told this is not possible, or at least it is not desired. We should lift state up, use context, or composition, or any other pattern to solve this issue.
And while that is the correct way to go about it, sometimes you might just want to "reach down" to your components instead of children reaching up...
And for me, it was a bit of an "aha moment" finding this technique on what is actually possible in React.
Entering refs
When we interact with the real DOM we do this using refs. Refs to other objects maintaning their own "state":
function App() {
const ref = useRef();
useEffect(() => {
ref.current.innerText =
"I am manupulating the refs 'state'";
}, []);
return <div className="App" ref={ref} />;
}
This technique can be used to attach references to your components. And while we interact with "something else" it really feels like we are manipulating the state of our children...
The old faithful counter example
Let us assume we have a self-contained Counter component with a simple state.
function Counter() {
const [value, setValue] = useState(0);
function changeValue(factor) {
return function () {
setValue(value + factor);
};
}
return (
<div className="counter-container">
<button onClick={changeValue(-1)}>-</button>
<div className="counter-value">{value}</div>
<button onClick={changeValue(1)}>+</button>
</div>
);
}
Now our specifications change and we need to do custom changes to the number from the parent component.
The correct way to go would, of course, is to lift value and the change handlers to the parent component. Then we could let the parent component maintain the state, and thus update the counter component.
But let us not do this. We are going freaky
forwardRef and useImperativeHandle to the rescue
We are using two utils from the React library to solve the issue. First of all forwardRef
This function wraps a component and lets us attach the ref to another child component. This is usually needed in component libraries to attach your ref to the DOM element (like the example above). When wrapping in forwardRef, your component receives two arguments: the first usual props object, and a second (optional) ref, the actual ref object from the parent instantiating the component.
const Counter = forwardRef(function (props, ref) {
...
})
Next up, the useImperativeHandle hook
This hook that (as stated on the docs) "customizes the instance value that is exposed to parent components when using ref". (And also warn us that this is not a good practice... But let us ignore the second part 😊)
Meaning, we can take a ref and attach properties or functions to it. Thus making them available for the parent component instantiating the ref.
What we add to the component is this piece of code:
useImperativeHandle(ref, () => ({
/** In the imperative handler the change will
immediatly be executed.
*/
changeValue: (factor) => changeValue(factor)(),
setValue
}));
Now the full code for the counter component looks something like this:
const Counter = forwardRef(function (_, ref) {
const [value, setValue] = useState(0);
function changeValue(factor) {
return function () {
setValue(value + factor);
};
}
useImperativeHandle(ref, () => ({
/** In the imperative handler, the change will
immediately be executed.
*/
changeValue: (factor) => changeValue(factor)(),
setValue
}));
return (
<div className="counter-container">
<button onClick={changeValue(-1)}>-</button>
<div className="counter-value">{value}</div>
<button onClick={changeValue(1)}>+</button>
</div>
);
});
Now where ever we use the counter component we can create a ref using the const ref = useRef()
method and pass it to the counter component: <Counter ref={ref} />
. Where ever we have access to the ref we can execute the functions setValue and changeValue like this:
<button
className="wide"
onClick={() => ref.current.setValue(2)}
>
Set counter to 2
</button>
The full code and example can be found here
Summary
As stated, this might not be the best performant or most correct way to solve the original issue. But it is a fun way to explore the possibilities with React. I have only used this with my internal component libraries to access or manipulate some little piece of internal state, where the logic of the component is intended to be self-contained. But then something happens, and you might need to reach for that little piece of state or handler...
Top comments (0)