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>
);
}
You should be able to achieve by
setState function of the child component. This will trigger re-render of the child but not of itself (parent).Demo: https://codesandbox.io/s/child-re-rendering-only-x37ol
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.
You can create an object, store the hook on that object, and use the hook from the object on the children. Not sure why the output count increments twice...
import { useState } from "react";
let parentRenderCount = 0;
let child1RenderCount = 0;
let child2RenderCount = 0;
const Child1 = ( { stateHolder } ) => {
const [count, setCount] = useState( 0 )
console.log('child1 render', ++child1RenderCount)
stateHolder.count = count
stateHolder.setCount = setCount
return <div>
{count}
</div>
}
const Child2 = ( { stateHolder } ) => {
console.log('child2 render', ++child2RenderCount)
return <button onClick={() => stateHolder.setCount( stateHolder.count + 1 )}>
clickme
</button>
}
function App() {
const stateHolder = { }
console.log('parent render', ++parentRenderCount)
return (
<div className="App">
<Child1 stateHolder={stateHolder} />
<Child2 stateHolder={stateHolder} />
</div>
);
}
export default App;