Toggle button value based on state values in React.js - javascript

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.

Related

Switching between product category cards

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}`}
>

removing object from array of objects - react

Why this does not remove item from the list but only for console.log? As you can see i update list in the function that i assign later to the button.
let DATA = [
{
id: 1,
name: "item1"
},
{
id: 2,
name: "item2"
}
];
const App = () => {
const deleteItem = (id) => {
DATA = DATA.filter((item) => item.id !== id);
console.log(DATA);
};
return (
<div className="App">
{DATA.map((item) => (
<p key={item.id} onClick={() => deleteItem(item.id)}>
{item.name}
</p>
))}
</div>
);
}
const root = ReactDOM.createRoot(
document.getElementById("root")
).render(<App/>);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
It does remove the item from the list. But there's nothing telling the component to re-render the UI.
What you're looking for is state in React. Updating state triggers a component re-render:
const App = () => {
// store the data in state
const [data, setData] = React.useState([
{
id: 1,
name: "item1"
},
{
id: 2,
name: "item2"
}
]);
const deleteItem = (id) => {
// update state with the new data
setData(data.filter((item) => item.id !== id));
};
return (
<div className="App">
{data.map((item) => (
<p key={item.id} onClick={() => deleteItem(item.id)}>
{item.name}
</p>
))}
</div>
);
}
const root = ReactDOM.createRoot(
document.getElementById("root")
).render(<App/>);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
It deletes it but react does not re-render. You have to use a setState method to trigger the re-render, you could put a copy of DATA in a state variable.

React functional component child won't re-render when props change

I'm passing an array of objects to a child as props, and I wanted the child component to re-render when the aray changes, but it doesn't:
parent:
export default function App() {
const [items, setItems] = useState([]);
const buttonCallback = () => {
setItems([...items, { text: "new item" }]);
};
return (
<div className="App">
<h1>Items list should update when I add item</h1>
<button onClick={buttonCallback}>Add Item</button>
<Items />
</div>
);
}
child:
const Items = ({ itemlist = [] }) => {
useEffect(() => {
console.log("Items changed!"); // This gets called, so the props is changing
}, [itemlist]);
return (
<div className="items-column">
{itemlist?.length
? itemlist.map((item, i) => <Item key={i} text={item.text + i} />)
: "No items"}
<br />
{`Itemlist size: ${itemlist.length}`}
</div>
);
};
I found this question with the same issue, but it's for class components, so the solution doesn't apply to my case.
Sandbox demo
<Items propsname={data} />
const buttonCallback = () => {
setItems([...items, { text: "new item" }]);
};
but you should put it as:
const buttonCallback = () => {
setItems([...items, { text: "new item", id: Date.now() }]);
};
Because is not recommended to use index as a key for react children. Instead, you can use the actual date with that function. That is the key for React to know the children has changed.
itemlist.map((item) => <Item key={item.id} text={item.text} />)
Try below:
you are adding array as a dependency, to recognise change in variable, you should do deep copy or any other thing which will tell that useEffect obj is really different.
`
const Items = ({ itemlist = [] }) => {
const [someState,someState]=useState(itemlist);
useEffect(() => {
someState(itemlist)
}, [JSON.stringify(itemlist)]);
return (
<div className="items-column">
{someState?.length
? someState.map((item, i) => <Item key={i} text={item.text
+ i}
/>)
: "No items"}
<br />
{`Itemlist size: ${someState.length}`}
</div>
);
};

How to keep already chosen value in ReactJS

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.

How to implement multiple checkbox using react hook

I want to implement multiple checkboxes on my HTML page using react-hook.
I tried implementing using this URL: https://medium.com/#Zh0uzi/my-concerns-with-react-hooks-6afda0acc672. In the provided link it is done using class component and working perfectly but whenever I am using React hook setCheckedItems to update checkbox checked status it's not re-rendering the view.
The very first time the view is rendering and console.log() is printing from Checkbox component. After clicking on checkbox function handleChange gets called and checkedItems updates the value but the view is not rendering again (no console.log() printing). And {checkedItems.get("check-box-1")} is also not printing any value.
Below is my sample code.
CheckboxExample :
import React, { useState } from 'react';
import Checkbox from '../helper/Checkbox';
const CheckboxExample = () => {
const [checkedItems, setCheckedItems] = useState(new Map());
const handleChange = (event) => {
setCheckedItems(checkedItems => checkedItems.set(event.target.name, event.target.checked));
console.log("checkedItems: ", checkedItems);
}
const checkboxes = [
{
name: 'check-box-1',
key: 'checkBox1',
label: 'Check Box 1',
},
{
name: 'check-box-2',
key: 'checkBox2',
label: 'Check Box 2',
}
];
return (
<div>
<lable>Checked item name : {checkedItems.get("check-box-1")} </lable> <br/>
{
checkboxes.map(item => (
<label key={item.key}>
{item.name}
<Checkbox name={item.name} checked={checkedItems.get(item.name)} onChange={handleChange} />
</label>
))
}
</div>
);
}
export default Example;
Checkbox:
import React from 'react';
const Checkbox = ({ type = 'checkbox', name, checked = false, onChange }) => {
console.log("Checkbox: ", name, checked);
return (<input type={type} name={name} checked={checked} onChange={onChange} /> )
}
export default Checkbox;
I don't think using a Map to represent the state is the best idea.
I have implemented your example using a plain Object and it works:
https://codesandbox.io/s/react-hooks-usestate-xzvq5
const CheckboxExample = () => {
const [checkedItems, setCheckedItems] = useState({}); //plain object as state
const handleChange = (event) => {
// updating an object instead of a Map
setCheckedItems({...checkedItems, [event.target.name] : event.target.checked });
}
useEffect(() => {
console.log("checkedItems: ", checkedItems);
}, [checkedItems]);
const checkboxes = [
{
name: 'check-box-1',
key: 'checkBox1',
label: 'Check Box 1',
},
{
name: 'check-box-2',
key: 'checkBox2',
label: 'Check Box 2',
}
];
return (
<div>
<lable>Checked item name : {checkedItems["check-box-1"]} </lable> <br/>
{
checkboxes.map(item => (
<label key={item.key}>
{item.name}
<Checkbox name={item.name} checked={checkedItems[item.name]} onChange={handleChange} />
</label>
))
}
</div>
);
}
EDIT:
Turns out a Map can work as the state value, but to trigger a re-render you need to replace the Map with a new one instead of simply mutating it, which is not picked by React, i.e.:
const handleChange = (event) => {
// mutate the current Map
checkedItems.set(event.target.name, event.target.checked)
// update the state by creating a new Map
setCheckedItems(new Map(checkedItems) );
console.log("checkedItems: ", checkedItems);
}
but in this case, I think there is no benefit to using a Map other than maybe cleaner syntax with .get() and .set() instead of x[y].
As an alternative to Map, you might consider using a Set. Then you don't have to worry about initially setting every item to false to mean unchecked. A quick POC:
const [selectedItems, setSelectedItems] = useState(new Set())
function handleCheckboxChange(itemKey: string) {
// first, make a copy of the original set rather than mutating the original
const newSelectedItems = new Set(selectedItems)
if (!newSelectedItems.has(itemKey)) {
newSelectedItems.add(itemKey)
} else {
newSelectedItems.delete(itemKey)
}
setSelectedItems(newSelectedItems)
}
...
<input
type="checkbox"
checked={selectedItems.has(item.key)}
onChange={() => handleCheckboxChange(item.key)}
/>
Seems a bit of a long way round but if you spread the map out and apply it to a new Map your component will re-render. I think using a Object reference instead of a Map would work best here.
const {useState} = React
const Mapper = () => {
const [map, setMap] = useState(new Map());
const addToMap = () => {
const RNDM = Math.random().toFixed(5)
map.set(`foo${RNDM}`, `bar${RNDM}`);
setMap(new Map([...map]));
}
return (
<div>
<ul>
{[...map].map(([v, k]) => (
<li key={k}>
{k} : {v}
</li>
))}
</ul>
<button onClick={addToMap}>add to map</button>
</div>
);
};
const rootElement = document.getElementById("react");
ReactDOM.render(<Mapper />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
As a supplement to using a single object to hold the state of numerous items, the updates will not occur as expected if updating multiple items within a single render. Newer commits within the render cycle will simply overwrite previous commits.
The solution to this is to batch up all the changes in a single object and commit them all at once, like so:
// An object that will hold multiple states
const [myStates, setMyStates] = useState({});
// An object that will batch all the desired updates
const statesUpdates = {};
// Set all the updates...
statesUpdates[state1] = true;
statesUpdates[state2] = false;
// etc...
// Create a new state object and commit it
setMyStates(Object.assign({}, myStates, statesUpdates));
export default function Input(props) {
const {
name,
isChecked,
onChange,
index,
} = props;
return (
<>
<input
className="popup-cookie__input"
id={name}
type="checkbox"
name={name}
checked={isChecked}
onChange={onChange}
data-action={index}
/>
<label htmlFor={name} className="popup-cookie__label">{name}</label>
</>
);
}
const checkboxesList = [
{name: 'essential', isChecked: true},
{name: 'statistics', isChecked: false},
{name: 'advertising', isChecked: false},
];
export default function CheckboxesList() {
const [checkedItems, setCheckedItems] = useState(checkboxesList);
const handleChange = (event) => {
const newCheckboxes = [...checkedItems];
newCheckboxes[event.target.dataset.action].isChecked = event.target.checked;
setCheckedItems(newCheckboxes);
console.log('checkedItems: ', checkedItems);
};
return (
<ul className="popup-cookie-checkbox-list">
{checkboxesList.map((checkbox, index) => (
<li className="popup-cookie-checkbox-list__item" key={checkbox.name}>
<Input
id={checkbox.name}
name={checkbox.name}
isChecked={checkbox.isChecked}
onChange={handleChange}
index={index}
/>
</li>
))}
</ul>
);
}```

Categories

Resources