I am trying to render some array/object values in a Modal with React. The problem is when I trigger the modal, values from all modals are rendered.
I searched and learned that it's because I need to store the value ID in state and then tell react to trigger that specific ID not all. But I don't know how to apply it.
Here's a basic example:
const items = [{id: 1, name: 'John', friends: ['Michael', 'Tom']},{id: 2, name:
'Robert', friends: ['Jim', 'Max']}]
const [show, setShow] = useState(false);
<>
{items.map((i) => {
<div key={i.id}>
<h1>{i.name}</h1>
<button onClick={() => setShow(true)}>View Friends</button>
</div>;
})}
{show ? <div>{items.map((i) => i.friends)}</div> : null} **I know this is wrong**
</>
So I was wondering how can I store the ID to let React know that I want that specific item to render and not all.
Save id in state
const [show, setShow] = useState(null);
...
<button onClick={() => setShow(i.id)}>View Friends</button>
...
{show !== null && <div>{ items.find(i => i.id === show).friends }</div>}
You have to save the selection id in the component state.
const items = [{id: 1, name: 'John', friends: ['Michael', 'Tom']},{id: 2, name:
'Robert', friends: ['Jim', 'Max']}]
const [show, setShow] = useState(false);
const [id, setId] = useState(null);
<>
{items.map((i) => {
<div key={i.id}>
<h1>{i.name}</h1>
// set the user's selection into another local state
<button onClick={() => { setShow(true); setId(i.id)}}>View Friends</button>
</div>;
})}
// filter the current records for the selected id
{show && <div>{items.filter(obj => obj.id === id)?[0].friends}</div>}
</>
First store the selected record in state like
const [selectedRecord,setSelectedRecord] = useState(null);
now when you click on button update selected record like this
<button onClick={() => { setShow(true); setSelectedRecord(i)}}>View Friends</button>
and use it to render inside modal like this
{show ? <div>name :{selectedRecord.name} Friends :{ selected.friends.join(',')}</div> : null}
Finally your code should be like this
const items = [{id: 1, name: 'John', friends: ['Michael', 'Tom']},{id: 2, name:
'Robert', friends: ['Jim', 'Max']}]
const [show, setShow] = useState(false);
const [selectedRecord,setSelectedRecord] = useState(null);
<>
{
items.map((i) => {
<div key={i.id}>
<h1>{i.name}</h1>
// set the user's selection into another local state
<button onClick={() => { setShow(true); setSelectedRecord(i)}}>View Friends</button>
</div>;
})}
{show ? <div>name :{selectedRecord.name} Friends :{ selected.friends.join(',')}</div> : null}
</>
Related
I have cards with categories, clicking on which opens hidden content. I need the card to close when switching between them and if a click occurs behind the content.
import React, { useRef } from "react";
import s from "./Shop.module.css";
import { useState } from "react";
export const Shop = () => {
const card = [
{
name: "Brands",
cat: ["Adidas", "Nike", "Reebok", "Puma", "Vans", "New Balance"],
show: false,
id: 0,
},
{
name: "Size",
cat: ["43", "43,5"],
show: false,
id: 1,
},
{
name: "Type",
cat: ["Sneakers ", "Slippers"],
show: false,
id: 2,
},
];
const [active, setActive] = useState({});
const handleActive = (id) => {
setActive({ ...active, [id]: !active[id] });
};
const handleDisable = (index) => {
setActive(index);
};
return (
<div className={s.container}>
<div className={s.brandInner}>
{card.map((i, index) => {
return (
<div className={s.brandCard} key={i.id}>
<button
className={`${s.brandBtn} `}
onClick={() => handleActive(i.id, index)}
onBlur={() => handleDisable(index)}
>
<p className={`${active[i.id] ? `${s.brandBtnActive}` : ``}`}>
{i.name}
</p>
</button>
<div
className={`${s.openCard} ${active[i.id] ? "" : `${s.dNone}`}`}
>
<ul className={s.brandList}>
{i.cat.map((elem) => {
return (
<li key={elem} className={s.brandItem}>
{elem}
</li>
);
})}
</ul>
<button className={s.brandOpenBtn}>Apply</button>
</div>
</div>
);
})}
</div>
</div>
);
};
I tried to do it through onBlur, but this way I can't interact with the content that appears when opening the card, please help
You could do this a few different ways, here are two.
Array version
You can do this by using a array to keep track of the ids that are active.
const [active, setActive] = useState([]);
For the event handler we will creata a new toggleActive function which replaces the others. This will check if the id is already in the array and if so remove it, else add it.
const toggleActive = (id) => {
setActive((prevActive) => {
if (prevActive.includes(id)) {
return prevActive.filter((activeId) => activeId !== id);
}
return [...prevActive, id];
});
};
Then in the return of the component we need to updated some logic as well. Then handlers only take in the id of the i. To check if the id is in the array with can use includes.
<button
className={s.brandBtn}
onClick={() => toggleActive(i.id)}
>
<p className={`${active.includes(i.id) ? s.brandBtnActive : ""}`}>
{i.name}
</p>
</button>
<div
className={`${s.openCard} ${active.includes(i.id) ? "" : s.dNone}`}
>
Object version
This version is to do it with a object.
const [active, setActive] = useState({});
The handler, this will toggle the value of the id starting with false if there is no value for the id yet.
const toggleActive = (id) => {
setActive((prevActive) => {
const prevValue = prevActive[id] ?? false;
return {
...prevActive,
[id]: !prevValue,
};
});
};
The elements
<button
className={s.brandBtn}
onClick={() => toggleActive(i.id)}
>
<p className={`${active[i.id] ? s.brandBtnActive : ""}`}>
{i.name}
</p>
</button>
<div
className={`${s.openCard} ${active[i.id] ? "" : s.dNone}`}
>
Edit: toggle with closing others
First we declare the state with a initial value of null
const [active, setActive] = useState(null)
We create the toggleActive function which checks if the id to toggle is the previous id, if so return null else return the new active id
const toggleActive = (id) => {
setActive((prevActive) => {
if (prevActive === id) return null;
return id;
});
};
For the rendering it is quite simple, add the toggleActive function to the button and check if the active is the same id
<button
className={s.brandBtn}
onClick={() => toggleActive(i.id)}
>
<p className={`${active === i.id ? s.brandBtnActive : ""}`}>
{i.name}
</p>
</button>
<div
className={`${s.openCard} ${active === i.id ? "" : s.dNone}`}
>
I'm displaying different cars and a button to add or remove the selections the user has made. How do I get the buttons to change state individually? As of now, it changes the state of all the buttons to one value.
const cars = [
{ name: "Benz", selected: false },
{ name: "Jeep", selected: false },
{ name: "BMW", selected: false }
];
export default function App() {
const isJeepSelected = true;
const isBenzSelected = true;
return (
<div className="App">
{cars.map((values, index) => (
<div key={index}>
<Item
isBenzSelected={isBenzSelected}
isJeepSelected={isJeepSelected}
{...values}
/>
</div>
))}
</div>
);
}
const Item = ({ name, isBenzSelected, isJeepSelected }) => {
const [toggle, setToggle] = useState(false);
const handleChange = () => {
setToggle(!toggle);
};
if (isBenzSelected) {
cars.find((val) => val.name === "Benz").selected = true;
}
console.log("cars --> ", cars);
console.log("isBenzSelected ", isBenzSelected);
console.log("isJeepSelected ", isJeepSelected);
return (
<>
<span>{name}</span>
<span>
<button onClick={handleChange}>
{!toggle && !isBenzSelected ? "Add" : "Remove"}
</button>
</span>
</>
);
};
I created a working example using Code Sandbox. Could anyone please help?
There's too much hardcoding here. What if you had 300 cars? You'd have to write 300 boolean useState hook calls, and it still wouldn't be dynamic if you had an arbitrary API payload (the usual case).
Try to think about how to generalize your logic rather than hardcoding values like "Benz" and Jeep. Those concepts are too closely-tied to the arbitrary data contents.
cars seems like it should be state since you're mutating it from React.
Here's an alternate approach:
const App = () => {
const [cars, setCars] = React.useState([
{name: "Benz", selected: false},
{name: "Jeep", selected: false},
{name: "BMW", selected: false},
]);
const handleSelect = i => {
setCars(prevCars => prevCars.map((e, j) =>
({...e, selected: i === j ? !e.selected : e.selected})
));
};
return (
<div className="App">
{cars.map((e, i) => (
<div key={e.name}>
<Item {...e} handleSelect={() => handleSelect(i)} />
</div>
))}
</div>
);
};
const Item = ({name, selected, handleSelect}) => (
<React.Fragment>
<span>{name}</span>
<span>
<button onClick={handleSelect}>
{selected ? "Remove" : "Add"}
</button>
</span>
</React.Fragment>
);
ReactDOM.createRoot(document.querySelector("#app"))
.render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
Consider generating unique ids for elements rather than using indices or assuming the name is unique. crypto.randomUUID() is a handy way to do this.
I have following code.
What I'm trying to do is, in the first step, select one of the elements, store it in my state, and in the last step, console.log all my data. Also, the user can go from the last step to the first and change what he chose before. But the problem is that I can't save what the user selects for the first time.
For example, if the user selects the second one, and then on the last step they go back, then the first one is displayed as selected. How can I fix this?
here is my code
App.js
const [current, setCurrent] = useState(0);
const [data, setData] = useState({
firstName: "AAA",
lastName: "BBB",
age: 26
});
const steps = [
{
content: (
<PackageChoose setCurrent={setCurrent} data={data} setData={setData} />
),
id: 0
},
{
content: <LastStep setCurrent={setCurrent} data={data} />,
id: 1
}
];
return (
<div className="App">
<div>{steps[current].content}</div>
</div>
);
packageChoose (or first step)
const PackageChoose = ({ setCurrent, data, setData }) => {
const [selected, setSelected] = useState(1);
const [packageType, setPackageType] = useState(data.package || "choice");
return (
<div>
<div
onClick={() => {
setPackageType("choice");
setData({ ...data, packageType: packageType });
}}
>
<SelectCard
id={1}
selected={selected}
onSelect={setSelected}
text="text 1"
/>
</div>
<div
onClick={() => {
setPackageType("select");
setData({ ...data, packageType: packageType });
}}
>
<SelectCard
id={2}
selected={selected}
onSelect={setSelected}
text="text 2"
/>
</div>
<button onClick={() => setCurrent(1)}>Next</button>
</div>
);
};
Last step
const LastStep = ({ setCurrent, data }) => {
return (
<div>
LastStep
<button
onClick={() => {
setCurrent(0);
}}
>
Previous
</button>
<button onClick={() => console.log("data===>", data)}> submit </button>
</div>
);
};
Selected Card reusable component
const SelectCard = ({ id, selected, onSelect, text }) => {
const myClassName =
id === selected
? Styles.selectCardWrapperActives
: Styles.selectCardWrapper;
return (
<div className={classNames(myClassName)} onClick={() => onSelect(id)}>
<div> {text} </div>
</div>
);
};
Please help me to fix this problem.
You can move the selected state in PackageChoose to App level.
In App.js define the selected state and pass as props.
export default function App() {
const [selected, setSelected] = useState(1);
...
...
<PackageChoose
...
...
selected={selected}
setSelected={setSelected}
/>
}
In PackageChoose use the props passed above and remove the local selected state.
const PackageChoose = ({ setCurrent, data, setData, setSelected, selected }) => {
You need to update the packageType inside onClick handler. Since setState calls are batched and enqueued inside event handler and state updates may be asynchronous. You can't access the packageType state immediately after setting it.
PackageChoose.js
Card 1
onClick={() => setData({ ...data, packageType: "choice" })}
Card 2
onClick={() => setData({ ...data, packageType: "select" })}
set the packageType directly on data.
I am working my way through the ReactSortable examples (react-sortablejs), but I cant figure out how to prevent adding duplicate objects when cloning from one list to another:
const [state, setState] = useState([
{ id: 0, name: "shrek", ...defs },
{ id: 1, name: "fiona", ...defs }
]);
const [state2, setState2] = useState([
]);
...
<div>
<ReactSortable
group={{ name: "cloning-group-name", pull: "clone" , put: false }}
animation={200}
delayOnTouchStart={true}
delay={2}
list={state}
setList={setState}
clone={item => ({ ...item })}
sort= {false}
>
{state.map(item => (
<div key={item.id}>{item.name}</div>
))}
</ReactSortable>
<ReactSortable
// here they are!
group={{ name: "cloning-group-name"}}
delayOnTouchStart={true}
delay={2}
list={state2}
setList={setState2}
onAdd = { }
>
{state2.map(item => (
<div key={item.id}>{item.name}</div>
))}
</ReactSortable>
I've been trying to use the onAdd, but I just can't figure out the logic. Thanks in advance.
In a React project, I'am calling API to search recipes and load recipe on button click. My intention is to serve single API for both functions. Is there any appropriate solution?
App.js
const [query, setQuery] = useState("porridge");
const [recipes, setRecipes] = useState([]);
const [alert, setAlert] = useState("");
// const [checked, setChecked] = useState(false);
const [radioValue, setRadioValue] = useState('1');
const radios = [
{ name: 'Chicken', value: 'chicken', active: true},
{ name: 'Bread', value: 'bread' },
{ name: 'Fish', value: 'fish' },
{ name: 'Soup', value: 'soup' },
{ name: 'Rice', value: 'rice' },
{ name: 'Meat', value: 'meat' }
];
const url = `https://cors-anywhere.herokuapp.com/https://api.edamam.com/search?
q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}&from=0&to=12`;
{/* Load recipes on submit button click */}
const getData = async () => {
if (query !== "") {
const result = await Axios.get(url);
console.log(result)
if (!result.data.more) {
return setAlert("No food with such name");
}
setRecipes(result.data.hits);
setQuery("");
setAlert("");
} else {
setAlert("Please fill the form");
}
};
const onChange = e => setQuery(e.target.value);
const onSubmit = e => {
e.preventDefault();
getData();
};
{/* Load recipes on radio button click */}
const handleChange = async (e) => {
let checkValue = e.target.value;
if(checkValue) {
const result2 = await Axios.get(`https://cors-anywhere.herokuapp.com/https://api.edamam.com/search?q=${checkValue}&app_id=${APP_ID}&app_key=${APP_KEY}&from=0&to=12`);
setRecipes(result2.data.hits);
}
}
{/* Submit Button */}
<form onSubmit={onSubmit} className="search-form">
{alert !== "" && <Alert alert={alert} />}
<input
type="text"
name="query"
onChange={onChange}
value={query}
autoComplete="off"
placeholder="Search Food"
/>
<input type="submit" value="Search" />
<br/>
</form>
{/* Radio Button */}
<ButtonGroup toggle style={{width:'100%'}}>
{radios.map((radio, idx) => (
<ToggleButton
key={idx}
type="radio"
active="true"
variant="light"
name="radio"
value={radio.value}
checked={radioValue === radio.value}
onChange={(e) => {
handleChange(e);
setRadioValue(e.currentTarget.value)
}}
size="lg"
>
{radio.name}
</ToggleButton>
))}
</ButtonGroup>
<div className="recipes">
{recipes !== [] &&
recipes.map(recipe => <Recipe key={uuidv4()} recipe={recipe} />)}
</div>
As seen from above I've to call API two times, it would be better if same API is used for both functions. I tried to call from main API but query is not updated. Event value which I'am getting from radio button can't be taken as query outside function. So any better solution to tackle?