Reputation: 14619
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>
);
}
Upvotes: 2
Views: 1694
Reputation: 786
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;
Upvotes: 1
Reputation: 33749
You can store the Dispatch<SetStateAction<T>>
function in a ref
in the parent:
import ReactDOM from 'react-dom';
import {
Dispatch,
MutableRefObject,
ReactElement,
SetStateAction,
useEffect,
useRef,
useState,
} from 'react';
type ChildProps = {
setValueRef: MutableRefObject<Dispatch<SetStateAction<string>> | undefined>;
};
function Child1 (props: ChildProps): ReactElement {
const [dateString, setDateString] = useState(new Date().toISOString());
useEffect(() => {
props.setValueRef.current = setDateString;
}, [props.setValueRef, setDateString]);
return <div>{dateString}</div>;
}
function Child2 (props: ChildProps): ReactElement {
const handleClick = (): void => {
props.setValueRef.current?.(new Date().toISOString());
};
return <button onClick={handleClick}>Update</button>;
}
function Parent (): ReactElement {
const setValueRef = useRef<Dispatch<SetStateAction<string>> | undefined>();
const renderCountRef = useRef(0);
renderCountRef.current += 1;
return (
<div>
<div>Render count: {renderCountRef.current}</div>
<Child1 {...{setValueRef}} />
<Child2 {...{setValueRef}} />
</div>
);
}
ReactDOM.render(<Parent />, document.getElementById('root'));
Here's the same code in a runnable snippet (with types erased and import
statements converted to use the UMD version of React):
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
<div id="root"></div>
<script type="text/babel" data-type="module" data-presets="react">
const {useEffect, useRef, useState} = React;
function Child1 (props) {
const [dateString, setDateString] = useState(new Date().toISOString());
useEffect(() => {
props.setValueRef.current = setDateString;
}, [props.setValueRef, setDateString]);
return <div>{dateString}</div>;
}
function Child2 (props) {
const handleClick = () => {
props.setValueRef.current?.(new Date().toISOString());
};
return <button onClick={handleClick}>Update</button>;
}
function Parent () {
const setValueRef = useRef();
const renderCountRef = useRef(0);
renderCountRef.current += 1;
return (
<div>
<div>Render count: {renderCountRef.current}</div>
<Child1 {...{setValueRef}} />
<Child2 {...{setValueRef}} />
</div>
);
}
ReactDOM.render(<Parent />, document.getElementById('root'));
</script>
That being said, this is a strange pattern, and the scenario can be solved idiomatically using React.memo
(this is the case it's designed for).
Upvotes: 0
Reputation: 336
The event could be propagated over the RxJS Subject, with value if it is needed.
// in parent
// const subject = new rxjs.Subject();
const subject = { // or create lightweight alternative
nextHandlers: [],
next: function(value) {
this.nextHandlers.forEach(handler => handler(value));
},
subscribe: function(nextHandler) {
this.nextHandlers.push(nextHandler);
return {
unsubscribe: () => {
this.nextHandlers.splice(
this.nextHandlers.indexOf(nextHandler),
1
);
}
};
}
};
// provide next method to the childOne
onClick={value => subject.next(value)}
// provide subscribe method to the childTwo
subscribeOnClick={(nextHandler) => subject.subscribe(nextHandler)}
// in childTwo
useEffect(() => {
const subscription = subscribeOnClick(value => setContent(value));
return () => subscription.unsubscribe();
}, [subscribeOnClick]);
Upvotes: 0
Reputation: 2027
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
Upvotes: 3
Reputation: 599
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.
Upvotes: 1