DEV Community

Cover image for Manipulating child state in React, a fun anti-pattern
Simon Ström
Simon Ström

Posted on • Updated on

Manipulating child state in React, a fun anti-pattern

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} />;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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) {
...
})
Enter fullscreen mode Exit fullscreen mode

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
  }));
Enter fullscreen mode Exit fullscreen mode

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>
  );
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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...

Latest comments (0)