how to use React.memo with a Component contains children - reactjs

I have two Components, and I wrapped Parent with React.memo:
Child
const Child = ()=> <div>I'm Child</div>
export default Child
Parent
const Parent = (props)=> <div>{props.children}</div>
export default React.memo(Parent)
Use in App:
const App = () => {
const [count, setCount] = useState(0)
return(
<div>
<button onClick={()=>setCount(count+1)}></button>
<Parent>
<Child></Child>
</Parent>
</div>
)
}
Click the button, result:
The Parent Component will rerender, so the memo not working because it's children is a function component
So, how can I prevent rerender?
I know a way to solve by useMemo, but it's ugly and not friendly, do you have better ideas?
const App = () => {
const [count, setCount] = useState(0)
const children = useMemo(()=><Child></Child>,[])
return(
<div>
<button onClick={()=>setCount(count+1)}></button>
<Parent>
{children}
</Parent>
</div>
)
}

Wrap your <Child /> with React.memo:
const Child = ()=> {
console.log('render') // fires only once - on initial render
return <div>I'm Child</div>
}
const MChild = React.memo(Child);
const Parent = (props)=> <div>{props.children}</div>
const MParent = React.memo(Parent)
const App = () => {
const [count, setCount] = useState(0);
return(
<div>
<button onClick={()=>setCount(count+1)}>increment {count}</button>
<MParent>
<MChild></MChild>
</MParent>
</div>
)
}
render(<App />, document.getElementById('root'));

const children = useMemo(()=><Child></Child>,[])
Is the easiest way to go. Using memo(Child) wont work since jsx in fact returns a new object whenever you call <Child />. React.memo by default just use simple shallow comparison so there really is no other direct way to solve it. You can create your own function that would eventually support children and pass it to React.memo(MyComp, myChildrenAwareEqual).

Move <Parent><Child/></Parent> into a separate component, and memoize that component instead:
const Family = memo(() => <Parent><Child/></Parent>);
const App = () => {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count => count + 1)}>{count}</button>
<Family />
</>
)
}
demo

const Memo = React.memo(({ children, value }) => {
return children
}, (prev, next) => prev.value === next.value)
And then to use it
function Title() {
const [count, setCount] = useState(0)
const onClick = () => {
setCount(c => c + 1)
}
const a = ''
return (
<>
<div onClick={onClick}>{count}</div>
<Memo value={a}>
<Child>
<Nothing2 a={a} />
</Child>
</Memo>
</>
)
}
When a changes, Child renders, otherwise it bails out with previous render.
I outline the rational behind it in my blog, https://javascript.plainenglish.io/can-usememo-skip-a-child-render-94e61f5ad981

Related

React - reuse state of parent component in component imported as children prop

I want to pass event prop from Child to Parent, so when we click the button in Child component, state from Parent should be triggered.
Let's assume that we have two components, Parent and Child, but Child will not be imported directly in Parent, like this
export default function Parent() {
const [count, setCount] = useState(0);
const handleClick = num => {
setCount(current => current + num);
};
return (
<div>
<Child handleClick={handleClick} />
<h2>Count: {count}</h2>
</div>
);
}
and it's very straitforward to pass prop in this case, but how to do when we have situation when Parent does not know which component will be passed as {children} prop, like this:
export default function Parent({children}) {
const [count, setCount] = useState(0);
const handleClick = num => {
setCount(current => current + num);
};
return (
<div>
{children}
</div>
);
}
and later we import some child component
<Parent>
<Child />
</Parent>
You can use cloneElement along with React.Children.map. cloneElement clones and returns a new React element using a given element as the starting point. You can add props, ref and keys on top.
React.Children.map will invoke a function on every immediate child.
//function body
....
let newChildren = React.Children.map(children, (child) => React.cloneElement(child, { handleClick })
);
return (
<div>
{newChildren}
</div>
);
Example
This would couple parent to a child. children property is specifically to make sure parent does not care what the child is.
Push the state & callback up the component hierarchy, to the component that knows specific types of both parent and child components.
const [count, setCount] = useState(0);
return (<Parent count={count}>
<Child onClick={num => setCount(count => count + num} />
</Parent>);
Simply, You can achieve this via cloneElement like this.
export const Parent = ({children}) => {
const handleClick = () => {
console.log('okk')
}
return (
<div>
{ React.cloneElement(children, { handleClick} )}
</div>
)}
and now you are able to trigger handleClick function from the child component.

Why does a React component unmount but jsx doesn't

I came across this behaviour recently and was trying to understand its cause. Basically, what I noticed so far is that a React child component will be mounted and unmounted on state changes of its parent. However, a jsx containing the same child component does not.
I put together this simplified example to demonstrate the behaviour.
const Child = ({ title }) => {
const [count, setCount] = React.useState(0);
const increment = () => setCount((x) => x + 1);
return (
<button onClick={increment}>
{title} Current count = {count}
</button>
);
};
const App = () => {
const [, setState] = React.useState(false);
const rerender = () => setState((x) => !x);
const ChildWrapper = () => <Child title="Component" />;
const childWrapperJsx = <Child title="jsx" />;
return (
<div>
<button onClick={rerender}>Re render parent</button>
<br />
<ChildWrapper />
{childWrapperJsx}
</div>
);
}
const domContainer = document.querySelector('#root');
const root = ReactDOM.createRoot(domContainer);
const e = React.createElement;
root.render(e(App));
<script src="https://unpkg.com/react#18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#18/umd/react-dom.development.js" crossorigin></script>
<div id="root"></div>
Does anyone know the reason behind this? Is there a way to prevent React from unmounting a child component in this case?
I think your question is about the difference between the two buttons' behavior, one is coming back to zero after clicking to rerender the parent component and the other not.
First of all, we should understand the life-cycle of a function component, the render is executing each state change.
function App() {
/**
* it will be executed each render
*/
return (<div />);
}
Also we have to understand the difference between create a component and instantiate a component.
// this is creating a component
const ChildWrapper = () => <Child title="Component" />;
// this is instantiating a component
const childWrapperJsx = <Child title="jsx" />;
JSX is only a tool to transpile the syntaxe of this <Child title="jsx" /> to React.createElement('div', { title: "jsx" }) for example. To explain better, the code is transpiled to something like this:
// this is creating a component
const ChildWrapper = () => React.createElement('button', { title: 'Component' });
// this is instantiating a component
const childWrapperJsx = React.createElement('button', { title: 'jsx' }) ;
Without going deep in the hole. In your implementation we have both components implemented into the render of the parent, like this.
function App() {
const ChildWrapper = () => <Child title="Component" />;
const childWrapperJsx = <Child title="jsx" />;
return (<div />);
}
Right now we realized that the first implementation is creating a new component each render, so that react can't memoize the component in the tree, it is not possible to do it.
// each render ChildWrapper is a new component.
const ChildWrapper = () => <Child title="Component" />;
And the second one, the childWrapperJsx is already a react element instantiated and memoized. React will preserve the same instance on the parent component life cycle.
According to React's best practice, it is not recommended to create components inside the render of another component. If you try to put both implementations outside of the component, you will be able to see that both components won't be unmounted after the parent component's render.
const ChildWrapper = () => <Child title="Component" />;
const childWrapperJsx = <Child title="jsx" />;
function App() {
return (
<>
<ChildWrapper />
{childWrapperJsx}
</>
);
}

React: useState hook updates only the last instance from multiple instances of the same functional component

When I click on any of 3 items then only the last one is incremented. When using class-based component instead of functional one for Item component then everything is fine. Could someone explain please?
const { Fragment, useState } = React;
const List = () => {
return (
<Fragment>
<Item /><Item /><Item />
</Fragment>
)
};
const Item = () => {
[count, setCount] = useState(0);
return (
<div className="item" onClick={() => setCount(count + 1)}>{count}</div>
);
};
ReactDOM.render(
<List />,
document.getElementById('root')
);
https://jsfiddle.net/antonfil/qk08srn5/17/
You are creating globals when you do
[count, setCount] = useState(0);
You need to do
const [count, setCount] = useState(0);

React component does no re-render when it is swapped with the same one

Please consider the following code:
import React from "react";
import "./styles.css";
const Component = ({ title }) => {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log("Mounted");
}, []);
return (
<div>
<h2>{title}</h2>
<p>{count}</p>
<button onClick={() => setCount(c => c + 1)}>count up</button>
</div>
);
};
export default function App() {
const [index, setIndex] = React.useState(0);
const changeComponent = () => {
setIndex(c => (c === 1 ? 0 : 1));
};
const components = [
{
render: () => <Component title="one" />
},
{
render: () => <Component title="two" />
}
];
return (
<>
<button onClick={changeComponent}>toggle component</button>
{components[index].render()}
</>
);
}
https://codesandbox.io/s/mystifying-hermann-si7cn
When you click toggle component, title changes, but component is not unmounted, you can see it because count is not reset.
How to make it so that new component is mounted on toggle component click?
React needs a way to differentiate one component instance from the other. This will fix it
const components = [
{
render: () => <Component key={1} title="one" />
},
{
render: () => <Component key={2} title="two" />
}
];
Its the same reason react requires dynamically rendered lists to have a key prop. It informs react of which component to update.

Change react hook state from parent component

I have a hook component like this:
import React, { useState} from "react";
const MyComponent = props => {
const [value, setValue] = useState(0);
const cleanValue = () => {
setValue(0);
};
return (<span><button onClick={()=>setValue(1)}/>{value}<span>)
}
I want to reset value from the parent component. How I can call clean value from parent component? the parent component is a stateful component.
If the parent has to control the child state then probably the state must reside in the parent component itself. However you can still update child state from parent using ref and exposing a reset method in child. You can make use of useImperativeHandle hook to allow the child to only expose specific properties to the parent
const { useState, forwardRef, useRef, useImperativeHandle} = React;
const Parent = () => {
const ref = useRef(null);
return (
<div>
<MyComponent ref={ref} />
<button onClick={() => {ref.current.cleanValue()}} type="button">Reset</button>
</div>
)
}
const MyComponent = forwardRef((props, ref) => {
const [value, setValue] = useState(0);
const cleanValue = () => {
setValue(0);
};
useImperativeHandle(ref, () => {
return {
cleanValue: cleanValue
}
});
return (<span><button type="button" onClick={()=>setValue(1)}>Increment</button>{value}</span>)
});
ReactDOM.render(<Parent />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="app"/>
From the React documentation about Fully uncontrolled component with a key:
In order to reset the value ..., we can use the special React attribute called key. When a key changes, React will create a new component instance rather than update the current one. Keys are usually used for dynamic lists but are also useful here.
In this case, we can use a simple counter to indicate the need for a new instance of MyComponent after pressing the Reset button:
const { useState } = React;
const Parent = () => {
const [instanceKey, setInstanceKey] = useState(0)
const handleReset = () => setInstanceKey(i => i + 1)
return (
<div>
<MyComponent key={instanceKey} />
<button onClick={handleReset} type="button">Reset</button>
</div>
)
}
const MyComponent = () => {
const [value, setValue] = useState(0)
return (
<span>
<button type="button" onClick={()=>setValue(v => v + 1)}>{value}</button>
</span>
)
};
ReactDOM.render(<Parent />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="app"/>
You can't / shouldn't. Using hooks instead of stateful class components doesn't change the fact that if you want the parent to own the state, you need to declare the state in the parent.
It should look something like this, depending on when you want to reset the value (here I used another button):
const MyButton = (props) = (
// Whatever your button does, e.g. styling
<span>
<button {...props} />
<span>
)
const Parent = props => {
const [value, setValue] = useState(0);
const cleanValue = () => setValue(0);
return (
<div>
<MyButton onClick={() => setValue(1)}>
{value}
</MyButton>
<button onClick={cleanValue}>
Reset
</button>
</div>
)
}

Resources