ReactJS: Manage multiple checkbox inputs with useState - reactjs

I have the following example component that uses multiple checkboxes for choosing what items to remove from a list of objects:
import React, { useState } from "react";
import "./styles.css";
const data = [
{
name: "test1",
result: "pass"
},
{
name: "test2",
result: "pass"
},
{
name: "test3",
result: "pass"
},
{
name: "test4",
result: "pass"
},
{
name: "test5",
result: "pass"
}
];
export default function App() {
const [allChecked, setAllChecked] = useState(false);
const [isChecked, setIsChecked] = useState({});
const [formData, setFormData] = useState(data);
const handleAllCheck = e => {
setAllChecked(e.target.checked);
};
const handleSingleCheck = e => {
setIsChecked({ ...isChecked, [e.target.name]: e.target.checked });
};
const onDelete = () => {
console.log(isChecked);
const newData = data.filter(
item => !Object.keys(isChecked).includes(item.name)
);
console.log(newData);
setFormData(newData);
};
return (
<div className="App">
<div>
<label>All</label>
<input
name="checkall"
type="checkbox"
checked={allChecked}
onChange={handleAllCheck}
/>
<label />
</div>
{formData.map((test, index) => (
<div key={index}>
<label>{test.name}</label>
<input
type="checkbox"
name={test.name}
checked={allChecked ? true : isChecked[test.name]}
onChange={handleSingleCheck}
/>
</div>
))}
<button onClick={() => onDelete()}>DELETE</button>
</div>
);
}
This is mostly working, except for check all. It seems onChange will not update while using useState. I need to be able to select all the objects or uncheck some to mark for deletion.
Any help is greatly appreciated.
CodeSandbox Example: https://codesandbox.io/s/modest-hodgkin-kryco
UPDATE:
Okay, after some help from Richard Matsen,
Here is a new solution without direct DOM manipulation:
import React, { useState, useEffect } from "react";
import "./styles.css";
const data = [
{
name: "test1",
result: "pass"
},
{
name: "test2",
result: "pass"
},
{
name: "test3",
result: "pass"
},
{
name: "test4",
result: "pass"
},
{
name: "test5",
result: "pass"
}
];
export default function App() {
const [allChecked, setAllChecked] = useState(false);
const [isChecked, setIsChecked] = useState();
const [loading, setLoading] = useState(true);
const [formData, setFormData] = useState(data);
const handleAllCheck = e => {
setAllChecked(e.target.checked);
};
const handleSingleCheck = e => {
setIsChecked({ ...isChecked, [e.target.name]: e.target.checked });
};
const onDelete = () => {
const itemList = Object.keys(isChecked).map((key:any) => {
if (isChecked[key] === true) {
return key
}
})
const result = formData.filter((item:any) => !itemList.includes(item.name))
console.log(result)
setFormData(result)
}
useEffect(() => {
if (!loading) {
setIsChecked(current => {
const nextIsChecked = {}
Object.keys(current).forEach(key => {
nextIsChecked[key] = allChecked;
})
return nextIsChecked;
});
}
}, [allChecked, loading]);
useEffect(() => {
const initialIsChecked = data.reduce((acc,d) => {
acc[d.name] = false;
return acc;
}, {})
setIsChecked(initialIsChecked)
setLoading(false)
}, [loading])
return (
<div className="App">
<div>
<label>All</label>
<input
name="checkall"
type="checkbox"
checked={allChecked}
onChange={handleAllCheck}
/>
<label />
</div>
{!loading ? formData.map((test, index) => (
<div key={index}>
<label>{test.name}</label>
<input
type="checkbox"
name={test.name}
checked={isChecked[test.name]}
onChange={handleSingleCheck}
/>
</div>
)): null}
<button onClick={() => onDelete()}>DELETE</button>
</div>
);
}
codesandbox of working solution:
https://codesandbox.io/s/happy-rubin-5zfv3

The basic problem is checked={allChecked ? true : isChecked[test.name]} stops the unchecking action from happening - once allChecked is true it does not matter what value isChecked[test.name] has, the expression is always going to be true.
You should rely only on isChecked for the value, and treat changing allChecked as a side-effect.
useEffect(() => {
setIsChecked(current => {
const nextIsChecked = {}
Object.keys(current).forEach(key => {
nextIsChecked[key] = allChecked;
})
return nextIsChecked;
});
}, [allChecked]);
...
{formData.map((test, index) => (
<div key={index}>
<label>{test.name}</label>
<input
type="checkbox"
name={test.name}
checked={isChecked[test.name]}
onChange={handleSingleCheck}
/>
</div>
))}
There's also this warning cropping up
Warning: A component is changing an uncontrolled input of type checkbox to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.
So that's basically saying don't initialize isChecked to {}, because the input's checked property is initially undefined. Use this instead,
{
test1: false,
test2: false,
test3: false,
test4: false,
test5: false,
}
or this way
const data = { ... }
const initialIsChecked = data.reduce((acc,d) => {
acc[d.name] = false;
return acc;
}, {})
export default function App() {
const [allChecked, setAllChecked] = useState(false);
const [isChecked, setIsChecked] = useState(initialIsChecked);
...

The problem with your code was how you were handling allChecked. I have made some changes to your code and it works now.
const data = [
{
name: "test1",
result: "pass"
},
{
name: "test2",
result: "pass"
},
{
name: "test3",
result: "pass"
},
{
name: "test4",
result: "pass"
},
{
name: "test5",
result: "pass"
}
];
function App() {
const [allChecked, setAllChecked] = useState(false);
// using an array to store the checked items
const [isChecked, setIsChecked] = useState([]);
const [formData, setFormData] = useState(data);
const handleAllCheck = e => {
if (allChecked) {
setAllChecked(false);
return setIsChecked([]);
}
setAllChecked(true);
return setIsChecked(formData.map(data => data.name));
};
const handleSingleCheck = e => {
const {name} = e.target;
if (isChecked.includes(name)) {
setIsChecked(isChecked.filter(checked_name => checked_name !== name));
return setAllChecked(false);
}
isChecked.push(name);
setIsChecked([...isChecked]);
setAllChecked(isChecked.length === formData.length)
};
const onDelete = () => {
const data_copy = [...formData];
isChecked.forEach( (checkedItem) => {
let index = formData.findIndex(d => d.name === checkedItem)
delete data_copy[index]
}
)
setIsChecked([])
// filtering out the empty elements from the array
setFormData(data_copy.filter(item => item));
setAllChecked(isChecked.length && isChecked.length === data.length);
};
return (
<div className="App">
<form>
<label>All</label>
<input
name="checkall"
type="checkbox"
checked={allChecked}
onChange={handleAllCheck}
/>
{ formData.map((test, index) => (
<div
key={index}
>
<label>{test.name}</label>
<input
type="checkbox"
name={test.name}
checked={isChecked.includes(test.name)}
onChange={handleSingleCheck}
/>
</div>
))
}
<label />
</form>
<button onClick={onDelete}>DELETE</button>
</div>
);
}

I think you should merge allChecked and isChecked state vars, because they represent the same thing, but your denormalizing it by creating two different vars! I suggest to keep isChecked, and modify all its entries when you press the allChecked input. Then, you can use a derived var allChecked (defined in your component or by using useMemo hook) to know if all your checks are checked or not.

Well, after some time working I came up with:
import React, { useState } from "react";
import "./styles.css";
import { useFormInputs } from "./checkHooks";
const data = [
{
name: "test1",
result: "pass"
},
{
name: "test2",
result: "pass"
},
{
name: "test3",
result: "pass"
},
{
name: "test4",
result: "pass"
},
{
name: "test5",
result: "pass"
}
];
export default function App() {
const [fields, handleFieldChange] = useFormInputs({
checkedAll: false
});
const allcheck = () => {
const checkdata = document.querySelectorAll(".checkers").length;
const numChecks = Array.from(new Array(checkdata), (x, i) => i);
numChecks.map(item => {
console.log(item);
async function checkThem() {
let element = await document.getElementsByClassName("checkers")[item];
element.click();
}
return checkThem();
});
};
return (
<div className="App">
<div>
<label>All</label>
<input name="checkall" type="checkbox" onChange={allcheck} />
<label />
</div>
{data.map((test, index) => (
<div key={index}>
<label>{test.name}</label>
<input
className="checkers"
type="checkbox"
name={test.name}
onChange={handleFieldChange}
/>
</div>
))}
</div>
);
}
Relevent codesandbox: https://codesandbox.io/s/admiring-waterfall-0vupo
Any suggestions welcomed. Also, thanks for the help guys!

Related

How to apply multiple filters in React for e-commerce app

I am creating an e-commerce app where I am trying to apply filters e.g. filter by gender and/or brands. The filters only work individually and if I try to click both, the other filter cancels.
Please help! thank you in advance.
I am creating an e-commerce app where I am trying to apply filters e.g. filter by gender and/or brands. The filters only work individually and if I try to click both, the other filter cancels.
Please help! thank you in advance.
Sorry about the duplicate of question, Stackoverflow kept stopping me from posting because I have too much codes instead of description.
import React, { useState, useEffect } from 'react';
import './Clothing.css'
import data from '../../data/data2.json';
const Clothing = () => {
const [items, setItems] = useState([]);
const [color, setColor] = useState(null);
const types = [
{ id: 11, value: 'All' },
{ id: 22, value: 'Cap' },
{ id: 33, value: 'Sweatshirt' }
]
const genders = [
{ id: 55, value: 'Men' },
{ id: 66, value: 'Women' }
]
const brands = [
{ value: 'Graver' },
{ value: 'LMC' }
]
useEffect(() => {
setItems(data);
}, [])
const handleGender = (e) => {
const filteredItems = data.filter((d) => {
return d.gender === e.target.value
})
setItems(filteredItems)
}
const handleBrand = (e) => {
const filteredItems = data.filter((d) => {
return d.brand === e.target.value
})
setItems(filteredItems)
}
return (
<div className="bodyDiv">
<div className="leftMenu">
<div className='leftList'>
<h3>Gender</h3>
{
genders.map((g) =>
<button
id={g.id}
value={g.value}
onClick={(e) => {
handleGender(e);
handleColor(e);
}}
>{g.value}</button>
)
}
</div>
<div className='leftList'>
<h3>Brand</h3>
{
brands.map((b) =>
<button
id={b.id}
value={b.value}
onClick={(e) => {
handleBrand(e);
}}
>{b.value}</button>
)
}
</div>
<div className='itemSection'>
<div className='itemCount'>
Total {items.length}
</div>
<div className="itemLists">
{items.length === 0
? "No items found"
:
items.map((item) =>
<div className="itemCard" key={item.id}>
<img src={item.img} alt='clothes' />
<div className="itemText">
<h4>{item.brand}</h4>
<h4>{item.gender}</h4>
</div>
</div>
)}
</div>
</div>
</div>
)
}
export default Clothing
I have solved this problem. Message me if you need help.

How to update dynamic multiple input (user can add those input themself)?

I have a form. Initially there is some default values (user name and address). When user click add, there is an extra input which user can enter another name and address, and the extra name and address will store in additionConfigs.
Example:
https://codesandbox.io/s/elastic-pateu-2uy4rt
import "./styles.css";
import { useState, useEffect } from "react";
export default function App() {
const [value, setValue] = useState([]);
const [additionConfigs, setAdditionConfigs] = useState([]);
useEffect(() => {
setTimeout(() => {
setValue([
{
id: 1,
baseName: "XXX",
config: {
name: "Kenny",
address: "New york"
}
},
{
id: 2,
baseName: "YYY",
config: {
name: "Ben",
address: "Boston"
}
},
{
id: 3,
baseName: "ZZZ",
config: {
name: "Mary",
address: "Los Angeles"
}
}
]);
}, 1000);
}, []);
const onAddBaseConfig = (item) => {
setAdditionConfigs((preValue) => [
...preValue,
{
id: item.id,
config: {
name: "",
address: ""
}
}
]);
};
console.log(additionConfigs);
const onChangeName = (e, id) => {
setAdditionConfigs((preValue) => {
const newValue = preValue.map((v) => {
if (v.id === id) {
return {
...v,
config: {
...v.config,
name: e.target.value
}
};
}
return v;
});
return newValue;
});
};
const onChangeAddress = (e, id) => {
setAdditionConfigs((preValue) => {
const newValue = preValue.map((v) => {
if (v.id === id) {
return {
...v,
config: {
...v.config,
address: e.target.value
}
};
}
return v;
});
return newValue;
});
};
return (
<>
{value.length > 0 &&
value.map((v, index) => (
<div className="item" key={index}>
<div className="item">
{v.config.name}
{v.config.address}
{additionConfigs.length > 0 &&
additionConfigs
.filter((config) => config.id === v.id)
.map((config) => (
<div>
<label>name</label>
<input
value={config.config.name}
onChange={(e) => onChangeName(e, config.id)}
/>
<label>address</label>
<input
value={config.config.address}
onChange={(e) => onChangeAddress(e, config.id)}
/>
</div>
))}
</div>
<button onClick={() => onAddBaseConfig(v)}>Add</button>
</div>
))}
</>
);
}
Currently, I use config.id to update the extra name and address, but there is an issue that if user add two or more extra name and address input, when updating the first one, the second will update, too.
How do I update respectively? Giving each group of input a flag?
Assuming that the component should not modify the base value as it is set by a useEffect, but keep a additionConfigs which need to support any amount of config inputs, perhaps one solution could be to make additionConfigs state an object.
The additionConfigs object could have id from base value as key and an array of configs as value, and perhaps each config need its own id, so that they can be controlled by the added input, without major refactor of the existing code structure.
Forked live with modifications: codesandbox
Perhaps try the following as an example:
Define additionConfigs state as an object:
const [additionConfigs, setAdditionConfigs] = useState({});
Update logic for additionConfigs when adding a config input:
(The id logic here is only adding previous id++, and should probably be replaced by a unique id generator in actual project)
const onAddBaseConfig = (item) => {
setAdditionConfigs((preValue) => {
const preConfigs = preValue?.[item.id];
const newId = preConfigs
? preConfigs.reduce((acc, cur) => (cur.id > acc ? cur.id : acc), 0) + 1
: 1;
return {
...preValue,
[item.id]: preConfigs
? [
...preConfigs,
{
id: newId,
config: {
name: "",
address: ""
}
}
]
: [
{
id: newId,
config: {
name: "",
address: ""
}
}
]
};
});
};
Update logic for a config input for name, a baseId is added as an argument as each base value can have multiple configs:
const onChangeName = (e, id, baseId) => {
setAdditionConfigs((preValue) => {
const newArr = preValue[baseId].map((v) => {
if (v.id === id) {
return {
...v,
config: {
...v.config,
name: e.target.value
}
};
}
return v;
});
return { ...preValue, [baseId]: newArr };
});
};
Same but for address:
const onChangeAddress = (e, id, baseId) => {
setAdditionConfigs((preValue) => {
const newArr = preValue[baseId].map((v) => {
if (v.id === id) {
return {
...v,
config: {
...v.config,
address: e.target.value
}
};
}
return v;
});
return { ...preValue, [baseId]: newArr };
});
};
Output with the changes:
<>
{value.length > 0 &&
value.map((v, index) => (
<div className="item" key={index}>
<div className="item">
{v.config.name}
{v.config.address}
{additionConfigs?.[v.id] &&
additionConfigs?.[v.id].length > 0 &&
additionConfigs?.[v.id].map((config, index) => (
<div key={config.id}>
<label>name</label>
<input
value={config.config.name}
onChange={(e) => onChangeName(e, config.id, v.id)}
/>
<label>address</label>
<input
value={config.config.address}
onChange={(e) => onChangeAddress(e, config.id, v.id)}
/>
</div>
))}
</div>
<button onClick={() => onAddBaseConfig(v)}>Add</button>
</div>
))}
</>

Betton are not disappear when removing atomFamily recoil.js

Why when user click "remove button" in ListItem component, the item text are disappear but the button itself are still there, if possible how to get rid of that "remove button" too?
P.S The atom family item are got removed but the ui are not get updated ("remove button" are still there), is that a normal things?
import React, {useState} from "react";
import { atom, useRecoilState, useResetRecoilState, useRecoilCallback, atomFamily, useRecoilValue, useSetRecoilState } from "recoil";
import "./styles.css";
const idsState = atom({
key: "circleColor",
default: [],
});
const noteState = atomFamily({
key: "noteState",
default: []
})
const ListItem = ({ id }) => {
const [note, setNote] = useRecoilState(noteState(id));
const handleRemoveNote = useResetRecoilState(noteState(id));
return (
<div key={note.id} className="list-item">
<p>{note.text}</p>
<button onClick={handleRemoveNote}>Remove</button>
</div>
)
}
const App = () => {
const ids = useRecoilValue(idsState);
const nextId = ids.length;
const addNote = useRecoilCallback(({set}) => (newNote) => {
set(idsState, [...ids, nextId])
set(noteState(nextId), newNote);
})
const [text, setText] = useState("");
const handleAddNote = (e) => {
e.preventDefault();
const id = Math.round(Math.random() * 1000);
const newNote = {
text,
id,
subNote: [
{
label: "zero",
value: "0"
},
{
label: "one",
value: "1"
},
{
label: "two",
value: "two"
}
]
};
addNote(newNote);
}
return (
<div>
<form className="form-container" onSubmit={handleAddNote}>
<input onChange={e => setText(e.target.value)} />
<button>Add</button>
</form>
<div>
{ids.map(id => (
<ListItem id={id} />
))}
</div>
</div>
);
};
export default App;

How do I retrieve the selected value from a dropdown menu in ReactJS

I'm trying to get the value from the selected item of two drop down menu (DDM). The first DDM will populate the second DDM base on user input. E.g. first DDM select "langauge", second DDM will only show "c++, java, python, c#". I'm having difficulty in retrieving the value from both drop down menu as string ? Any suggestions ? Many thanks.
Current Code
/** Function that will set different values to state variable
* based on which dropdown is selected
*/
const changeSelectOptionHandler = (event) => {
setSelected(event.target.value);
};
/** Different arrays for different dropdowns */
const algorithm = [
"Searching Algorithm",
"Sorting Algorithm",
"Graph Algorithm",
];
const language = ["C++", "Java", "Python", "C#"];
const dataStructure = ["Arrays", "LinkedList", "Stack", "Queue"];
/** Type variable to store different array for different dropdown */
let type = null;
/** This will be used to create set of options that user will see */
let options = null;
/** Setting Type variable according to dropdown */
if (selected === "Algorithm") {
type = algorithm;
} else if (selected === "Language") {
type = language;
} else if (selected === "Data Structure") {
type = dataStructure;
}
/** If "Type" is null or undefined then options will be null,
* otherwise it will create a options iterable based on our array
*/
if (type) {
options = type.map((el) => <option key={el}>{el}</option>);
}
return (
<div>
<form>
<div>
{/** Bind changeSelectOptionHandler to onChange method of select.
* This method will trigger every time different
* option is selected.
*/}
<select onChange={changeSelectOptionHandler}>
<option>Choose...</option>
<option>Algorithm</option>
<option>Language</option>
<option>Data Structure</option>
</select>
</div>
<div>
<select>
{
options
}
</select>
</div>
</form>
</div>
);
};
export default App;
Try this approach,
import { useEffect, useState } from "react";
export default function App() {
const [selected, setSelected] = useState("");
const [type, setType] = useState([]);
const [selected2, setSelected2] = useState("");
const algorithm = [
"Searching Algorithm",
"Sorting Algorithm",
"Graph Algorithm"
];
const language = ["C++", "Java", "Python", "C#"];
const dataStructure = ["Arrays", "LinkedList", "Stack", "Queue"];
const changeSelectOptionHandler = (event) => {
setSelected(event.target.value);
};
const changeDp2Handle = (event) => {
setSelected2(event.target.value);
};
useEffect(() => {
let type = [];
if (selected === "Algorithm") {
type = algorithm;
} else if (selected === "Language") {
type = language;
} else if (selected === "Data Structure") {
type = dataStructure;
}
setSelected2("");
setType(type);
}, [selected]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<select onChange={changeSelectOptionHandler}>
<option>Choose...</option>
<option>Algorithm</option>
<option>Language</option>
<option>Data Structure</option>
</select>
</div>
<hr />
<div>
<select onChange={changeDp2Handle}>
<option>Choose...</option>
{type.map((v) => (
<option key={v}>{v}</option>
))}
</select>
{selected2}
</div>
</div>
);
}
codesandbox - https://codesandbox.io/s/jovial-resonance-joqlo?file=/src/App.js:0-1208
I would suggest to do something like so
import { useState } from "react";
import "./styles.css";
const Select = ({ options, fieldName }) => {
const [value, setValue] = useState();
return (
<select
name={fieldName}
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
>
{options.map(({ value, label }, i) => {
return (
<option key={i} value={value}>
{label}
</option>
);
})}
</select>
);
};
const dataStructureOptions = [
{
label: "Arrays",
value: "Arrays"
},
{
label: "LinkedList",
value: "LinkedList"
},
{
label: "Stack",
value: "Stack"
},
{
label: "Queue",
value: "Queue"
}
];
const languageOptions = [
{
label: "C++",
value: "C++"
},
{
label: "Java",
value: "Java"
},
{
label: "Python",
value: "Python"
},
{
label: "C#",
value: "C#"
}
];
const filterOptions = [
{
label: "Choose...",
value: "Choose..."
},
{
label: "Algorithm",
value: "Algorithm"
},
{
label: "Language",
value: "Language"
},
{
label: "Data Structure",
value: "Data Structure"
}
];
const mappedSelectedOptions = {
Language: languageOptions,
"Data Structure": dataStructureOptions
};
const FILTER_FIELD_NAME = "filter";
export default function App() {
const [currentSelectedFilter, setCurrentSelectedFilter] = useState();
const onFormChange = (e) => {
const fieldName = e.target.name;
if (fieldName === FILTER_FIELD_NAME) {
setCurrentSelectedFilter(e.target.value);
}
};
const onSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
console.log("onSubmit", formData);
};
const currentMappedOptions = mappedSelectedOptions[currentSelectedFilter];
return (
<div>
<form onChange={onFormChange} onSubmit={onSubmit}>
<Select options={filterOptions} fieldName={FILTER_FIELD_NAME} />
{currentMappedOptions && (
<Select options={currentMappedOptions} fieldName="options" />
)}
<button>Submit</button>
</form>
</div>
);
}

I am trying to change the input value but its not changing, may i know the reason

I am trying to change the input value but its not changing.
Onchange the values are not changing
May i know the the reason
any suggestion?
please refer below snippet
// snippets
import React, {useState} from 'react';
const StackOverFlow = () => {
let rowData = [
{ header: "first" },
{ header: "second" },
{ header: "third" }
];
const [name, setName] = useState({ fn: "test" });
const [data, setData] = useState(rowData);
const getOnchange = (e) => {
console.log('--e--',e.target.value)
setName({ ...name, fn: e.target.value })
}
let updateValue = () => {
setData([
...data,
{
header: (
<input
type="text"
value={name.fn}
onChange={getOnchange}
/>
)
}
]);
};
return (
<div>
{data.map(val => (
<h6>{val.header}</h6>
))}
<button onClick={updateValue}> Click </button>
</div>
);
};
export default StackOverFlow
The value is not changing because once you set a JSX in state data it will take the value during you call setData and does not change when getOnChange is executed,
import React, { useState } from "react";
const StackOverFlow = () => {
let rowData = [
{ header: "first" },
{ header: "second" },
{ header: "third" }
];
const [name, setName] = useState({ fn: "test" });
const [data, setData] = useState(rowData);
const getOnchange = e => {
console.log("--e--", e.target.value);
setName({ ...name, fn: e.target.value });
};
let updateValue = () => {
setData([
...data,
{
header: name.fn
}
]);
setName({ ...name, fn: e.target.value });
};
return (
<div>
{data.map(val => (
<h6 key={val.header}>{val.header}</h6>
))}
<input type="text" value={name.fn} onChange={getOnchange} />
<button onClick={updateValue}> Click </button>
</div>
);
};
export default StackOverFlow;
this is an alternate approach on what you're trying to achieve.

Resources