How to handle state on array of checkboxes? - javascript

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

Related

React.useMemo is re-rendering all items in array

I have a react component that stores a set of fruits in useState. I have a memoized function (visibleFruits) that filters fruits. I map visibleFruits to the dom.
The problem is, when i check a fruit, all visible fruits re-render.
I am expecting that only the selected one re-renders since it is the only one that is changing.
Is there a way for me to use this pattern but prevent all from re-rendering on check?
In real life, there is a complex function in visibleFruits useMemo. So I can't simply append the filter before the map.
Edit, here is updated edit:
const Example = () => {
const [fruits, setFruits] = React.useState([
{ id: 1, name: 'apple', visible: true, selected: false },
{ id: 2, name: 'banana', visible: false, selected: false },
{ id: 3, name: 'orange', visible: true, selected: false }
])
const visibleFruits = React.useMemo(() => {
return fruits.filter((f) => f.visible)
}, [fruits])
const handleCheck = (bool, id) => {
setFruits((prev) => {
return prev.map((f) => {
if (f.id === id) {
f.selected = bool
}
return f
})
})
}
return (
<div>
{visibleFruits.map((fruit) => {
return <FruitOption fruit={fruit} handleCheck={handleCheck} />
})}
</div>
)
}
const FruitOption = ({ fruit, handleCheck }) => {
console.log('** THIS RENDERS TWICE EVERY TIME USER SELECTS A FRUIT **')
return (
<div key={fruit.id}>
<input
checked={fruit.selected}
onChange={(e) => handleCheck(e.target.checked, fruit.id)}
type='checkbox'
/>
<label>{fruit.name}</label>
</div>
)
}
export default Example
First, there's a problem with the handleCheck function (but it's not related to what you're asking about). Your code is modifying a fruit object directly (f.selected = bool), but you're not allowed to do that with React state, objects in state must not be directly modified, and rendering may not be correct if you break that rule. Instead, you need to copy the object and modify the copy (like you are with the array):
const handleCheck = (bool, id) => {
setFruits((prev) => {
return prev.map((f) => {
if (f.id === id) {
return {...f, selected: bool}; // ***
}
return f;
});
});
};
But that's not what you're asking about, just something else to fix. :-)
The reason you see the console.log executed twice after handleCheck is that the component has to be re-rendered (for the change), and there are two visible fruits, so you see two calls to your FruitOption component function. There are two reasons for this:
handleChange changes every time your Example component function is called, so FruitOption sees a new prop every time; and
FruitOption doesn't avoid re-rendering when its props don't change, so even once you've fixed #1, you'd still see two console.log calls; and
Separately, there's no key on the FruitOption elements, which can cause rendering issues. Always include a meaningful key when rendering elements in an array. (Don't just use the index, it's problematic; but your fruit objects have an id, which is perfect.)
To fix it:
Memoize handleChange so that it's not recreated every time, probably via useCallback, and
Use React.memo so that FruitOption doesn't get called if its props don't change (see the end of this answer for the class component equivalent), and
Add a meaningful key to the FruitOption elements in Example
Taking those and the handleChange fix above and putting them all together:
const Example = () => {
const [fruits, setFruits] = React.useState([
{ id: 1, name: 'apple', visible: true, selected: false },
{ id: 2, name: 'banana', visible: false, selected: false },
{ id: 3, name: 'orange', visible: true, selected: false }
]);
const visibleFruits = React.useMemo(() => {
return fruits.filter((f) => f.visible);
}, [fruits]);
const handleCheck = React.useCallback(
(bool, id) => {
setFruits((prev) => {
return prev.map((f) => {
if (f.id === id) {
return {...f, selected: bool}; // ***
}
return f;
});
});
},
[] // *** No dependencies since all it uses is `setFruits`, which is
// stable for the lifetime of the component
);
return (
<div>
{visibleFruits.map((fruit) => {
// *** Note the key
return <FruitOption key={fruit.id} fruit={fruit} handleCheck={handleCheck} />
})}
</div>
);
}
// *** `React.memo` will compare the props and skip the call if they're the same, reusing
// the previous call's result.
const FruitOption = React.memo(({ fruit, handleCheck }) => {
console.log(`Rendering fruit ${fruit.id}`);
return (
<div key={fruit.id}>
<input
checked={fruit.selected}
onChange={(e) => handleCheck(e.target.checked, fruit.id)}
type='checkbox'
/>
<label>{fruit.name}</label>
</div>
);
});
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
As you can see, with all that in place, only the changed fruit is re-rendered.
Re React.memo: For components with more complicated requirements, you can provide a function as a second argument that determines whether the two sets of props are "the same" for rendering purposes. By default, React.memo just does a shallow equality comparison, which is often sufficient.
Finally: For class components, the equivalent of React.memo without providing an equality callback is extending PureComponent instead of Component. If you want to make your check of the props more fine-grained, you can implement shouldComponentUpdate instead.

How do I add an list element seperately to 2 lists?

I am pretty new to react. So I have one parent component which has two child components. These 2 children are the lists that should be displayed. So far I figured out how to transfer the data between two lists by checking the status property of the data. I am not able to understand how to add data into the separate lists and edit them since the parent component renders the 2 lists. Can anyone explain how to add and edit new data that the user will enter? Should I create new states and props on the Items page or should I create them on the child component page? I am pretty confused.
import React,{useState,useEffect} from 'react'
import { Completed } from './Completed'
import { Pending } from './Pending'
export const Items = () => {
const [items,setItems]=useState([
{
id: 1,
title:'Workout',
status:'Pending'
},
{
id: 2,
title:'Read Books',
status:'Pending'
},
{
id: 3,
title:'Cook Pizza',
status:'Pending'
},
{
id: 4,
title:'Pay Bills',
status:'Completed'
},
{
id: 5,
title:' Watch Big Short',
status:'Completed'
},
{
id: 6,
title:' Make nutrition Plan',
status:'Pending'
}
])
const updateStatus=(id,newStatus)=>{
let allItems=items;
allItems=allItems.map(item=>{
if(item.id===id){
console.log('in here')
item.status=newStatus;
}
return item
})
setItems(allItems)
}
return (
<div class="items">
<Pending items={items} setItems={setItems} updateStatus={updateStatus}/>
<Completed items={items} setItems={setItems} updateStatus={updateStatus}/>
</div>
)
}
import React from 'react'
export const Pending = ({items,setItems,updateStatus}) => {
return (
<div className="pending">
<h1>LEFT</h1>
{
items && items.map(item=>{
if(item && item.status==='Pending')
return <><p className="item" key={item.id}>{item.title} <button className="mark_complete" key={item.id} onClick={()=>{updateStatus(item.id,'Completed')}}>Move Right</button></p></>
})
}
</div>
)
}
import React from 'react'
export const Completed = ({items,setItems,updateStatus}) => {
return (
<div className="completed">
<h1>RIGHT</h1>
<form onSubmit={this.addItem}>
<input placeholder="enter task">
</input>
<button type="submit">add</button>
</form>
{
items && items.map(item=>{
if(item && item.status==='Completed')
return <><p className="item" key={item.id}>{item.title} <button className="mark_pending" key={item.id} onClick={()=>{updateStatus(item.id,'Pending')}}> Move Left</button></p> </>
})
}
</div>
)
}
I have attached the 3 components which are Items, Pending and Completed above.
It's almost always better to have the state in the parent and pass down props to the children. So you want to keep your items state where it is. You can create an addItem function and pass it down as a prop to any child.
I don't think it makes sense to be able to add items from both lists since new items should be 'Pending'. So I would recommend that you put your add form in a new component AddItem which would be a third child of Items. Once AddItem calls the addItem function from props, that item will get saved to the state in items and it will show up in the Pending list automatically.
If all new items have status 'Pending' then the only information that we should need to add an item is the title of the task.
This function goes in Items:
const addItem = (title) => {
// set state using a callback function of current state
setItems((current) => {
// the highest number of all current ids, or 0 if empty
const maxId = current.reduce((max, o) => Math.max(max, o.id), 0);
// the next id is the max plus 1
const id = maxId + 1;
// add new item to the current - concat won't mutate the array
return current.concat({
id,
title,
status: "Pending"
});
});
};
Your AddItem component uses a controlled input to create the text for the new item.
export const AddItem = ({ addItem }) => {
const [title, setTitle] = useState("");
const handleSubmit = (e) => {
// prevent form submission from reloading the page
e.preventDefault();
// call the addItem function with the current title
addItem(title);
// clear the form
setTitle("");
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="enter task"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button type="submit">add</button>
</form>
);
};
Inside the return of Items, include your form:
<AddItem addItem={addItem} />
Unrelated to the question at hand, there are a few other improvements that you can make to your code.
Your updateStatus function actually mutates the current item. You should instead create a new object for the changed item by copying everything except the status.
You are getting warnings about unique keys because the key must be on the outermost component inside the .map(). You put a fragment <> outside the <p> which has the key, so remove the fragment.
In my opinion the filtering of which item goes in each list should be done by the parent. Your Completed and Pending components are extremely similar. You should combine them into one component. Everything that is different between the two, such as texts and class names, can be controlled by the props that you pass in.
import React, { useState } from "react";
export const ItemsList = ({
items,
title,
className,
buttonText,
onClickButton
}) => {
return (
<div className={className}>
<h1>{title}</h1>
{items.map((item) => (
<p className="item" key={item.id}>
<span className="item_title">{item.title}</span>
<button
className="move_item"
key={item.id}
onClick={() => {
onClickButton(item.id);
}}
>
{buttonText}
</button>
</p>
))}
</div>
);
};
// example of how to compose components
// this keeps the same setup that you had before, but without repeated code
export const Completed = ({ items, updateStatus }) => {
return (
<ItemsList
title="RIGHT"
buttonText="Move Left"
className="completed"
items={items.filter((item) => item.status === "Completed")}
onClickButton={(id) => updateStatus(id, "Pending")}
/>
);
};
export const AddItem = ({ addItem }) => {
const [title, setTitle] = useState("");
const handleSubmit = (e) => {
// prevent form submission from reloading the page
e.preventDefault();
// call the addItem function with the current title
addItem(title);
// clear the form
setTitle("");
};
return (
<form onSubmit={handleSubmit}>
<input
placeholder="enter task"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button type="submit">add</button>
</form>
);
};
export const Items = () => {
const [items, setItems] = useState([
{
id: 1,
title: "Workout",
status: "Pending"
},
{
id: 2,
title: "Read Books",
status: "Pending"
},
{
id: 3,
title: "Cook Pizza",
status: "Pending"
},
{
id: 4,
title: "Pay Bills",
status: "Completed"
},
{
id: 5,
title: " Watch Big Short",
status: "Completed"
},
{
id: 6,
title: " Make nutrition Plan",
status: "Pending"
}
]);
const addItem = (title) => {
// set state using a callback function of current state
setItems((current) => {
// the highest number of all current ids, or 0 if empty
const maxId = current.reduce((max, o) => Math.max(max, o.id), 0);
// the next id is the max plus 1
const id = maxId + 1;
// add new item to the current - concat won't mutate the array
return current.concat({
id,
title,
status: "Pending"
});
});
};
const updateStatus = (id, newStatus) => {
setItems((current) =>
// arrow function without braces is an implicit return
current.map((item) =>
item.id === id
? // copy to new item if id matches
{
...item,
status: newStatus
}
: // otherwise return the existing item
item
)
);
};
return (
<div className="items">
<AddItem addItem={addItem} />
{/* can set the props on ItemsList here */}
<ItemsList
title="LEFT"
buttonText="Move Right"
className="pending"
items={items.filter((item) => item.status === "Pending")}
// create a function that just takes the `id` and sets the status to "Completed"
onClickButton={(id) => updateStatus(id, "Completed")}
/>
{/* or do it in a separate component */}
<Completed items={items} updateStatus={updateStatus} />
</div>
);
};
export default Items;
Code Sandbox Link

Rendering Content Conditionally in React JS Based on the state

I have a page that renders questions that have been posted. I want to create a button that displays only answered questions based on the state = {isAnswered: true}.
Is the state isAnswered is true then onClick will display answered questions only where isAnswered is set to true in the object.
How can I used this Filter button to conditionally render these based on their state.
Should the function be stored as constant called in the render function or before this?
this.state.posts is an array of these objects on the back end:
Here is what I have attempted.
class Posts extends Component {
state = {
posts: []
}
render () {
let posts = <p style={{ textAlign: 'center' }}>Something went wrong!</p>;
let {isAnswered} = this.state;
const renderAuthButton = () => {
if (isAnswered === true) {
if ( !this.state.error ) {
posts = this.state.posts.map( (post) => {
return (
<Post
key={post.key}
id={post.key}
title={post.title}
type={post.type}
body={post.body}
answer={post.answer}
onChange={(value, id) => this.postAnswerHandler(value,id)}
clicked={(body) => this.displayAnswerHandler(body)}
/>
);
} );
}
}
}
}
return (
<button onClick={renderAuthButton()}>Filter</button>
{posts}
)
You are misinterpreting your data structure. this.state has a property this.state.posts which is an array. Each element in the array is an object with multiple properties including isAnswered.
When you do this:
let {isAnswered} = this.state;
You are looking for a property this.state.isAnswered which does not exist. There is no top-level isAnswered property. It is something that exists within each post object and is different for every post. So you need to be looking at isAnswered inside of your loop.
There's honestly a lot that's weird and backwards here. Don't create a callback inside of render()! Don't return JSX from a callback!
Here's my attempt to clean it up. I am adding a property to this.state which tells us whether or not to filter the posts. Clicking the button changes this.state.isFiltered. The render function renders appropriately based on the current state.
class Posts extends Component {
state = {
posts: [],
isFiltered: false,
isError: false
};
async componentDidMount() {
// do your API fetch and set the state for `posts` and `isError`
try {
const fetchedPosts = someApiFunction();
this.setState({
posts: fetchedPosts
});
} catch (error) {
this.setState({
isError: true
});
}
}
onClickFilter = () => {
// toggles filter on and off
this.setState((prevState) => ({
isFiltered: !prevState.isFiltered
}));
};
render() {
if (this.state.isError) {
return <p style={{ textAlign: "center" }}>Something went wrong!</p>;
}
// show only answered posts if isFiltered is true, or all posts if false
const visiblePosts = this.state.isFiltered
? this.state.posts.filter((post) => post.isAnswered)
: this.state.posts;
return (
<>
<button onClick={this.onClickFilter}>Filter</button>
{visiblePosts.map((post) => {
return (
<Post
key={post.key}
id={post.key}
title={post.title}
type={post.type}
body={post.body}
answer={post.answer}
onChange={(value, id) => this.postAnswerHandler(value, id)}
clicked={(body) => this.displayAnswerHandler(body)}
/>
);
})}
</>
);
}
}

dynamically generated Toggle (switches) in react js not working

There are two types of switch status in my project. One is default and the other is generated from API.When the item is changed toggle switch on/off won't work.
constructor(props) {
super(props);
this.state = {
switch_status: [true, false],
items: [{title:toyota}, {title:bmw}]
}
}
There is a function, Which get data from API and set into items:
changeItems = () => {
this.setState({ items: [{title:toyota, switch_status: true},
{title:porche, switch_status: true},
{title:bmw, switch_status: false}]
});
}
on/off not working, When Items changed:
//Switch on/off function
handleChange = (event, id) => {
const isChecked = event;
this.setState(
({switch_status}) => ({
switch_status: {
...switch_status,
[id]: isChecked,
}
})
);
}
//Loop Items
this.state.items.map((item, index) => (
<Switch
className="custom-switch custom-switch-primary"
checked={this.state.switch_status[index]}
id={index}
onChange={event => handleChange(event, index)}
/>
))
There is nothing wrong in your state handling logic really but your componentDidUpdate() is getting called infinite times because the check inside is not working and it overwrites your toggle state even when you don't need to.
Change you componentDidUpdate() to:
componentDidUpdate(previousProps, previousState) {
if (
JSON.stringify(previousProps.mediaTypes.items) !==
JSON.stringify(this.props.mediaTypes.items)
) {
this.dataListRender();
this.setState({
customMediaTypesItems: this.props.mediaTypes.items.custom_media_types
});
}
}
First of all; you are passing a new reference to a component as prop on every render and that causes needless DOM updates
Second is that you initialise the state with a different structure than when you are setting state. I assume that
{
items: [
{ title: toyota, switch_status: true },
{ title: porche, switch_status: true },
{ title: bmw, switch_status: false }
];
}
Is your actual state because you use that to render. You can do the following:
const Switch = React.memo(
//use React.memo to create pure component
function Switch({ label, checked, toggle, id }) {
console.log("rendering:", label);
// prop={new reference} is not a problem here
// this won't re render if props didn't
// change because it's a pure component
// if any of the props change then this needs to re render
return (
<label>
{label}
<input
type="checkbox"
checked={checked}
onChange={() => toggle(id)}
/>
</label>
);
}
);
class App extends React.PureComponent {
state = {
items: [
{ title: "toyota", switch_status: true },
{ title: "porche", switch_status: true },
{ title: "bmw", switch_status: false }
]
};
toggle = _index =>
this.setState({
items: this.state.items.map((item, index) =>
_index === index // only toggle the item at this index
? { ...item, switch_status: !item.switch_status }
: item // do not change the item
)
});
render() {
//please note that using index is not a good idea if you
// change the order of the state.items, add or remove some item(s)
// if you plan to do that then give each item a unique id
return (
<div>
{this.state.items.map((item, index) => (
<Switch
label={item.title}
checked={item.switch_status}
toggle={this.toggle}
id={index}
key={index}
/>
))}
</div>
);
}
}
//render app
ReactDOM.render(<App />, document.getElementById("root"));
<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="root"></div>
I believe there is an issue while getting the checked state.
In your current implementation, you have written const isChecked = event; in the handleChange method which will always be true since the event object is always available.
It should be const isChecked = event.target.checked; for it to set the toggled checkbox state correctly.

setState for clicked item only in react app

In my react app i have multiple checkboxes and I'm toggling the checked state using onClick, it's setting the state but it's changing it for all the checkboxes in the page, i want only the pressed one, here is the code:
Initial state:
state: {checked: false}
Checkbox:
return boxes.map(box => (
<Checkbox checked={this.state.checked} onClick={() => this.onCheck(box.id)} />
))
Function:
onCheck(id) { this.setState({ checked: !this.state.checked }); }
Then you'll have to have one state variable for each checkbox. For simplicity, let's put all booleans defining whether the n-th checkbox has been checked into a single array.
You can write a minimal component like this:
class App extends React.Component {
constructor(props) {
super(props);
this.state = { boxes: [{id: 10}, {id: 20}] };
this.state.checked = this.state.boxes.map(() => false);
this.onCheck = this.onCheck.bind(this);
}
onCheck(id) {
let index = this.state.boxes.findIndex(box => box.id==id);
this.setState({
checked: this.state.checked.map((c,i) => i==index ? !c : c)
})
}
render() {
return (<div>
{this.state.boxes.map((box,i) => (
<input
key={box.id}
type="checkbox"
checked={this.state.checked[i]}
onChange={() => this.onCheck(box.id)}
/>
))}
<pre>this.state = {JSON.stringify(this.state,null,2)}</pre>
</div>);
}
}
ReactDOM.render(<App/>, document.querySelector('#root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
It's changing all of the checkboxes because all of the checkboxes are referring to the same variable in state. You could store their ids in an array:
state: {
checkedIdArray: []
}
Then check if the current box's id is in the array to determine whether it is checked:
<Checkbox
key={box.id}
checked={this.state.checkedIdArray.includes[box.id]}
onClick={() => this.onCheck(box.id)}
/>
Finally, your onCheck() method would look something like this:
onCheck(id) {
if (this.state.checkedIdArray.includes(id)) {
this.setState({
checkedIdArray: this.state.checkedIdArray.filter((val) => val !== id)
});
} else {
this.setState({
checkedIdArray: [...this.state.checkedIdArray, id]
});
}
}
Haven't tested or anything but something like this should get you where you want to go.

Categories