ReactJS - Multiple elements sharing the same states - javascript

So I have 4 panels inside this Collapse from ant design and all 4 of them share the same states.
Here's the code for the Collapse:
const CustomCollapse = (props) => {
const [disabled, setDisabled] = useState(true);
const [followed, setFollowed] = useState(false);
const [opened, setOpen] = useState(false);
let [key, setKey] = useState([props.keys]);
useEffect(() => {
setFollowed(props.isFollowed)
}, [props.isFollowed])
const handlePanel= () => setOpen(prev => !prev);
const handlePanelClose = () => props.setShow(prev => !prev);
const combineFunc = () =>{
handlePanel();
handlePanelClose();
}
return (
<StyledCollapse accordian activeKey={props.show ? key : []} onChange={combineFunc}>
<AntCollapse.Panel
{...props}
header={props.header}
showArrow={false}
bordered={false}
key={props.keys}
extra={
<span>
<span style={{float: 'right'}}>
{followed ? <img src={tickIcon} alt="" style={{height:'1.2em', marginLRight:'10px', width:'auto', objectFit:'contain'}} /> : ""}
</span>
<div className={styles.extraContainer}>
{
!opened && !followed && <div id={styles.themeBox}><p>+10</p></div> // show this box
}
{
opened && !followed && <img src={arrowDownIcon} alt="" style={{height:'1.2em', marginLRight:'10px', width:'auto', objectFit:'contain'}} /> // show this icon
}
</div>
</span>
}
>
{props.children}
</AntCollapse.Panel>
</StyledCollapse>
);
};
Here's the code in father component which uses Custom Collapse which is imported as AntCollapse:
<AntCollapse isFollowed={followed} show={show} keys="1" setShow={setShow} id={styles.telegramHeader1} header="Follow XXX on Telegram">
<Row type='flex' align='middle' justify='center'>
<Button href={links} target="_blank" style={buttonStyle1} onClick={() => setClicked(false)}>
<Icon type="link" style={{ color: '#fff' }} theme="outlined" />
Subscribe
</Button>
</Row>
<span className={styles.greyLine}> </span>
<Row type='flex' align='middle' justify='center'>
<Button onClick={() => {handleFollow(); handleShow(); }} style={buttonStyle2} disabled={clicked}>Continue</Button>
</Row>
</AntCollapse>
I have 4 similar AntCollapse and I only show 1 of them here cause all 4 of them use followed and show. And whenever I expand the panel and click continue, all 4 of them are set to followed and show. How do I change the states seperately?

Related

Why React Parent not re-rendering when it's local state it's changed by children

I have a parent component that has local state, and pass the setState function to the children.
The children sucessfully changes the parent local stage with the setState function, but the component does not re-render even after this state has change. If I make any change to the state in the parent component, I can see the rendering of both, the modifications made from the child (confirmin that it was actually modifying the local state), and the change made by the parent. But this re-renderin happens only when the change comes from the same component (the parent). I.e, there's no re-rendering if the change comes form any of the children.
Parent component:
const CustomizeItem = ({ item, i }) => {
const [removed, setRemoved] = useState([]);
const [added, setAdded] = useState([]);
const [price, setPrice] = useState(item.price);
const handleRemovable = (removable) => {
if (removed.indexOf(removable) === -1) {
setRemoved([...removed, removable]);
} else {
let removedCopy = [...removed];
removedCopy.splice(removed.indexOf(removable), 1);
setRemoved(removedCopy);
}
};
useEffect(() => {
console.log(added);
console.log(price);
}, [added, price]);
return (
<div className="border-2 ">
<p>
Personaliza la {item.name} Nro {i + 1}:
</p>
<div>
<p>Remover:</p>
{item.removables.map((r, i) => {
return (
<label>
{r.name}
<input
name={r.name}
type="checkbox"
checked={null}
onChange={() => handleRemovable(r)}
/>
</label>
);
})}
</div>
<div className="flex flex-col">
<p>Añadir:</p>
{item.extras.map((extra, i) => {
return (
<ExtraItem
key={i + 1}
extra={extra}
added={added}
setAdded={setAdded}
removed={removed}
/>
);
})}
</div>
<div>
<p>Resumen: </p>
<div>
{g(item, "Un", "Una", "s")} <strong>"{item.name}"</strong>
{removed.length > 0 && (
<p>
{removed.map((r) => (
<p>✖️ Sin {r.name}</p>
))}
</p>
)}
{added.length > 0 && (
<p>
{added.map((a) => (
<p>✔️ Con extra de {a.name}</p>
))}
</p>
)}
</div>
</div>
<p>Precio: ${price}</p>
<button></button>
</div>
);
};
export default CustomizeItem;
Children:
const ExtraItem = ({ added, setAdded, extra }) => {
const [count, setCount] = useState(0);
const addExtra = (extra) => {
let addedCopy = added;
addedCopy.push(extra);
setAdded(addedCopy);
setCount(count + 1);
};
const removeExtra = (extra) => {
let addedCopy = added;
addedCopy.splice(addedCopy.indexOf(extra), 1);
setAdded(addedCopy);
setCount(count - 1);
};
return (
<div className="flex justify-between">
<p>
{extra.name} - ${extra.price}
</p>
<div className="flex gap-2">
<IconButton
variant="outlined"
color="red"
className="flex-none"
size="sm"
onClick={() => removeExtra(extra)}
>
<i class="fa-sharp fa-solid fa-minus" />
</IconButton>
<Typography variant="h4" className="text-justify text-green-400">
{count}
</Typography>
<IconButton
variant="outlined"
color="green"
className="flex-none"
onClick={() => addExtra(extra)}
size="sm"
>
<i className="fas fa-plus" />
</IconButton>
</div>
</div>
);
};
export default ExtraItem;
Note that the useEffect with the dependency of added (in the parent) isn't triggered when the change comes from any of the children...
You can try copying the array and then setting it to the state. Like this:
let addedCopy = [...added];
addedCopy.push(extra);
setAdded(addedCopy);

Thumbnail click event for antd carousel

Hi all I have following code
const App = () => {
const mediaRef = useRef(null);
const navRef = useRef(null);
const [direction, setDirection] = useState(null);
const onChange = (currentSlide) => {
if (direction === "next") {
mediaRef.current.goTo(currentSlide + 1, false);
} else {
mediaRef.current.goTo(currentSlide - 1, false);
}
};
const handleNext = () => {
setDirection("next");
navRef.current.next();
};
const handlePrev = () => {
setDirection("prev");
navRef.current.prev();
};
const imageList = [ some images array ];
return (
<>
<>
<Carousel
asNavFor={navRef.current}
ref={mediaRef}
>
{imageList?.map((el, id) => (
<ImageWrapper key={id}>
<img src={el} alt={"name"} />
</ImageWrapper>
))}
</Carousel>
{imageList?.length > 1 && (
<>
<Button onClick={handlePrev}>
<LeftOutlined />
</Button>
<Button onClick={handleNext}>
<RightOutlined />
</Button>
</>
)}
</>
{imageList?.length > 1 && (
<Carousel
slidesToShow={3}
centerMode={true}
asNavFor={mediaRef.current}
ref={(carousel) => (navRef.current = carousel)}
beforeChange={onChange}
>
{imageList?.map((el, id) => (
<>
<divkey={id}>
<img src={el} alt={"name"} />
</div>
</>
))}
</Carousel>
)}
<>
<Button onClick={handlePrev}>
<LeftOutlined />
</Button>
<Button onClick={handleNext}>
<RightOutlined />
</Button>
</>
</>
);
};
I am doing following, I am taking antd carousel adding another carousel for thumbnails and putting custom arrows, clicking on next and on previouse buttons it automatically change main image and thumbnail.
Now I am trying to put onClick event on thumbnails, I mean Thumbnails should be clickable and by click, the large image and thumbnail should be changed. How can I achieve that ?
You can add a click handler on the thumbnail with the clicked id as parameter
<ThumbnailWrapper key={id}>
<img
src={el}
alt={"name"}
onClick={() => thumbnailClicked(id)}
/>
</ThumbnailWrapper>
then do something with the id in the defined function:
const thumbnailClicked = (id) => {
console.log("thumbnail clicked:",id);
//then e.g. set parent the same as the thumbnail.
mediaRef.current.goTo(id, false);
};

How to map components and ensure the component has the same attribute individually

Currently I have a map function that render a serial of image, and I realized that they share the same hover state, which means they will perform the same action when hovered. Is there are any standard practice to map duplicate components while assigning them unique/individual properies?
{itemData.map((item) => (
<ImageListItem key={item.img}>
<img
src={item.img}
alt={item.title}
loading="lazy"
onMouseOver={() => {setHover(true)}}
onMouseOut={() => {setHover(false)}}
style={{ transform: hover ? 'scale(1.5, 1.5)' : null }}
/>
<ImageListItemBar
title={item.title}
subtitle={item.author}
actionIcon={
<IconButton
sx={{ color: 'rgba(255, 255, 255, 0.54)' }}
aria-label={`info about ${item.title}`}
>
<InfoIcon />
</IconButton>
}
/>
You should use a component, which create a unique state for each element, i wrote an easy to understand example.
import React, { useState } from "react"
const items = [
{
title: 'Card1',
price: 100
},
{
title: 'Card2',
price: 50
},
{
title: 'Card3',
price: 200
},
]
export default function App() {
return (
<>
{
items.map(element => {
return(
<Card {...element}/>
)
})
}
</>
)
}
function Card({title, price, key}) {
const [isHovered, setHover] = useState(false)
return (
<>
<div
key={key}
onMouseOver={() => {setHover(true)}}
onMouseOut={() => {setHover(false)}}
>
<div>
{title}
</div>
<h3>
{
isHovered && price
}
</h3>
</div>
</>
);
}
I made the card price to show if hovered so you can see it works on each individual component.
Code sandbox if you want to check it out.
To provide unique properties, you need to have something that uniquely identifies your image component and use it to manage your state. In your case, your state hover should be an array or an object, not a boolean. Since you are using item.img as a key, I assume it is unique and hence it can help in your state management like this:
const [hover, setHover] = useState({});
{itemData.map((item) => (
<ImageListItem key={item.img}>
<img
src={item.img}
alt={item.title}
loading="lazy"
onMouseOver={() => setHover({...hover, [item.img]: true})}
onMouseOut={() => setHover({...hover, [item.img]: false})}
style={{ transform: hover ? 'scale(1.5, 1.5)' : null }}
/>
<ImageListItemBar
title={item.title}
subtitle={item.author}
actionIcon={
<IconButton
sx={{ color: 'rgba(255, 255, 255, 0.54)' }}
aria-label={`info about ${item.title}`}
>
<InfoIcon />
</IconButton>
}
/>
))
}
If you want the state to be in the parent without going all the way to an array or object, you can use a number instead. If only one item at a time is going to be active, you can just use the index of the active item as the state:
const { useState } = React;
const things = ["foo", "bar", "baz"];
function Component() {
const [active, setActive] = useState(-1);
const updateActivity = (index) => setActive(index === active ? -1 : index);
return (
<ul>
{things.map((thing, index) => (
<li>
<button key={index} onClick={() => updateActivity(index)}>
{index === active
? <strong>{thing}</strong>
: thing}
</button>
</li>
))}
<li>Value: {active}</li>
</ul>
);
}
ReactDOM.render(
<Component />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Alternatively, in cases where you want multiple items to be simultaneously active, you can use a "bit flag" approach where each bit of the value represents whether or not the corresponding index is active:
const { useState } = React;
const things = ["foo", "bar", "baz"];
function Component() {
const [active, setActive] = useState(0);
const updateActivity = (index) => setActive(active ^ Math.pow(2, index));
return (
<ul>
{things.map((thing, index) => (
<li>
<button key={index} onClick={() => updateActivity(index)}>
{active & Math.pow(2, index)
? <strong>{thing}</strong>
: thing}
</button>
</li>
))}
<li>Value: {active} ({active.toString(2).padStart(3, "0")})</li>
</ul>
);
}
ReactDOM.render(
<Component />,
document.getElementById("react2")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="react2"></div>

How to make rowEvents in react-bootstrap-table-next be called on a button only instead of a row

I am working on a react-bootstrap-table, I set it up in a way when I click on the icon button, a modal pops up displaying the information of the row data. But when I click on another column of a row, the modal also pops up displaying data.
I want a situation whereby the modal only pops up when you click on the icon, instead of the row. This is making the cellEdit function not to be called.
const [modalInfo, setModalInfo] = useState([])
const [show, setShow] = useState(false);
const [showModal, setShowModal] = useState(null);
const rowEvents = {onClick: (e, row) => {
setModalInfo(row);
toggleTrueFalse();
},
};
const toggleTrueFalse = () => setShowModal(handleShow);
Cell Edit
const cellEdit = cellEditFactory({
mode: 'click',
blurToSave: true,})
Modal
const ModalContent = () => {
return (
<>
<Modal isOpen show={show}>
<ModalHeader>Terminal Info</ModalHeader>
<ModalBody>
<ul>
<h6 style={{ fontFamily: 'Georgia' }}>id : {modalInfo.id}</h6>
</ul>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={handleClose}>
<FontAwesomeIcon icon="ban" />
Close
</Button>
</ModalFooter>
</Modal>
</>
);
};
jsx
return (
<>
<div>
<h2 style={{ color: "red", fontFamily: "Georgia" }}>Terminals</h2>
</div>
<BootstrapTable
keyField="id"
data={data}
columns={columns}
cellEdit={cellEdit}
rowEvents={rowEvents}
/>
{show ? <ModalContent /> : null}
</>
);
If I can be able to achieve this, then cellEdit will work fine.

How to get the results to only display the data with a certain label on click?

I have made a rudimentary recipe searching app in React, the data received from the API is displayed in recipe cards in the Recipe component. I want to add buttons which once click filter the results to display the recipes cards with the Vegan healthLabel.
This is the App component which interacts with the API. I'm stuck on how to get the results to only display the data with a certain label on click.
const App = () =>
const APP_ID = '072f4029';
const APP_KEY = '1e1f9dc0b5c22bdd26363da4bbaa74b8';
const [recipes, setRecipes] = useState([]);
const [search, setSearch] = useState('');
const [query, setQuery] = useState('');
useEffect(() => {
getRecipes();
}, [query])
const getRecipes = async () => {
const response = await fetch(`https://api.edamam.com/search?q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}&from=0&to=12`)
const data = await response.json();
setRecipes(data.hits);
}
const updateSearch = e => {
setSearch(e.target.value);
}
const getSearch = e => {
e.preventDefault();
setQuery(search);
setSearch('');
}
const props = useSpring({ opacity: 1, from: { opacity: 0 } })
return (
<div className='App'>
<div className="header">
<div className="logo">
<img className="knife" src={logo} alt="Logo" />
<h1>Recipe Finder</h1>
</div>
</div>
<form onSubmit={getSearch} className="search-form">
<InputGroup>
<InputGroupAddon addonType="prepend">
<InputGroupText><FontAwesomeIcon icon={faSearch} /></InputGroupText>
</InputGroupAddon>
<Input className="search-bar" type="text" placeholder="Search for recipe..." value={search} onChange={updateSearch} />
</InputGroup>
<Button color="primary" size="sm" className="search-button" type="submit">Search</Button>
</form>
<UncontrolledAlert className="alert" color="info">
sambailey.dev
</UncontrolledAlert>
<div style={props} className="recipes">
{recipes.map(recipe => (
<Recipe
key={recipe.recipe.label}
title={recipe.recipe.label}
theUrl={recipe.recipe.url}
image={recipe.recipe.image}
ingredients={recipe.recipe.ingredients}
source={recipe.recipe.source}
healthLabels={recipe.recipe.healthLabels}
servings={recipe.recipe.yield} />
))}
</div>
</div>
);
}
export default App;
This is the Recipe card component
const Recipe = ({ title, theUrl, image, ingredients, source, healthLabels, servings, deleteRecipe }) => {
const [modal, setModal] = useState(false);
const toggle = () => setModal(!modal);
const down = <FontAwesomeIcon icon={faSortDown} />
const zoom = <FontAwesomeIcon onClick={toggle} className={style.maximise} icon={faSearchPlus} />
const Heart = styled(Checkbox)({
position: 'absolute',
top: 1,
right: 1,
});
return (
<div className={style.recipe}>
<Heart className={style.heart} icon={<FavoriteBorder />} checkedIcon={<Favorite />} name="checkedH" />
<div className={style.top}>
<h6>{title}</h6>
<Badge className={style.badge} color="primary">{source}</Badge>
<p>Serves: <Badge color="primary" pill>{servings}</Badge></p>
<div className={style.imageContainer}>
<img onClick={toggle} src={image} alt='food' />
{zoom}
</div>
<Modal isOpen={modal} toggle={toggle}>
<img src={image} alt="" className={style.maxi} />
</Modal>
</div>
<ol className={style.allergens}>
{healthLabels.map(healthLabel => (
<li>{healthLabel}</li>
))}
</ol>
<div className={style.ingr}>
<p className={style.inghead} id="toggler">Ingredients <Badge color="secondary">{ingredients.length}</Badge> {down}</p>
<UncontrolledCollapse toggler="#toggler">
<ol id="myol">
{ingredients.map(ingredient => (
<li className={style.customList}>{ingredient.text}</li>
))}
</ol>
</UncontrolledCollapse>
<Button className={style.button} outline color="primary" size="sm" href={theUrl} target="_blank">Method</Button>
</div>
<div className={style.info}>
<div className={style.share}>
<WhatsappShareButton url={theUrl}><WhatsappIcon round={true} size={20} /></WhatsappShareButton>
<FacebookShareButton url={theUrl}><FacebookIcon round={true} size={20} /></FacebookShareButton>
<EmailShareButton url={theUrl}><EmailIcon round={true} size={20} /></EmailShareButton>
</div>
</div>
</div >
);
}
export default Recipe;
Your useEffect is already dependent on the query property. To trigger a new fetch, you could set the state of the query parameter to the one you want to fetch:
label onclick pseudocode:
export default function Recipe({ onLabelClick, label }) {
return (
<div onClick={onLabelClick}>
{label}
</div>
);
}
You can then load a Recipe like so:
<Recipe
onLabelClick={() => setQuery("what you want your new query to be")
label={recipe.recipe.label}
/>
When clicked, the query property will be updated and the useEffect will be triggered as a result. This will lead to a new fetch!
[EDIT] The OP asked also for an example on how to filter already loaded recipes:
// Let's assume a recipe has a property "title"
const [recipes, setRecipes] = useState([]);
const [filter, setFilter] = useState("");
const [filteredRecipes, setFilteredRecipes] = useState([]);
useEffect(() => {
if (filter) {
const newFilteredRecipes = recipes.filter(recipe => recipe.title.toLowerCase().includes(filter.toLowerCase()));
setFilteredRecipes(newFilteredRecipes);
}
}, [recipes, filter]);
return (
<>
{filteredRecipes.map((recipe, index) => {
return <Recipe
key={index}
onLabelClick={() => setQuery("what you want your new query to be")
label={recipe.recipe.label}
/>
}
}
</>
);

Categories

Resources