Toggling nested state object in React - javascript

I have a state object that contains an array of objects:
this.state = {
feeling: [
{ name: 'alert', status: false },
{ name: 'calm', status: false },
{ name: 'creative', status: false },
{ name: 'productive', status: false },
{ name: 'relaxed', status: false },
{ name: 'sleepy', status: false },
{ name: 'uplifted', status: false }
]
}
I want to toggle the boolean status from true to false on click event. I built this function as a click handler but it doesn't connect the event into the state change:
buttonToggle = (event) => {
event.persist();
const value = !event.target.value
this.setState( prevState => ({
status: !prevState.status
}))
}
I'm having a hard time following the control flow of the nested React state change, and how the active event makes the jump from the handler to the state object and vice versa.
The whole component:
export default class StatePractice extends React.Component {
constructor() {
super();
this.state = {
feeling: [
{ name: 'alert', status: false },
{ name: 'calm', status: false },
{ name: 'creative', status: false },
{ name: 'productive', status: false },
{ name: 'relaxed', status: false },
{ name: 'sleepy', status: false },
{ name: 'uplifted', status: false }
]
}
}
buttonToggle = (event) => {
event.persist();
const value = !event.target.value
this.setState( prevState => ({
status: !prevState.status
}))
}
render() {
return (
<div>
{ this.state.feeling.map(
(stateObj, index) => {
return <button
key={ index }
onClick={ this.buttonToggle }
value={ stateObj.status } >
{ stateObj.status.toString() }
</button>
}
)
}
</div>
)
}
}

In order to solve your problem, you should first send the index of the element that is going to be modified to your toggle function :
onClick = {this.buttonToggle(index)}
Then tweak the function to receive both the index and the event.
Now, to modify your state array, copy it, change the value you are looking for, and put it back in your state :
buttonToggle = index => event => {
event.persist();
const feeling = [...this.state.feeling]; //Copy your array
feeling[index] = !feeling[index];
this.setState({ feeling });
}
You can also use slice to copy your array, or even directly send a mapped array where only one value is changed.

Updating a nested object in a react state object is tricky. You have to get the entire object from the state in a temporary variable, update the value within that variable and then replace the state with the updated variable.
To do that, your buttonToggle function needs to know which button was pressed.
return <button
key={ index }
onClick={ (event) => this.buttonToggle(event, stateObj.name) }
value={ stateObj.status } >
{ stateObj.status.toString() }
</button>
And your buttonToggle function could look like this
buttonToggle = (event, name) => {
event.persist();
let { feeling } = this.state;
let newFeeling = [];
for (let index in feeling) {
let feel = feeling[index];
if (feel.name == name) {
feel = {name: feel.name, status: !feel.status};
}
newFeeling.push(feel);
}
this.setState({
feeling: newFeeling,
});
}
Here's a working JSFiddle.
Alternatively, if you don't need to store any more data per feeling than "name" and "status", you could rewrite your component state like this:
feeling: {
alert: false,
calm: false,
creative: false,
etc...
}
And buttonToggle:
buttonToggle = (event, name) => {
event.persist();
let { feeling } = this.state;
feeling[name] = !feeling[name];
this.setState({
feeling
});
}

I think you need to update the whole array when get the event. And it is better to not mutate the existing state. I would recommend the following code
export default class StatePractice extends React.Component {
constructor() {
super();
this.state = {
feeling: [
{ name: "alert", status: false },
{ name: "calm", status: false },
{ name: "creative", status: false },
{ name: "productive", status: false },
{ name: "relaxed", status: false },
{ name: "sleepy", status: false },
{ name: "uplifted", status: false },
],
};
}
buttonToggle = (index, value) => (event) => {
event.persist();
const toUpdate = { ...this.state.feeling[index], status: !value };
const feeling = [...this.state.feeling];
feeling.splice(index, 1, toUpdate);
this.setState({
feeling,
});
};
render() {
return (
<div>
{this.state.feeling.map((stateObj, index) => {
return (
<button
key={index}
onClick={this.buttonToggle(index, stateObj.status)}
value={stateObj.status}
>
{stateObj.status.toString()}
</button>
);
})}
</div>
);
}
}

Related

Updating the state of object values in React

I have an array set in state like:
const Theme = {
name: "theme",
roots: {
theme: Theme,
},
state: {
theme: {
quiz: {
quizGender: null,
quizSleepComfort: {
justMe: {
soft: null,
medium: null,
firm: null,
},
partner: {
soft: null,
medium: null,
firm: null,
}
},
},
},
},
actions: {
// ...
},
};
I then have a component that has checkboxes, one for soft, medium, and firm. The code for the component is:
const Question = ({ state }) => {
const [checkedItems, setCheckedItems] = useState([]);
const checkboxes = [
{
label: "Firm",
value: "firm",
},
{
label: "Medium",
value: "medium",
},
{
label: "Soft",
value: "soft",
},
];
state.theme.quiz.quizSleepComfort.justMe = checkedItems;
return (
<QuestionCommonContainer>
{checkboxes.map((item, id) => (
<QuizCheckbox
label={item.label}
name={item.label}
value={item.value}
selected={checkedItems[item.value] === true}
onChange={(e) => {
setCheckedItems({
...checkedItems,
[e.target.value]: e.target.checked,
});
}}
/>
))}
</QuestionCommonContainer>
);
};
export default connect(Question);
This specific component is just interacting with state.theme.quiz.quizSleepComfort.justMe object, not the partner object.
As of right now when a checkbox is selected, let's say the checkbox for "firm" is checked, the state gets updated to what looks like this:
...
quizSleepComfort: {
justMe: {
firm: true,
},
partner: {
soft: null,
medium: null,
firm: null,
}
},
...
I am trying to figure out how I would be able to alter this components code so that instead of setting the justMe object to include only the items that are checked (in this case "firm"), it should keep the other items as well ("soft", "medium") as null.
Please let me know if there is more info i should provide.
Okay. So the following is bad practice
state.theme.quiz.quizSleepComfort.justMe = checkedItems;
You should pass a function to the Question component, something like onChange.
The onChange function should update the state in your parent component. Use the spread operator ... to get a copy of the old object. for example
const onChange = (newState) =>
setState((oldState) => ({
...oldState,
justMe: { ...oldState.justMe, ...newState },
}));
the resulting object will contain all the properties of the original state but will overwrite any property set on newState in justMe. If the property that you want to update is more nested, just repeat the steps of spreading.
--- UPDATE ---
I have added an example that I think is close to what you are trying to achieve.
const Parent = () => {
const [state, setState] = useState(initialState);
const onChange = useCallback(
(newState) =>
setState((oldState) => ({
...oldState,
theme: {
...oldState.theme,
quiz: {
...oldState.theme.quiz,
quizSleepComfort: {
...oldState.theme.quizSleepComfort,
justMe: {
...oldState.theme.quizSleepComfort.justMe,
...newState,
},,
},
},
},
})),
[],
);
return <Question onChange={onChange} />;
};
const checkboxes = [
{
label: 'Firm',
value: 'firm',
},
{
label: 'Medium',
value: 'medium',
},
{
label: 'Soft',
value: 'soft',
},
];
const Question = ({ onChange }) => {
const [checkedItems, setCheckedItems] = useState([]);
useEffect(() => {
onChange(checkedItems);
}, [checkedItems, onChange]);
return (
<QuestionCommonContainer>
{checkboxes.map((item, id) => (
<QuizCheckbox
label={item.label}
name={item.label}
value={item.value}
selected={checkedItems[item.value] === true}
onChange={(e) => {
setCheckedItems((oldCheckedItems) => ({
...oldCheckedItems,
[e.target.value]: e.target.checked,
}));
}}
/>
))}
</QuestionCommonContainer>
);
};
export default connect(Question);
As you are having a really nested object to update, it might be worth taking a look at Object.assign

Update state array by object Id

I want to update state array object by particular id.
Suppose I have following object in state. And I tried to update by following way using id but, it doesn't work for me.
It didn't update state data.
this.state = {
data: [{id:'124',name:'qqq'},
{id:'589',name:'www'},
{id:'45',name:'eee'},
{id:'567',name:'rrr'}]
}
publishCurrentProject = user => {
this.setState(prevState => ({
data: prevState.data.map(item =>
item.id === user.id ? { ...user } : item
),
}))
}
let user = {id:'124',name:'ttt'};
publishCurrentProject(user);
Any help would be greatly appreciated.
Maybe the problem is on how you called the publishCurrentProject(), maybe you put that function in the wrong context. I use your implementation and it still works
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [
{ id: "124", name: "qqq" },
{ id: "589", name: "www" },
{ id: "45", name: "eee" },
{ id: "567", name: "rrr" }
]
};
this.handleClick = this.handleClick.bind(this);
this.publishCurrentProject = this.publishCurrentProject.bind(this);
}
handleClick(e) {
let user = { id: "124", name: "ttt" };
this.publishCurrentProject(user);
}
publishCurrentProject(user) {
this.setState((prevState) => ({
data: prevState.data.map((item) =>
item.id === user.id ? { ...user } : item
)
}));
}
render() {
return (
<div className="App">
<h1>Test</h1>
{this.state.data.map((el) => (
<p>{el.name}</p>
))}
<button onClick={this.handleClick}>Change</button>
</div>
);
}
}
Codesandbox for worked example

How to change a object state inside a map function

I need to increase or decrease state value in catalog > spec > units, if I click on increase button the number in units should increase by one and if I click on decrease button it should decrease by one, I'd tried by setting state in the render, but it didn't work and I think this is not a good practice. How can I create a function to setState of units without declaring it inside the render method?
Here is an example of my code:
export default class Order extends Component {
constructor(props) {
super(props);
this.state = {
catalog: [
{
photo: 'https://via.placeholder.com/400x400',
title: 'My title',
description: 'Bla bla bla...',
spec: { size: 'FAM', units: 1, price: 999999, id: 'CMB0', selectedIndicator: '', isSelected: false, name: 'A simple name' },
isCombo: true
},
],
}
}
}
render(){
return(
{this.state.catalog.map((item, index) => {
<div key={index}>
<strong>{item.title}</strong>
<span>{item.spec.units}</span>
<button onClick={() => item.spec.units + 1}>increase</button>
<button onClick={() => item.spec.units - 1}>decrease</button>
</div>})
}
)
}
Try this
increase = title => {
const newCatalogState = this.state.catalog.map(item => {
if (item.title === title) {
return {
...item,
spec: {
...item.spec,
units: item.spec.units + 1
}
};
}
return item;
});
this.setState({
catalog: newCatalogState
});
};
decrease = title => {
const newCatalogState = this.state.catalog.map(item => {
if (item.title === title) {
return {
...item,
spec: {
...item.spec,
units: item.spec.units - 1
}
};
}
return item;
});
this.setState({
catalog: newCatalogState
});
};
<button onClick={() => this.increase(item.title)}>increase</button>
<button onClick={() => this.decrease(item.title)}>decrease</button>
you can check here codesandbox hope it helps
Try this:
export default class Order extends Component {
constructor(props) {
super(props);
this.state = {
catalog: [
{
photo: 'https://via.placeholder.com/400x400',
title: 'My title',
description: 'Bla bla bla...',
spec: { size: 'FAM', units: 1, price: 999999, id: 'CMB0', selectedIndicator: '', isSelected: false, name: 'A simple name' },
isCombo: true
},
],
}
}
}
const updateUnits = (index, value) => {
const { catalog } = this.state
catalog[index].spec.units += value
this.setState({catalog})
}
render(){
return(
{ this.state.catalog.map((item, index) => {
<div key={index}>
<strong>{item.title}</strong>
<span>{item.spec.units}</span>
<button onClick={() => this.updateUnits(index, 1)}>increase</button>
<button onClick={() => this.updateUnits(index, -1)}>decrease</button>
</div>})
}
)
}

How to make checkbox change specific object property to false

Started this todo app in react that takes input and adds the input to the array of objects. Each todo item has a checkbox next to it. I want when the checkbox is checked, completed of the specific property to change to either true or false depending on the initial value but I keep running to errors.
See function isCompleted and help me find a way to do this.
const Todos = () => {
const [todo, setTodo] = useState([
{
id: 1,
title: "Go to store",
completed: true
},
{
id: 2,
title: "Buy groceries",
completed: false
},
{
id: 3,
title: "Go to dinner with wife",
completed: true
}
]);
const [work, setWork] = useState("");
const newTodo = e => {
setWork(e.target.value);
};
const addTodo = e => {
e.preventDefault();
setTodo(prevTodo => [
...prevTodo,
{ id: prevTodo.length + 1, title: work, completed: false }
]);
setWork("");
};
const isCompleted = () => {
setTodo(todo.map(todos => {
if (todos.completed) {
todos.completed = false
}
else {
todos.completed = true
}
}))
};
return (
<div>
<form onSubmit={addTodo}>
<input
type="text"
value={work}
onChange={newTodo}
className="inputText"
/>
<button>Add</button>
</form>
<div>
{todo.map(todos => (
<TodoItem
key={todos.id}
title={todos.title}
completed={todos.completed}
id={todos.id}
isCompleted={isCompleted}
/>
))}
</div>
</div>
);
};
You want to pass in the id of the specific todo to mark just that one as completed.
const isCompleted = (id) => {
setTodo(todo.map(todos => {
if (todos.id === id) {
todos.completed = true;
}
return todos;
}))
};
...
<TodoItem
key={todos.id}
title={todos.title}
completed={todos.completed}
id={todos.id}
isCompleted={() => isCompleted(todos.id)}
/>

CRUD UI bug when applying sortBy

I did a CRUD (UI only) simple component in react, but how do I make the primary contact to be the first one in my table? The app can do crud, check and uncheck primary contact, only one primary contact is allowed in the table.
Working demo
https://codesandbox.io/s/r7kmp9rkom
================================================
I've tried using lodash's sortBy
(Broken demo using sortBy
https://codesandbox.io/s/pjj3098lmx)
line 130
<tbody>
{sortBy(contacts, o => !o.primary).map((o, i) => {
return (
<tr className={classNames({ primary: o.primary })} key={i}>
<td>{o.name}</td>
<td>{o.email}</td>
<td>
<button
onClick={() =>
this.setState({
openModal: true,
modalAction: "update",
selected_contact: o,
selected_contact_index: i
})
}
>
Edit
</button>
</td>
</tr>
);
})}
</tbody>
But it broke the functionality. I think it has to do with the index problem.
I couldn't solve it I don't know why sortBy doesn't retain the index. Another silly option would be using flexbox order but I hope I could solve it using just javascript.
As you told it is an index problem
I have used lodash's uniqueId method to retain id value, never use an index as key for a dynamic list when we have operations like deleting/updating/adding.
if an app is a server-side render id must come from a backend.
id: uniqueId("contact_")
Working Demo link
static defaultProps = {
data: {
contacts: [
{
name: "James",
email: "james#havard.edu",
primary: false,
id: uniqueId("contact_")
},
{
name: "Mark",
email: "mark#standford.edu",
primary: true,
id: uniqueId("contact_")
}
]
}
};
onSaveContact = (action, newContact, newContact_index) => {
if (action === "add") {
if (newContact.primary) {
const setNonePrimary = this.state.contacts.map(o => ({
...o,
primary: false
}));
this.setState({
contacts: [
...setNonePrimary,
{ ...newContact, id: uniqueId("contact_") }
]
});
} else {
this.setState({
contacts: [
...this.state.contacts,
{ ...newContact, id: uniqueId("contact_") }
]
});
}
} else if (action === "update") {
this.setState({
contacts: [
...this.state.contacts.map((o, i) => {
if (o.id === newContact_index) {
return { ...newContact };
} else {
if (newContact.primary) {
return { ...o, primary: false };
} else {
return { ...o };
}
}
})
]
});
} else if (action === "delete") {
this.setState({
contacts: [
...this.state.contacts.filter((o, i) => {
if (o.id !== newContact_index) {
return o;
}
})
]
});
}

Categories