Checkbox not unchecked when list rerender - javascript

I have array of objects, objects have few property with "checked" property, and when i clicked on checkbox is checked, but when i switch for another list this check is stays checked , example: click maybe you know a universal way to use checkbox?
I try use Checkbox from MUI, with attribute OnChange, example:
<Checkbox className={classes.check} onChange={() => item.checked = !item.checked} size="small" color="primary"/>
I'm going through an array of objects and the item is an object that has the checked property

Try like this(take as an example), it might work:
Your list:
const list = [
{
id: 1,
...rest data
},
{
id: 2,
...rest data
},
{
id: 3,
...rest data
}
];
Your onChange Function and take a one array state and store every checked id in that state and use it for showing checkbox:
const [selectedEntryIds, setSelectedEntryIds] = useState([]);
const onSelect = (id) => {
if (!selectedEntryIds.includes(id)) {
setSelectedEntryIds([...selectedEntryIds, id]);
} else {
setSelectedEntryIds([...selectedEntryIds.filter(item => item !== id)]);
}
};
Your checkbox code (Assume your mapping with list and sending id):
<Checkbox
showCheckIcon
name={`checkbox-${item.id}`}
onChange={() => onSelect(item.id)}
checked={selectedEntryIds?.includes(item.id)}
/>

Related

How to work with multiple checkboxes in react and collect the checked checkboxes

I'm currently working on a filter component where multiple checkboxes can be selected.
Now I want to toggle the state of the checkboxes (which is currently possible and works) and store the checked checkboxes in an array.
If a checkbox is unchecked, it should of course be removed from the array. I've tried the useState hook, but without success --> The checkboxes are added multiple times and the unchecked ones are not removed..
Here is the current status:
// Here I'm creating an array with a fixed size (based on the received data)
const [checkboxState, setCheckboxState] = useState(new Array(receivedData.length).fill(false));
// With this useState I wan't to collect the checked checkboxes
const [checkedCheckboxes, setCheckedCheckboxes] = useState([]);
// This is my handler method that gets triggered when a checkbox get's checked/unchecked
// ..and toggles the state of the checkbox
const handleCheckboxState = (position: number) => {
const updatedCheckedState = checkboxState.map((item, index) => (index === position ? !item : item));
setCheckboxState(updatedCheckedState);
collectCheckedCheckboxes();
};
// With this method I wan't to push the checked checkboxes into the array
// ..and remove the unchecked ones
const collectCheckedCheckboxes = () => {
checkboxState.map((item, index) => {
if (item === true) {
return checkedCheckboxes.push(receivedData[index]);
} else {
return checkedCheckboxes.slice(index, 1);
}
});
};
The checkboxes are rendered like this:
<div className="checkboxes">
{receivedData?.map((data, index) => (
<CheckBox
value={data.value}
checked={checkboxState[index]}
onChange={() => handleCheckboxState(index)}
/>
))}
</div>
What am I doing wrong?
Your CheckBox-component does not contain a key property. This is helpful for React to identify which items have changed, are added, or are removed.
Source: https://reactjs.org/docs/lists-and-keys.html
I also do not understand why you have two states, checkboxState and checkedCheckboxes. Is there another reason for this? I think this would be easier with a single state which holds the indexes (or values) of the checked checkboxes.
[update after comments]
The code below is the desired solution by OP to have the selected object values in a React state.
const { useState } = React;
const Checkboxes = () => {
// With this useState I wan't to collect the checked checkboxes
const [checkedCheckboxes, setCheckedCheckboxes] = useState([]);
// This is my handler method that gets triggered when a checkbox get's checked/unchecked
// ..and toggles the state of the checkbox
const handleCheckboxChange = (data) => {
const isChecked = checkedCheckboxes.some(checkedCheckbox => checkedCheckbox.value === data.value)
if (isChecked) {
setCheckedCheckboxes(
checkedCheckboxes.filter(
(checkedCheckbox) => checkedCheckbox.value !== data.value
)
);
} else {
setCheckedCheckboxes(checkedCheckboxes.concat(data));
}
};
const receivedData = [{ value: "A" }, { value: "B" }, { value: "C" }];
return (
<>
<div className="checkboxes">
<h1>Checkboxes:</h1>
{receivedData?.map((data, index) => (
<input
key={`cb-${index}`}
value={data.value}
type="checkbox"
checked={checkedCheckboxes.some(checkedCheckbox => checkedCheckbox.value === data.value)}
onChange={() => handleCheckboxChange(data)}
/>
))}
</div>
<div>
<h1>State:</h1>
<pre>{JSON.stringify(checkedCheckboxes, null, 2)}</pre>
</div>
</>
);
};
ReactDOM.render(<Checkboxes />, document.getElementById("app"));

The question about React.memo and performance optimisation

Suppose I have long list (let's assume there is no pagination yet) where each list item has input and ability to update own value (as a part of collection). Let's say code looks something like that:
const initItems = [
{ id: 0, label: "Hello world" },
...
{ id: 100, label: "Goodby" }
];
function List() {
const [items, setItems] = React.useState([...initItems]);
const handleChange = React.useCallback((e, id) => {
setItems(items.map(item => {
if (item.id === id) {
return {
...item,
label: e.target.value
}
}
return item;
}));
}, [items]);
return (
<ul>
{items.map(({ id, label }) => {
return (
<Item
id={id}
key={id}
label={label}
onChange={handleChange}
/>
)
})}
</ul>
)
}
// Where Item component is:
const Item = React.memo(({ onChange, label, id }) => {
console.log('Item render');
return (
<li>
<input type="text" value={label} onChange={e => onChange(e, id)} />
</li>
)
});
Looks pretty straightforward, right? While wrapping Item component with React.memo() what I wanted to achieve is to avoid re-render of each Item when some of the Item's gets updated. Well, I'm not sure it should works with this strategy, since each Item is a part of collection (items) and when I update any Item then items gets mapped and updated. What I did try - is to write custom areEqual method for Item component, where I do comparison of label value from props:
function areEqual(prev, next) {
return prev.label === next.label;
}
however with this approach the behaviour of updating items breaks down completely and updating next item reset previous updates and so on (I even could not observe any pattern to explain).
So the question: is it possible to avoid re-rendering of every item in such collection while having ability to update value of individual item?
Your problem here that you change callback on each render. So, you change callback, it changes onChange and this, in turn, runs rerender. To avoid it you can use updater function with setState.
const handleChange = React.useCallback((e, id) => {
// I made separate function so it would be easier to read
// You can just write `(items) =>` before your `items.map` and it will work
function updater(items) {
// we have freshest items here
return items.map((item) => {
if (item.id === id) {
return {
...item,
label: e.target.value,
};
}
return item;
});
}
// pass function
setItems(upadter);
// removed items from dependencies
}, []);
This way, your updater function will always get current value of state into parameters, and your props will update for actually updated item. Another solution would be to write custom updater that compares all values, but onChange. This is ok in short term, but this can become complex and cumbersome to maintain.
Here is live example: https://codesandbox.io/s/unruffled-johnson-ubz1l

Select specific checkbox among an array of checkboxes

I have data coming from getTasks() and I store it on tasks
I created a component for each task with their data with a checkbox for each task.
I want to click a checkbox and display only the one I clicked, not them all.
I am using React Native. How can I do this?
thanks.
export default () => {
const [tasks, setTasks] = React.useState([]);
const [checked, setChecked] = React.useState(false);
React.useEffect(() => {
(async () => {
const response = await getTasks();
setTasks(response);
})();
}, []);
return tasks.map(({id, title, description, date}) => (
<View key={id} style={styles.task}>
<CheckBox
value={checked}
onValueChange={() => {
setChecked((prevState) => !prevState);
}}
/>
<View style={styles.taskTextContainer}>
<Text>{title}</Text>
<Text>{description}</Text>
<Text>{date}</Text>
</View>
</View>
));
};
You're passing the same value={checked} to all checkboxes. You can create an array of booleans to accomplish that.
You have two problems in that code, first is the problem you described, the second one is the way you're changing the state of that checkbox.
About the first problem, in order to display the respective state for each task, you should also include a property that defines if the task is marked or not.
First, verify the name of the property thats set if the task is done or not, I'll give the name here as done and done is of type boolean.
// notice the done property
return tasks.map(({id, title, description, date, done}) => (
Then, you should use done to change the checked state of your Checkbox like this:
<Checkbox
value={done}
...
/>
Now you should change a little how you change that checked state.
First, you need to write a function to change the value of done, to fase or true, according by the Checkbox component's state and that's really simple:
const onCheckboxChange = (taskId) => {
// first, let's find the index of the changed task
const index = tasks.findIndex(({ id }) => id === taskId)
// next, let's get the data by the index
const data = tasks[index]
// now, we can just toggle done value like this
data.done = !data.done
// then, let's assign updated data to its own index in tasks array
tasks[index] = data
// finally, we can update the tasks state using a copy of changed tasks
setTasks([...tasks])
}

Calling setState in callback of setState generate a weird bug that prevents input's value to be updated by onChange

I have a list of input to generate dynamically from an array of data I retrieve from an API.
I use .map() on the array to generate each of my input, and set value={this.state.items[i]} and the onChange property (with a modified handleChange to handle change on an array properly).
Now, I set in my constructor this.state = { items: [{}] }, but since I don't know how many items are going to be generate, value={this.state.items[i].value} crash since this.state.items[n] doesn't exist.
The solution is then to set each this.state.items[i] = {} (using Array.push for example) first, and then generate all the inputs.
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
inputs: apiData.map((v, i) => {
return <input key={i} value={this.state.items[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
})
https://jsfiddle.net/qzb17dut/38/
The issue with this approach is that this.state.items doesn't exist yet on value={this.state.items[i].value} and we get the error Cannot read property 'value' of undefined.
Thankfully, setState() comes with a handy second argument that allows to do something only once the state is set. So I tried this:
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
}, () => this.setState({
inputs: apiData.map((v, i) => {
return <input key={i} value={this.state.items[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
}))
https://jsfiddle.net/qzb17dut/39/
(Update: Please have a look at this example that better illustrate the use case: https://jsfiddle.net/jw81uo4y/1/)
Looks like everything should work now right? Well, for some reason, I am having this very weird bug were value= doesn't update anymore like when you forget to set onChange= on an input, but here onChange= is still called, value= is just not updated making the field remaining not editable.
You can see on the jsfiddle the problem for each method. The first one doesn't have the state set yet, which would allow the input to be edited, but crash because the state value was not yet set. And the second method fix the first issue but introduce this new weird bug.
Any idea about what I am doing wrong? Am I hitting the limit of react here? And do you have a better architecture for this use case? Thanks!
What about this approach instead, where you set the state of the API values only and then, generate the input based on the state from the render via Array.prototype.map like so
constructor (props) {
this.state = {items: []}
}
async componentDidMount(){
const apiData = await fetchApiData()
this.setState({items: apiData})
}
handleChange = (value, index) => {
const items = this.state.items;
items[index].value = value;
this.setState({ items });
};
updateState = () => {
const items = this.state.items;
items.push({value: ''}); // default entry on our item
this.setState({ items });
};
// here ur state items is exactly same structure as ur apiData
onSubmit =()=> {
console.log('this is apiData now', this.state.items)
}
render () {
<button onClick={this.updateState}>update state with inputs</button>
<button onClick={this.onSubmit}>Submit</button>
{this.state.items.map((item, index) => (
<input
key={index}
value={item.value}
onChange={e => this.handleChange(e.target.value, index)}
/>
))}
}
here is the codesandbox code for it
https://codesandbox.io/s/icy-forest-t942o?fontsize=14
with this, it will generate the input based on the items on the state, which in turns have the click handler which updates the state.
Well if I understand correctly, apiData is assigned to state.items and then also used to generate the inputs array. That means that for your purpose apiData and state.items are equivalent. Why don't you use the third map argument like:
var apiData = [{ value: "" }, { value: "" }]
this.setState({
items: apiData,
inputs: apiData.map((v, i, arr) => {
return <input key={i} value={arr[i].value}
onChange={(e) => this.handleChangeArray(e, i)} />
})
});
or the apiData array directly?

How to handle state on array of checkboxes?

Is there a way to handle the checked state of an array of checkboxes?
I have this array:
const CheckboxItems = t => [
{
checked: true,
value: 'itemsCancelled',
id: 'checkBoxItemsCancelled',
labelText: t('cancellations.checkBoxItemsCancelled'),
},
{
checked: true,
value: 'requestDate',
id: 'checkboxRequestDate',
labelText: t('cancellations.checkboxRequestDate'),
},
{
checked: true,
value: 'status',
id: 'checkboxStatus',
labelText: t('cancellations.checkboxStatus'),
},
{
checked: true,
value: 'requestedBy',
id: 'checkboxRequestedBy',
labelText: t('cancellations.checkboxRequestedBy'),
},
];
And I am using it here:
class TableToolbarComp extends React.Component {
state = {
isChecked: true,
};
onChange = (value, id, event) => {
this.setState(({ isChecked }) => ({ isChecked: !isChecked }));
};
render() {
const { isChecked } = this.state;
return (
{CheckboxItems(t).map(item => (
<ToolbarOption key={item.id}>
<Checkbox
id={item.id}
labelText={item.labelText}
value={item.value}
checked={isChecked}
onChange={this.onChange}
/>
</ToolbarOption>
))}
)
}
}
The problem I am having is that every time I unchecked one, the rest of them get unchecked too. I need to manage the state separately to send some information to other components through a redux action.
EDIT:
This is the UI library I am using
You're using the container's isChecked as the state for all of your checkboxes, using a method on your container to flip that one flag that it applies to all of them (isChecked).
Instead, either:
Give the checkboxes themselves state, rather than making them simple objects, or
Maintain a state map in the container keyed by the checkbox item (or perhaps its name)
I would lean toward #1, which I think would look like this with that library:
class TableToolbarComp extends React.Component {
state = {
items: CheckboxItems(t) // Your code seems to have a global called `t`
};
onChange = (value, id, event) => {
this.setState(({ items }) => {
// Copy the array
items = items.slice();
// Find the matching item
const item = items.find(i => i.id === id);
if (item) {
// Update its flag and set state
item.checked = !item.checked;
return { items };
}
});
};
render() {
const { items } = this.state;
return (
{items.map(item => (
<ToolbarOption key={item.id}>
<Checkbox
id={item.id}
labelText={item.labelText}
value={item.value}
checked={item.checked}
onChange={this.onChange}
/>
</ToolbarOption>
))}
)
}
}
Changes:
Call CheckboxItems once, keep the result as state.
In onChange, find the relevant checkbox by id (the lib passes the id) and flip its checked flag
In render, get the items from state and for each item, use its checked flag, not your `isChecked (which I've removed entirely

Categories