Send order to children - javascript

const Parent = ({list}) => {
const closeAll = () => {
// What should be in here?
}
return (
<>
<button onClick={() => closeAll()}>Close All</button>
{list.map(item => <Accordion item={item}/>)}
</>
)
}
const Accordion = ({item}) => {
const [open, setOpen] = useState(false);
return (
<div onClick={() => setOpen(o => !o)}>
<p>item.name</p>
{open && <p>item.detail</p>
</div>
)
}
Basically, as above, there is the Accordion components and a parent component that hosts all of them. Each Accordion component has a state called open. I want to change state of each child from parent component. How can I send an order to a child component to change its state?

Lift your state up into Parent.
closeAll can just map over the list and set all the open properties to false.
Have a handleClick callback that you pass down to Accordion which sets the state of the clicked item's open property to the inverse in Parent
Take a look at the react docs for lifting state up.
import { useState } from "react";
const data = [
{
detail: "foo",
name: "bar"
},
{
detail: "foo1",
name: "bar1"
}
];
const Parent = ({ defaultList = data }) => {
const [list, setList] = useState(
defaultList.map((i) => ({
...i,
open: false
}))
);
const closeAll = () => {
setList(
list.map((i) => ({
...i,
open: false
}))
);
};
const handleClick = (i) => {
const newList = [...list];
newList[i].open = !list[i].open;
setList(newList);
};
return (
<>
<button onClick={() => closeAll()}>Close All</button>
{list.map((item, i) => (
<Accordion item={item} handleClick={() => handleClick(i)} />
))}
</>
);
};
const Accordion = ({ item, handleClick }) => {
return (
<div>
<button onClick={handleClick}>{item.name}</button>
{item.open && <p>{item.detail}</p>}
</div>
);
};
export default Parent;
If you are unable to lift your state there is an alternative approach using react refs.
Create ref (initially an empty array) that each Accordion will push its own close state setting function into when it first renders.
In Parent, loop over the the array of close state settings functions inside the ref and execute each.
const Parent = ({ list = data }) => {
const myRef = useRef([]);
const closeAll = () => {
myRef.current.forEach((c) => c());
};
return (
<>
<button onClick={() => closeAll()}>Close All</button>
{list.map((item, i) => (
<Accordion item={item} myRef={myRef} />
))}
</>
);
};
const Accordion = ({ item, myRef }) => {
const [open, setOpen] = useState(false);
useEffect(() => {
myRef.current.push(() => setOpen(false));
}, [myRef]);
return (
<div>
<button onClick={() => setOpen((o) => !o)}>{item.name}</button>
{open && <p>{item.detail}</p>}
</div>
);
};
export default Parent;

Using an internal state for the component is not recommended, at least from my point of view for what you are doing.
you can control the open state of each list item from its properties like the example here:
const Parent = ({ list }) => {
const [isAllClosed, setIsAllClosed] = useState(false);
const closeAll = () => {
setIsAllClosed(true)
};
return (
<>
<button onClick={closeAll}>Close All</button>
{list.map((item) => (
item.open = isAllClosed != null ? (!isAllClosed) : true;
<Accordion item={item} />
))}
</>
);
};
const Accordion = ({ item }) => {
return (
<div onClick={() => console.log('item clicked')}>
<p>item.name</p>
{item.open ? <p>item.detail</p> : null}
</div>
);
};
I also replaced you short circuit evaluation open && <p>item.detail</p> to a ternary. The reason for that is that you will get a string false being printed if not true, does it make sense?
You will need somehow control the state of the whole list whether an item is open or not from whoever is using the parent.
But avoid using internal state when you can.

I think you can try creating a state variable inside the parent and passing it as a prop to the child to control the behavior of the child.
const Parent = ({ list }) => {
const [masterOpen, setMasterOpen] = useState(true);
<>
<button onClick={() => setMasterOpen(false)}>Close All</button>
{list.map((item) => (
<Accordion item={item} parentOpen={masterOpen} />
))}
</>
);
};
const Accordion = ({ item, parentOpen }) => {
const [open, setOpen] = useState(false);
if (!parentOpen) {
setOpen(false);
}
return (
<div onClick={() => setOpen((o) => !o)}>
<p>{item.name}</p>
{open && <p>item.detail</p>}
</div>
);
};

Related

How to avoid re-renders in React when using conditional Render and children node prop

I have a component that I reuse most of its logic.
I'm looking to avoid re-renders on its children components which happen every time I hover over the parent:
const ReusableComponent = ({ conditional }) => {
const [isHovered, setIsHovered] = useState(false);
const AnotherReusableComponent = ({ children }) => (
<div>{children}</div>
);
const renderComponent = () => {
if (conditional) {
return <ComponentA />;
};
return <ComponentB />;
};
return (
<div>
<title
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
Menu
</title>
<div className={isHovered ? 'oneClass' : 'otherClass'}>
<AnotherReusableComponent>
{renderComponent()}
</AnotherReusableComponent>
</div>
</div>
);
}
Notes:
ComponentA and ComponentB are shown/hidden depending on className
onHover event toggles className
Tried memo, didn't work.
The re-render happens onHover
First, Components should never be declared inside other components, since they will be recreated at each render unless you memoize them with useMemo.
Even if you declare it outside you will experience a re-render of <AnotherReusableComponent> and its children when isHovered changes, hence you need to wrap it into React.memo() to ensure it won't re-render unless its props change:
const AnotherReusableComponent = React.memo(({ children }) => (
<div>{children}</div>
));
const ComponentA = React.memo(() => (
<div>A</div>
));
const ComponentB = React.memo(() => (
<div>B</div>
));
const ReusableComponent = ({ condition }) => {
const [isHovered, setIsHovered] = useState(false);
return (
<div>
<title
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
Menu
</title>
<div className={isHovered ? 'oneClass' : 'otherClass'}>
<AnotherReusableComponent>
{condition ? <ComponentA/> : <ComponentB/>}
</AnotherReusableComponent>
</div>
</div>
);
}
This should avoid the re-render of AnotherReusableComponent and its children unless condition changes.

React OnClick iteration

I want to do an onClick counter but I have a problem with the counter iterating correctly. In the app there are 3 "products" and after clicking "Add To Cart" button the state of the object is updated but all of the products are generated separately. I think that is cousing the problem where the counter is different for each of the products or everything will work correctly if I lift the state up, but the console.log is just freshly generated for all of the products. I'm not really sure so I need help with that.
Here is some code in the order from the parent to the last child:
import { useEffect, useState } from "react";
import ProductList from "./ProductList";
const Products = () => {
const [products, setProducts] = useState (null);
useEffect (() => {
fetch('http://localhost:8000/products')
.then(res => {
return res.json();
})
.then(data => {
setProducts(data);
})
}, []);
return (
<div className="ProductList">
{products && <ProductList products={products}/>}
</div>
);
}
export default Products;
import Card from "./Card";
const ProductList = (props) => {
const products = props.products;
return (
<div className="ProductList" >
{products.map((product) => (
<Card product={product} key={product.id} />))}
</div>
);
}
export default ProductList;
import { useState } from "react";
const Card= ({ product }) => {
const [showDescription, setShowDescription] = useState(false);
const [CartCounter, setCartCounter ] = useState(0);
console.log(CartCounter);
return (
<div className="Product-Preview" >
<div className="backdrop" style={{ backgroundImage: `url(${product.image})` }}></div>
<h2>{product.title}</h2>
<div>{product.price}</div>
<button className="ShowDescription" onClick={() => setShowDescription(!showDescription)}>Details</button>
<button className="AddToCart" onClick={() => setCartCounter(CartCounter + 1)}>Add To Cart </button>
{showDescription && <p>{product.description}</p>}
<br />
</div>
);
};
export default Card;
Ok, you want to keep track of an aggregated value. I'll list code in some high level.
const ProductList = () => {
const [count, setCount] = useState(0)
const addOrRemove = n => { setCount(v => v + n) }
return products.map(p => <Card addOrRemove={addOrRemove} />)
}
const Card = ({ addOrRemove }) => {
// optional if you want to track card count
// const [count, setCount] = useState(0)
return (
<>
<button onClick={() => { addOrRemove(1) }>Add</button>
<button onClick={() => { addOrRemove(-1) }>Remove</button>
</>
)
}
Essentially either you track the local count or not, you need to let the parent to decide what is the final count, otherwise there'll be some out of sync issue between the child and parent.

How to open modal bootrap from parent and close it by himself

I want to open a modal from a parent component with props, and then close it when everything it's done and notify to the parent in case he wants to open again.
const ModalChild = (props) => {
const [Activate, setActivate] = useState(props.Activate);
const toggle = () => setActivate(!Activate);
useEffect(() => {
setActivate(props.Activate)
}
}, []);
<Modal isOpen={Activate} toggle={false} >
<text>hello {props.hey}<text>
</Modal>
}
and the parents component, something like this:
const Accountlist= () => {
const [Activate, setActivate] = useState(false);
const toggle = (value) => {
setActivate(true)
}
render(
<button onClick={() => toggle(value)} />
<ModalEdit props={Activate}/>
)}
Please, anyone have anyidea?
If it were me, I will do it like this.
const Accountlist= () => {
const [activate, setActivate] = useState(false);
const toggleActivate = (value) => {
setActivate(value);
}
return [
<button onClick={() => toggleActivate(!activate)} />,
<ModalEdit isActivated={activate} toggleActivate={toggleActivate} />
]}
And at the child component.
const ModalChild = ({ isActivated, toggleActivate }) => {
useEffect(() => {
toggleActivate(!isActivated);
}, []);
<Modal isOpen={isActivated} toggle={() => toggleActivate(false)} >
<text>hello {props.hey}<text>
</Modal>
}
I'm not sure what you are trying to do but the rest with the logic I'll leave it to you.

MUI - How to open Dialog imperatively/programmatically

Normally this is how you use MUI Dialog. The code below is taken from the docs:
export default function AlertDialog() {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
return (
<div>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
Open Dialog
</Button>
<Dialog open={open} onClose={handleClose}>
{...}
</Dialog>
</div>
);
}
But I want it to create the Dialog imperatively, sort of like fire and forget. I do not want to embed the Dialog component in other components whenever I need to create them. Ideally I'd want to call it like this
createDialog(<>
<h1>My Dialog</h1>
<span>My dialog content</span>
<button onClick={() => closeDialog()}>Close</button>
</>)
So my component definition'd look like this
const createDialog = () => {
// ???
}
const closeDialog = () => {
// ???
}
export default function AlertDialog() {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => setOpen(true);
const handleClose = () => {
createDialog(<>
<h1>My Dialog</h1>
<span>My dialog content</span>
<button onClick={() => closeDialog()}>Close</button>
</>)
};
return (
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
Open Dialog
</Button>
);
}
You can reuse dialogs using React's Provider pattern. The official React document has explained in good detail so I won't cover it again here.
First create a custom Provider component in this case I'll call DialogProvider. This component will manage a list of Dialogs in local state.
const DialogContext = React.createContext();
export default function DialogProvider({ children }) {
const [dialogs, setDialogs] = React.useState([]);
return (
<DialogContext.Provider {...}>
{children}
</DialogContext.Provider>
);
}
As you can see, we have an array of dialogs here, it contains the dialog props that will be mapped to the actually <Dialog /> component when rendering.
export default function DialogProvider({ children }) {
const [dialogs, setDialogs] = React.useState([]);
return (
<DialogContext.Provider {...}>
{children}
{dialogs.map((dialog, i) => {
return <DialogContainer key={i} {...dialog} />;
})}
</DialogContext.Provider>
);
}
The <DialogContainer/> is the parent component of the <Dialog/>. Put anything that you want to be reusable in there. Here is a minimum example to get you started.
function DialogContainer(props: DialogContainerProps) {
const { children, open, onClose, onKill } = props;
return (
<Dialog open={open} onClose={onClose} onExited={onKill}>
{children}
</Dialog>
);
}
We can create and remove the dialog using setState as normal.
const [dialogs, setDialogs] = React.useState([]);
const createDialog = (option) => {
const dialog = { ...option, open: true };
setDialogs((dialogs) => [...dialogs, dialog]);
};
const closeDialog = () => {
setDialogs((dialogs) => {
const latestDialog = dialogs.pop();
if (!latestDialog) return dialogs;
if (latestDialog.onClose) latestDialog.onClose();
return [...dialogs].concat({ ...latestDialog, open: false });
});
};
But how do we call them in other components when we defined them here? Well, remember we're using Provider component here, which means we can pass the context data down so other components can reference, in this case we want to pass the createDialog and closeDialog down.
const [dialogs, setDialogs] = React.useState([]);
const createDialog = (option) => {/*...*/};
const closeDialog = () => {/*...*/};
const contextValue = React.useRef([createDialog, closeDialog]);
return (
<DialogContext.Provider value={contextValue.current}>
{children}
{dialogs.map((dialog, i) => ...)}
</DialogContext.Provider>
);
We're almost done here, now we need to add the DialogProvider to the component tree.
export default function App() {
return (
<DialogProvider>
<App {...} />
</DialogProvider>
);
}
But before we can use them, we should create a hook to easily access the context from the parent. So in your DialogProvider.jsx
export const useDialog = () => React.useContext(DialogContext);
Now we can use it like this.
import { useDialog } from "./DialogProvider";
export default function Content() {
const [openDialog, closeDialog] = useDialog();
const onOpenDialog = () => {
openDialog({
children: (
<>
<DialogTitle>This dialog is opened imperatively</DialogTitle>
<DialogContent>Some content</DialogContent>
<DialogActions>
<Button color="primary" onClick={closeDialog}>Close</Button>
</DialogActions>
</>
)
});
};
return (
<Button variant="contained" onClick={onOpenDialog}>
Show dialog
</Button>
);
}
Live Demo
You can play around in the live demo here

How can I set a loading state on a specific element of an array?

I have a list of components coming out of an array of objects. See the code:
const SitesList = () => {
const [loadingState, setLoadingState] = useState(false);
const copySite = (site) => {
setLoadingState(true);
copySiteAction(site)
.then(() => setLoadingState(false))
.catch(() => setLoadingState(false));
};
return filterSites.map((s, i) => (
<div>
{!loadingState ? (
<>
<p>Site: {s.name}</p>
<button onClick={() => copySite(s)}>Copy Site</button>
</>
) : (
<LoadingStateComponent />
)}
</div>
));
};
I want to show the component <LoadingStateComponent /> only on the index/component when I click the Copy Site button. As it is right now, every time I click on that button, all of the components disappear, hence what I see is the <LoadingStateComponent /> until loadingState is set to false.
Any ideas on how can I achieve this?
Rather than having a single "loading" state value, you need one for each item in the array. So, you basically need a second array that's the same size as filterSites to track the state of each index. Or, perhaps a better way would be a simple map linking indices to their corresponding loading state, so that you don't even need to worry about creating an array of X size.
const SitesList = () => {
const [loadingState, setLoadingState] = useState({});
const copySite = (site, index) => {
setLoadingState({...loadingState, [index]: true});
copySiteAction(site)
.then(() => setLoadingState(oldLoadingState => ({...oldLoadingState, [index]: false})))
.catch(() => setLoadingState(oldLoadingState => ({...oldLoadingState, [index]: false})));
};
return filterSites.map((s, i) => (
<div>
{!loadingState[i] ? (
<>
<p>Site: {s.name}</p>
<button onClick={() => copySite(s, i)}>Copy Site</button>
</>
) : (
<LoadingStateComponent />
)}
</div>
));
};
This may or may not solve your problem, but it will solve other problems and its too long for a comment.
This:
const SitesList = () => {
const [loadingState, setLoadingState] = useState(false);
const copySite = (site) => {
setLoadingState(true);
copySiteAction(site)
.then(() => setLoadingState(false))
.catch(() => setLoadingState(false));
};
return filterSites.map((s, i) => (
<div>
{!loadingState ? (
<>
<p>Site: {s.name}</p>
<button onClick={() => copySite(s)}>Copy Site</button>
</>
) : (
<LoadingStateComponent />
)}
</div>
));
};
Needs to look more like this:
const SitesList = () => {
const [loadingState, setLoadingState] = useState(false);
useEffect(() => {
if (loadingState) {
apiCall()
.then(whatever)
.catch(whatever)
.finally(() => setLoadingState(false));
}
}, [loadingState]);
return filterSites.map((s, i) => (
<div key={s.id}> // Note the key
{!loadingState ? (
<>
<p>Site: {s.name}</p>
<button onClick={() => setLoadingState(true)}>Copy Site</button>
</>
) : (
<LoadingStateComponent />
)}
</div>
));
};
React assumes that your pure functional component is a pure function. If you violate that assumption it is under no obligation to respect your side-effects or render them when you'd expect. You need to wrap any side-effects (like a xhr request) in a useEffect hook, and like all hooks it must be called unconditionally. The way I've written it the hook will be called every time loadingState changes and will make the call if it is true, you may need to tweak for your actual use case.
Additionally, items rendered from an array need a unique key prop, I'm assuming your sites have ids but you will need to figure out a way to generate one if not.

Categories

Resources