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>
);
}
CodePudding user response:
You should be able to achieve by
- Using state in child components
- Using a callback function in the parent component that calls the
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
CodePudding user response:
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.
CodePudding user response:
You can pass a hook back to the parent by passing a callback to the first child, then pass that to the next child. It's pretty sketch, but yeah it's possible. I have not verified that this does not re-render the parent, but I'm fairly certain it would not considering the hook is attached to the child and not the parent.
import './App.css';
import { useState } from "react";
const Child1 = ( stateCb ) => {
const [count, setCount] = useState( 0 )
stateCb( count, setCount )
return <div>
{count}
</div>
}
const Child2 = ( count, setCount ) => {
return <button onClick={() => setCount( count 1 )}>
clickme
</button>
}
function App() {
let count, setCount
return (
<div className="App">
{Child1(( c, sc ) => {
count = c;
setCount = sc
})}
{Child2(count, setCount)}
</div>
);
}
export default App;
CodePudding user response:
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]);
CodePudding user response:
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>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
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).