Update child state based on parent state react functional components - reactjs

Let's say we have a component Accordion that has an internal state isOpen, so you can close and open this component.
We now want to have a parent component that also has a state isOpen and has button. In this component, we have 2 times Accordion and we are passing to Accordion isOpen and we want that if the parent changes state isOpen Accordion accept this.
All component are functional components
const Accordion = ({ isOpen: parentIsOpen = false }) => {
const [isOpen, setIsOpen] = useState(parentIsOpen);
const handleSetIsOpen = () => setIsOpen(!isOpen);
return (
<div>
I'm open: {isOpen}
<button onClick={handleSetIsOpen}>toggle isOpen child</button>
</div>
);
};
const MasterComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const handleSetIsOpen = () => setIsOpen(!isOpen);
return (
<div>
<button onClick={handleSetIsOpen}>toggle isOpen parent</button>
<Accordion isOpen={isOpen} />
<Accordion isOpen={isOpen} />
</div>
);
};
In this case above Accordion will take on first render as the initial state parent isOpen prop. In case we press the button toggle isOpen parent we will change the parent state but children will not be updated.
To fix this we can use useEffect
const Accordion = ({ isOpen: parentIsOpen = false }) => {
const [isOpen, setIsOpen] = useState(parentIsOpen);
const handleSetIsOpen = () => setIsOpen(!isOpen);
useEffect(() => {
if (parentIsOpen !== isOpen) {
setIsOpen(parentIsOpen);
}
}, [parentIsOpen]);
return (
<div>
I'm open: {isOpen}
<button onClick={handleSetIsOpen}>toggle isOpen child</button>
</div>
);
};
const MasterComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const handleSetIsOpen = () => setIsOpen(!isOpen);
return (
<div>
<button onClick={handleSetIsOpen}>toggle isOpen parent</button>
<Accordion isOpen={isOpen} />
<Accordion isOpen={isOpen} />
</div>
);
};
in this case, children will be properly updated when a parent changes isOpen state.
There is one issue with this:
"React Hook useEffect has a missing dependency: 'isOpen'. Either include it or remove the dependency array react-hooks/exhaustive-deps"
So how to remove this issue that esLint is complaining and we do not want to put isOpen in this since it will cause bug.
in case we add isOpen into the array like this:
useEffect(() => {
if (parentIsOpen !== isOpen) {
setIsOpen(parentIsOpen);
}
}, [parentIsOpen, isOpen]);
We will have then a situation where we will click on the internal button in accordion and update the internal state then useEffect will run and see that parent has a different state than the child and will immediately set the old state.
So basically you have a loop where the accordion will never be open then.
The question is what is the best way to update the child state based on the parent state?
Please do not suggest to put all-state in parent and pass props without child state. this will not work since both Accordions in this example have to have their own state and be able to open and close in an independent way, but yet if parent says close or open it should listen to that.
Thank you!

Actually I would say this is way to do it
const Accordion = ({ isOpen: parentIsOpen = false }) => {
const [isOpen, setIsOpen] = useState(parentIsOpen);
const handleSetIsOpen = () => setIsOpen(!isOpen);
useEffect(() => {
setIsOpen(parentIsOpen);
}, [parentIsOpen]);
return (
<div>
I'm open: {isOpen}
<button onClick={handleSetIsOpen}>toggle isOpen child</button>
</div>
);
};
const MasterComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const handleSetIsOpen = () => setIsOpen(!isOpen);
return (
<div>
<button onClick={handleSetIsOpen}>toggle isOpen parent</button>
<Accordion isOpen={isOpen} />
<Accordion isOpen={isOpen} />
</div>
);
};
So just remove state check in a child component, let him update the state but since is updated with the same value it will not rerender or do some expensive behavior.
Tested it today and with a check, if states are different or without is the same, react takes care to not trigger rerender if the state that is updated is the same as before.

What you’re saying not to suggest is in fact the solution I would offer… You’ll need state to control isOpen for the parent component. Also, you should have separate methods in the parent that control state for each accordion, passed along to each accordion in props…
Not sure why you want separate state for the child components. I believe something like this would suffice.
const MasterComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const [isOpen1, setIsOpen1] = useState(false);
const [isOpen2, setIsOpen2] = useState(false);
const handleParentClose = () => {
setIsOpen(false);
setIsOpen1(false);
setIsOpen2(false);
}
return (
<div>
<button onClick={handleParentClose}>toggle isOpen parent</button>
<Accordion isOpen={isOpen1} setIsOpen={setIsOpen1} />
<Accordion isOpen={isOpen2} setIsOpen={setIsOpen2} />
</div>
);
};
const Accordion = props => {
return (
<div>
I'm open: {props.isOpen}
<button onClick={props.setIsOpen}>toggle isOpen child</button>
</div>
);
}
This doesn't include code for actual visibility toggle, but the state is there, including state that closes accordions on parent close.

Related

Rerender FunctionComponent when prop gets set with same value

I'm currently trying to implement some kind of modal (I'm aware that there is a bunch of libraries for that). The real code is much more complex because of a bunch of animation stuff, but it boils down to this (also see this Stackblitz):
const Modal: React.FunctionComponent<{ visible?: boolean }> = ({
visible,
}) => {
const [isVisible, setIsVisible] = React.useState(visible);
React.useEffect(() => setIsVisible(visible), [visible]);
if (!isVisible) {
return null;
}
return (
<div>
I'm visible <button onClick={() => setIsVisible(false)}>Close</button>
</div>
);
};
const App: React.FunctionComponent = () => {
const [showModal, setShowModal] = React.useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>Show modal</button>
<Modal visible={showModal} />
</div>
);
}
The first time the parent component sets the visible property it works without a problem. But when I close the "modal" and want to set the property again it does not show up again, because the property from the point of view of the "modal" didn't actually change.
Is there a way to always rerender a FunctionComponent when a property gets touched even if the value didn't change?
Have you try this:
const Modal: React.FunctionComponent<{ visible?: boolean }> = ({
visible,
setIsVisible
}) => {
if (!isVisible) {
return null;
}
return (
<div>
I'm visible <button onClick={() => setIsVisible(false)}>Close</button>
</div>
);
};
const App: React.FunctionComponent = () => {
const [showModal, setShowModal] = React.useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>Show modal</button>
<Modal visible={showModal} setIsVisible={setShowModal} />
</div>
);
}
It will then re-render also your parent component, because they share the same state
you're trying changing the value in the child element, this does not get reflected in the parent
My suggestion is that to close the modal from parent itself
which reduces the code complexity and there is only single source of data here
export const Modal: React.FunctionComponent<{ visible?: boolean , onClose }> = ({
visible,onClose
}) => {
const [isVisible, setIsVisible] = React.useState(visible);
React.useEffect(() => setIsVisible(visible), [visible]);
if (!isVisible) {
return null;
}
return (
<div>
I'm visible <button onClick={() => onClose()}>Close</button>
</div>
);
};
<Modal visible={showModal} onClose={()=>setShowModal(false)} />
working example https://stackblitz.com/edit/react-ts-heiqak?file=Modal.tsx,App.tsx,index.html

useCallback with onchange function in React JS

I have 2 components. In the parent component I have this:
const Demo = () => {
const [state, setState] = useState(true);
const onChange = useCallback(
(value: string) => {
console.log(value);
},
[],
);
return (
<div className="a">
<button onClick={() => setState(!state)}>sds</button>
<div className="123">
<Bar searchHandler={onChangeSearchHandler} />
</div>
</div>
);
};
In the Bar component I have this:
const Bar = ({ searchHandler }) => {
console.log('bar');
return (
<div>
<input type="text" onChange={(value) => searchHandler(value.target.value)} />
</div>
);
};
Wrapping onChange with useCallback I expect to cache the function and when I click on <button onClick={() => setState(false)}>sds</button> I don't want to render Bar component, but it is triggered. Why Bar component is triggered and how to prevent this with useCallback?
This has nothing to do with the onChange function you're wrapping with useCallback. Bar gets re-rendered because you're changing the state through setState in its parent component. When you change the state in a component all its child components get re-rendered.
You can verify it yourself by trying this:
const Demo = () => {
const [state, setState] = useState(true);
return (
<div className="a">
<button onClick={() => setState(!state)}>sds</button>
<div className="123">
<Bar />
</div>
</div>
);
};
const Bar = ({ searchHandler }) => {
console.log('bar');
return (
<div></div>
);
};
You'll see that the Bar gets re-rerender anyway.
If you want to skip re-rerendring any of the child components, you should memoize them using React.memo when applicable.
Also, you should familiarize yourself with how state in react works and how does it affect the nested components as this is a main concept.
The issue is that you haven't used React.memo on Bar component. The function useCallback works only if you use HOC from memo.
try this, in Bar component create this wrapped component:
const WrappedBar = React.memo(Bar);
and in parent component use this wrapped bar:
const Demo = () => {
const [state, setState] = useState(true);
const onChange = useCallback(
(value: string) => {
console.log(value);
},
[],
);
return (
<div className="a">
<button onClick={() => setState(!state)}>sds</button>
<div className="123">
<WrappedBar searchHandler={onChangeSearchHandler} />
</div>
</div>
);
};

how do control the state for multiple component with one function

I have one simple app that include 3 identical button and when I click the button, onClick event should trigger to display one span. for now, I have use one one state to control span show or not and once I click any one of button all span show. How can I implement the code, so when I click the button, only the correspond span display
import "./styles.css";
import React, { useState } from "react";
const Popup = (props) => {
return <span {...props}>xxx</span>;
};
export default function App() {
const [isOpen, setIsOpen] = useState(true);
const handleOnClick = () => {
setIsOpen(!isOpen);
};
return (
<div className="App">
<button onClick={handleOnClick}> Show popup1</button>
<Popup hidden={isOpen} />
<button onClick={handleOnClick}> Show popup2</button>
<Popup hidden={isOpen} />
<button onClick={handleOnClick}> Show popup3</button>
<Popup hidden={isOpen} />
</div>
);
}
codesandbox:
https://codesandbox.io/s/cocky-fermi-je8lr?file=/src/App.tsx
You should rethink how the components are used.
Since there is a repeating logic and interface, it should be separated to a different component.
const Popup = (props) => {
return <span {...props}>xxx</span>;
};
interface Props {
buttonText: string
popupProps?: any
}
const PopupFC: React.FC<Props> = (props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(!isOpen)}>{props.buttonText}</button>
<Popup hidden={isOpen} {...props.popupProps} />
</>
)
}
export default function App() {
const [isOpen, setIsOpen] = useState(true);
const handleOnClick = () => {
setIsOpen(!isOpen);
};
return (
<div className="App">
<PopupFC buttonText="Show popup1" />
<PopupFC buttonText="Show popup2" />
<PopupFC buttonText="Show popup3" />
</div>
);
}
If each Popup needs its own isOpen state, it would not be possible to achieve with a single boolean state.
Perhaps converting both the button and the span to a single component and letting each Popup component handle its own isOpen:
import "./styles.css";
import React, { useState } from "react";
const Popup = (props) => {
const [isOpen, setIsOpen] = useState(true);
const handleOnClick = () => {
setIsOpen(!isOpen);
};
return (
<>
<button onClick={handleOnClick}>{props.children}</button>
{isOpen && <span {...props}>xxx</span>}
</>
);
};
export default function App() {
return (
<div className="App">
<Popup>Show popup 1</Popup>
<Popup>Show popup 2</Popup>
<Popup>Show popup 3</Popup>
</div>
);
}
That happens simply because you are using the same state "isOpen" for all buttons,
once you click any one of them it reflects all buttons because it's the same value.
you could solve this using Custom Hook since you repeat the logic or you could separate them into small components
Based on your comment, you only want one popup to be open at a time. That was not clear in your original question so the other answers don't address this.
Right now you are just storing a value of isOpen that is true or false. That is not enough information. How do you know which popup is open?
If you want to show just one at a time, you can instead store the number or name (any sort of unique id) for the popup which is currently open.
We make the Popup a "controlled component" where instead of managing its own internal isOpen state, it receives and updates that information via props.
The App component is responsible for managing which popup is open and passing the right props to each Popup component. Since we are doing the same thing for multiple popups, I moved that logic into a renderPopup helper function.
Popup
interface PopupProps {
isOpen: boolean;
open: () => void;
close: () => void;
label: string;
}
const Popup = ({ isOpen, open, close, label }: PopupProps) => {
return (
<>
<button onClick={open}> Show {label}</button>
{isOpen && (
<div>
<h1>{label}</h1>
<span>xxx</span>
<button onClick={close}>Close</button>
</div>
)}
</>
);
};
App
export default function App() {
// store the label of the popup which is open,
// or `null` if all are closed
const [openId, setOpenId] = useState<string | null>(null);
const renderPopup = (label: string) => {
return (
<Popup
label={label}
isOpen={openId === label} // check if this popup is the one that's open
open={() => setOpenId(label)} // open by setting the `openId` to this label
close={() => setOpenId(null)} // calling `close` closes all
/>
);
};
return (
<div className="App">
{renderPopup("Popup 1")}
{renderPopup("Popup 2")}
{renderPopup("Popup 3")}
</div>
);
}
Code Sandbox

setState hook does't change state invoking from child

I am using hook in component to manage modal state.
(Class version of component reproduce the problem)
handleClick will open modal and handleModalClose should close.
I send handleModalClose to Modal component and with console.log could see, that it is processed, but the isModalOpen state not changed (the same for callback setState).
When I am trying to invoke it with setTimeout - state changes and Modal is closing.
Why the hell the state not changes when I invoke changing from child???
const [isModalOpen, setModalOpen] = useState(false);
const handleClick = () => {
setModalOpen(true);
// setTimeout(() => handleModalClose, 10000);
};
const handleModalClose = () => {
console.log('!!!!!!!!handleModalClose');
setModalOpen(false);
};
return (
<div onClick={handleClick}>
{isModalOpen && <Modal closeModal={handleModalClose} />}
</div>
);
and here is extract from Modal
const Modal = (props) => {
const { closeModal } = props;
return (
<>
<div className="modal">
<form className="" onSubmit={handleSubmit(onSubmit)}>
<button type="button" className="button_grey button_cancel_modal" onClick={closeModal}>
</button>
PROBLEM SOLVED. e.stopPropagation() - added.
const handleModalClose = (e) => {
e.stopPropagation();
console.log('!!!!!!!!handleModalClose');
setModalOpen(false);
};
Modal was closed and instantly reopen by bubbling w/o this.

How to prevent double rerender when I use props, useState and useEffect?

For example I have this component
const FooBar = (props) => {
console.log("render")
const [foo, setFoo] = useState(props.foo)
useEffect(() => {
setFoo(props.foo)
}, [props.foo])
return (
<div>
{foo} <button onClick={() => setFoo(x => x + 1)}>component plus</button>
</div>
);
}
And I can change it props like this
const App = () => {
const [foo, setFoo] = useState(1)
return (
<div>
<FooBar foo={foo} />
<button onClick={() => setFoo(x => x + 1)}>parent plus</button>
</div>
);
}
When I click on parent plus button the FooBar component will rerender two times. the first one is from props change and the second one is from setFoo inside useEffect.
How can I prevent second rerender?
I am not exactly sure what your goal with this example would be.
If you want to have two buttons that increment the same counter, than your code currently does not work as intended.
If you click the 'component plus' button several times and afterwards click the 'parent plus' button, your counter will reset to the value when you last clicked the parent button.
In case you want to pass state to child components, you usually do not want to save the passed state as the child components state. That will make it unnecessarily harder to sync up both states.
What you would do, is to pass the state and a function to set that state to the child component.
Your example would then look like this:
const App = () => {
const [foo, setFoo] = React.useState(1);
return (
<div>
<FooBar foo={foo} setFoo={setFoo} />
<button onClick={() => setFoo(x => x + 1)}>parent plus</button>
</div>
);
};
const FooBar = props => {
console.log("render");
return (
<div>
{props.foo} <button onClick={() => props.setFoo(x => x + 1)}>component plus</button>
</div>
);
};

Resources