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}
</>
);
}
Related
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.
A component has 2 child components, on changing the switch value in one of the child component, it should re-render the other child component. How to achieve this? Please explain with a code snippet.
React components render/rerender for 1 of 3 reasons:
The component enqueues a state update.
Any of the passed props change value.
The parent component rerenders.
Causing case 3 is the simplest way to achieve what you ask. The parent component of both children just needs some declared state that child 1 can call a callback prop when it updates its local state.
Example:
const Child1 = ({ onChange }) => {
const [value, setValue] = React.useState();
React.useEffect(() => {
onChange(value);
}, [onChange, value]);
return (
<React.Fragment>
<h1>Child 1</h1>
<label>
Value
<input
type="checkbox"
value={value}
onChange={(e) => setValue(e.target.checked)}
/>
</label>
</React.Fragment>
);
};
const Child2 = () => {
React.useEffect(() => {
console.log("Child 2 render " + new Date());
});
return <h1>Child 2</h1>;
};
function App() {
const [, rerender] = React.useState();
return (
<div className="App">
<Child1 onChange={rerender} />
<Child2 />
</div>
);
}
const rootElement = document.getElementById("root");
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root" />
Note that the key here is to simply get the parent component to rerender. There are other ways to trigger sibling components to rerender via a common ancestor component.
I tried the following code but it fails
So, this is my Parent Component:
import React from 'react'
import ChildComponent from './ChildComponent';
const ParentComponent = (props) => {
//step 1
// const inputRef = React.createRef();
const buttonRef = React.useRef();
const focusHandler = () => {
alert("hi");
}
return (
<div>
{/* In parent, we generally pass reference to child which we dint do here, lets see if props children help here */}
{props.children}
<ChildComponent ref="buttonRef" />
</div>
)
}
export default ParentComponent;
This is my child component:
import React from 'react'
const ChildComponent = React.forwardRef((props, ref) => {
return (
<div>
<button onClick={ref.focusHandler}>Focus Input</button>
</div>
)
})
export default ChildComponent;
On click of the button above in child component, I wish to call Parent method.
How can that be achieved?
EDITED
The reason you're getting the error is because refs in function components need to be passed using ref={buttonRef}, not ref="buttonRef". Class components have a thing they can do with string refs, but it's not recommended even there.
As for calling a function from a parent component, you don't need refs to do this. So if that was the only reason you were using a ref, you can remove the ref. Instead, pass the function as a prop:
const ParentComponent = (props) => {
const focusHandler = () => {
alert("hi");
}
return (
<div>
<ChildComponent focusHandler={focusHandler} />
</div>
)
}
const ChildComponent = (props) => {
return (
<div>
<button onClick={props.focusHandler}>Focus Input</button>
</div>
)
}
Just replace ref by focusHandler like below in parent component
<ChildComponent focusHandler={focusHandler} />
Then in ChildComponent, remove ref as well.
If you wonder how to use refs in this case (even though this is not the recommended way to pass callbacks), you need to assign focusHandler key and use the ref with ref.current, refer to Components and Props docs.
const ParentComponent = () => {
const buttonRef = React.useRef({ focusHandler: () => alert("hi") });
return (
<div>
<ChildComponent ref={buttonRef} />
</div>
);
};
const ChildComponent = React.forwardRef((props, ref) => {
return (
<div>
<button onClick={ref.current.focusHandler}>Focus Input</button>
</div>
);
});
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
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>
)
}