How to change child state from another child without re-rendering parent

I want to change the content of a child component in response to a user event in another child component without causing the parent to re-render.

I’ve tried storing the child state setter to a variable in the parent but it is not defined by the time the children have all rendered, so it doesn’t work.

Is there a minimal way to accomplish this (without installing a state management library)?

ChildToChild.tsx

import React, {
  Dispatch,
  SetStateAction,
  useEffect,
  useRef,
  useState,
} from "react";

export default function ChildToChild() {
  const renderCounter = useRef(0);
  renderCounter.current = renderCounter.current + 1;

  let setChildOneContent;
  const childOneContentController = (
    setter: Dispatch<SetStateAction<string>>
  ) => {
    setChildOneContent = setter;
  };

  return (
    <div>
      <h1>Don't re-render me please</h1>
      <p>No. Renders: {renderCounter.current}</p>
      <ChildOne childOneContentController={childOneContentController} />
      <ChildTwo setChildOneContent={setChildOneContent} />
    </div>
  );
}

function ChildOne({
  childOneContentController,
}: {
  childOneContentController: (setter: Dispatch<SetStateAction<string>>) => void;
}) {
  const [content, setContent] = useState("original content");

  useEffect(() => {
    childOneContentController(setContent);
  }, [childOneContentController, setContent]);

  return (
    <div>
      <h2>Child One</h2>
      <p>{content}</p>
    </div>
  );
}

function ChildTwo({
  setChildOneContent,
}: {
  setChildOneContent: Dispatch<SetStateAction<string>> | undefined;
}) {
  return (
    <div>
      <h2>Child Two</h2>
      <button
        onClick={() => {
          if (setChildOneContent) setChildOneContent("content changed");
        }}
      >
        Change Child One Content
      </button>
    </div>
  );
}

Answer

For an one-off, you could use the following (another ref to store & use the ChildOne setContent:

import React, {
  Dispatch,
  SetStateAction,
  useEffect,
  useRef,
  useState
} from "react";

function ChildOne({
  setChildOneRef
}: {
  setChildOneRef: React.MutableRefObject<React.Dispatch<
    React.SetStateAction<string>
  > | null>;
}) {
  const [content, setContent] = useState("original content");

  useEffect(() => {
    setChildOneRef.current = setContent;
  }, [setChildOneRef]);

  return (
    <div>
      <h2>Child One</h2>
      <p>{content}</p>
    </div>
  );
}

function ChildTwo({
  setChildOneRef
}: {
  setChildOneRef: React.MutableRefObject<React.Dispatch<
    React.SetStateAction<string>
  > | null>;
}) {
  return (
    <div>
      <h2>Child Two</h2>
      <button
        onClick={() => {
          setChildOneRef.current?.("content changed");
        }}
      >
        Change Child One Content
      </button>
    </div>
  );
}

export function ChildToChild() {
  const renderCounter = useRef(0);
  renderCounter.current = renderCounter.current + 1;

  const setChildOneRef = useRef<Dispatch<SetStateAction<string>> | null>(null);
  return (
    <div>
      <h1>Don't re-render me please</h1>
      <p>No. Renders: {renderCounter.current}</p>
      <ChildOne setChildOneRef={setChildOneRef} />
      <ChildTwo setChildOneRef={setChildOneRef} />
    </div>
  );
}

If this pattern is common in your code, you may still want to use state management library or evaluate if “childs” should be really separated.