I'm making a fairly simple React app to get some of the key concepts, however, I'm struggling a bit with updating my state. I got it to a point where it's working but it's not exactly working as intended.
this.state = {
list: [{
id: 1,
title: 'Figure out how to update state',
completed: false
},
{
id: 2,
title: 'Drink less coffee',
completed: false
}]
}
completeItemHandler = (id) => {
this.setState(prevState => {
const list = prevState.list.filter(item => item.id === id);
list[0].completed = true;
return ({...list})
},() => console.log(this.state))
}
the console log returns:
{
"id": 1,
"title": "Figure out how to update state",
"completed": true
},
list: [
{
"id": 1,
"title": "Figure out how to update state",
"completed": true
},
{
"id": 2,
"title": "Drink less coffee",
"completed": false
}
]
so it looks like it's both updates the state and appended that list object to the state as well which is obviously not what I want. Could someone please explain where I went wrong with this please? I'm not sure why it's created a new object in the state while also updating the object in the list array of objects.
Any help would be much appreciated and a solution to this would be appreciated also!
Because you spread array list to Object.
I would suggest use prevState.map instead filter -> mutation and spread.
prevState => ({
…prevState,
list: prevState.list.map(item => {
if (item.id !== id) return item;
return {
…item,
completed: true,
};
});
Got this to work using:
completeItemHandler = (id) => {
this.setState( prevState => {
const listIndex = prevState.list.findIndex(item => item.id === id);
//prevState.list[listIndex].completed = !prevState.list[listIndex].completed; // doesn't work
prevState.list[listIndex].completed = true; //works
return (
{...prevState}
)
},() => console.log(this.state))
}
However, this resulted in some weird behaviour doing prevState.list[listIndex].completed = !prevState.list[listIndex].completed; where it wouldn't work. The console.log would output the right value but the state would never update with it.
#Kirill Skomarovskiy's answer is a lot easier to read/ understand for me compared to that so I've used that instead
Related
const [Questions, setQuestions] = React.useState(props.QuestionsData);
const handleClick = (id, isCorrect) => {
if (isCorrect) {
setQuestions((prev) => {
prev.map((item, index) => {
if (index === id) {
return {
...item,
[item.Answers.correct.selected]: !item.Answers.correct.selected,
};
} else {
return item;
}
});
});
} else {
return;
}
};
This is the code im doing and what I want to do it loop over the Questions (which has a list of objects that look like this.
{
"category": "Entertainment: Video Games",
"type": "boolean",
"difficulty": "hard",
"question": "The first "Metal Gear" game was released for the PlayStation 1.",
"correct_answer": "False",
"incorrect_answers": [
"True"
],
"Answers": {
"correct": {
"MasterId": "MXTOfKnKw7dU7QP0UP0td",
"id": 0,
"answer": "False",
"selected": false,
"correct": true,
"userChosen": true
},
"wrong": {
"id": 1,
"answer": "True",
"selected": false
}
}
}
I tried to do everything but it returns undefined. I even tried to do the map alone with the value and logged out the needed value and it worked.
If someone can tell me a good way to loop over usestate array of objects and only edit the objects that satisfy the condition that would be great.
Remove the curly braces so that the value is actually returned.
setQuestions((prev) =>
prev.map((item, index) => {
if (index === id) {
return {
...item,
[item.Answers.correct.selected]: !item.Answers.correct.selected,
};
} else {
return item;
}
});
);
I don't recommend using the capitalization convention for variable names. Use camelCase and save those for Components, Classes, and Types.
For the sake of readability and clean code, I recommend you write data processing code outside of setSomeState()
Here's an example.
import { useState } from "react";
...
const [questionList, setQuestionList] = useState(props.questionsData);
const handleClick = (id, isCorrect) => {
if (isCorrect) {
const updatedList = questionList.map((item, index) => {
if (index === id) {
return {
...item,
[item.answers.correct.selected]: !item.answers.correct.selected,
};
} else {
return item;
}
});
// no messy prev state stuff, it's now clean and easy to maintain
setQuestionList(updatedList);
} else {
return;
}
};
Further, I see that you use props.questionData as a default value of the setState. It's not a good approach since one component's state's default value depends on the other component's value and can potentially cause errors.
I personally recommend you to keep the questions default state to an empty array, and use useEffect and do setQuestions inside the useEffect with props.questionData.
Can you imagine writing this handler every time you have array-based stated? Also map doesn't make sense because we only want to update one item, however map iterates over the entire array.
Write a generic function to update an arr at a specific index using a user-supplied func -
function update(arr, index, func) {
return [
...arr.slice(0, index),
func(arr[index]),
...arr.slice(index + 1)
]
}
Now in your component -
function MyComponent({ questions = [] }) {
const [questions, setQuestions] = useState(questions)
const updateQuestion = index => event =>
setQuestions(arr => update(arr, index, q => ({
/* update q here */
})))
return questions.map((q, index) =>
<Question key={index} question={q} onClick={updateQuestion(index)} />
)
}
Developing your own utility functions to operate on arrays and objects can be a fun exercise, but maybe challenging to get right. Look to popular libraries like Immutable.js and Immer for inspiration.
See these Q&A for live examples you can run in your browser -
Add Numbers in ReactJS without button
How to change Active when it is clicked
How to remove an element from an array passing the key using hooks?
how to add class and remove class from buttons in react?
You forgot to return the result of the map. If a function doesn't return anything in JS, undefined will be returned. In addition, the computed property isn't used correctly here:
{
...item,
[item.Answers.correct.selected]: !item.Answers.correct.selected,
}
Try this:
const handleClick = (id, isCorrect) => {
if (isCorrect) {
setQuestions((prev) =>
prev.map((item, index) => {
if (index === id) {
item.Answers.correct.selected = !item.Answers.correct.selected
}
return item
}));
}
};
I'm trying to update part of a state object that is nested. This is the object:
const [buttonObject, setButtonObject] = useState({
intro: [
{
id: '123',
name: 'first_intro_name',
selected: false,
},
{
id: '124',
name: 'second_intro_name',
selected: false,
},
],
experience: [
{
id: '789',
name: 'first_experience_name',
selected: false,
},
{
id: '8910',
name: 'second_experience_name',
selected: false,
},
],
});
When a button is clicked I want to toggle the selected state. I'm using a click handler that looks like this:
const handleButtonClick = ({ id, selected }) => {
if (id === '123') {
buttonsObject.intro.map(
pref => (pref.selected = pref.id === id ? !pref.selected : selected)
);
setButtonsObject(buttonsObject);
} else if (id === '124') {
buttonsObject.intro.map(
pref => (pref.selected = pref.id === id ? !pref.selected : selected)
);
setButtonsObject(buttonsObject);
}
};
It would handle experiences as well. The issue is that right now it seems like rather than updating the object it just overwrites the object or creates a new one. It also doesnt pass that information back down to the component even though I have it routed correctly so it should.
Is there better/correct syntax for updating nested state like this?
Thanks.
instead of checking again with if condition use higher order array function and spread operator to get optimal solution.
setButtonObject(previous => {
return {
...previous,
info: previous.info.map(item => item.id === id ? {
...item,
selected: true
} ? item)
}
})
I am trying to create shopping basket through Redux Toolkit. I am finding it hard to understand this piece of code that what is purpose of all this code. Specifically those if conditions. Cant understand how add and remove reducer is working
const basketSlice = createSlice({
name: "basket",
initialState: INITIAL_STATE,
reducers: {
add: (state, action) => {
return state.map(item => {
if (item.id !== action.payload.id) {
return item
}
return {
...item,
added: true
}
})
},
remove: (state, action) => {
return state.map(item => {
if (item.id !== action.payload.id) {
return item
}
return {
...item,
added: false
}
})
}
}
})
You should check https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
Basically it loops over the state items, creating a new array from what is returned in each iteration of the loop.
So, what it does for, say, the remove reducer:
Loop over each item in state, each time returning something that will be an entry in the new array
The if section checks if the id of the current loop element is the same than the one we want to remove: if it's not the same ID, we return the item "as is", if it's the same ID, we return added: false so we know it was removed.
In the end, you get a new array that was processed through this map function, allowing to do whatever check you need to.
Say I have an array with 3 items:
const state = [
{ id: 12, name: "Fancy Phone", added: true, },
{ id: 54, name: "Leather Jacket", added: true, },
{ id: 564, name: "AI World-Dominating Robot", added: true, },
]
And I want to remove the "AI World-Dominating Robot" because I don't want anymore trouble:
// Create a new array from the .map
return state.map(item => {
// here we loop over each item one by one
// IF the ID in the action payload (thus the ID you want to remove) is not the same as the current item ID, we don't want to remove it
if (action.payload.id !== item.id) {
return item // so we return the item "as-is", and as we returned something, the .map loop moves to the next item
}
return { ...item, added: false } // otherwise, we set "added: false" to flag the fact it's removed
I've got an initial array, which can be added to and deleted from, no problems there..
const initialItems = [
{
id: Date.now(),
text: 'Get milk',
},
{
id: Date.now(),
text: 'Get eggs',
},
]
..but I'm trying to figure out how to edit the text effectively of one of the items using a dispatch function.
My dispatch looks like this:
const editItemHandler = () => {
dispatch({
type: 'EDIT_ITEM',
id: Date.now(),
text: itemInputRef.current.value,
index,
})
}
Which is just passing the value of an input
<input
autoFocus
type='text'
ref={itemInputRef}
onKeyDown={(e) => {
if (e.key === 'Escape') {
setToggle(!toggle)
}
if (e.key === 'Enter') {
// Dispatch
editItemHandler()
setToggle(!toggle)
}
}}
/>
My reducer file looks like this:
const itemReducer = (state, action) => {
switch (action.type) {
case 'ADD_ITEM': {
return [
...state,
{
id: action.id,
text: action.text,
},
]
}
case 'EDIT_ITEM': {
// Attempt 1
return [...state.splice((item, index) => index, 1, action.text)]
// Attempt 2
return [
...state.filter((item, index) => index !== action.index),
{
id: action.id,
text: action.text,
},
]
}
case 'DELETE_ITEM': {
return [...state.filter((item, index) => index !== action.index)]
}
default: {
return state
}
}
}
export default itemReducer
I've commented in 2 approaches I've already tried in the 'EDIT_ITEM' type.
Approach 1 just deletes the item and adds a new valued one albeit at the bottom of the array, which isn't what I want so I'd have to try and reorder after.
Approach 2 is using splice, which I thought was what would work for replacing an item with the specified value. However all it returns is ONLY the 'edited' with the original text (so not even edited), and deletes everything else.
How am I using this function incorrectly, or is there a better approach to editing an item in place? I'm obviously doing something wrong but can't figure out what. I've searched about and tried various approach to no avail.
Ideally I'd want the item to also keep the same id as before as well, so how to keep that would be a plus.
To update an item in an array you have several choices :
case 'EDIT_ITEM': {
// using map
return state.map((item, i) =>
i === action.index ? { id: action.id, text: action.text } : item
// using slice
return [
...state.slice(0, action.index),
{ id: action.id, text: action.text },
...state.slice(action.index+1)
]
This is an incorrect use of splice
return [...state.splice((item, index) => index, 1, action.text)]
because splice return an array containing the deleted elements, and it doesn't accept an function as first argument but the index at which to start changing the array.
the correct way with splice :
case 'EDIT_ITEM': {
// using splice
let newState = [ ...state ]
newState.splice(action.index, 1, { id: action.id, text: action.text })
// or you can directly do
newState[action.index] = { id: action.id, text: action.text }
// and return the new state
return newState;
My state is as follows
this.state = {
todos: [{
title: 'asas',
status: 'incomplete',
uuid: 11
}, {
title: 'asas',
status: 'incomplete',
uuid: 12
}, {
title: 'asas',
status: 'complete',
uuid: 13
}],
currentTab: "Show All"
}
and whenever a user clicks on any of the todo items's checkBox i want to update the state status of the checkbox and i have written the following code for it
this.state.todos.map(todo => {
if (todo.uuid === uuid) todo.status = (todo.status === 'complete') ? 'incomplete' : 'complete'
});
this.forceUpdate();
Is Using forceUpdate a good approach here? as i have updating only a single value inside an array of objects. Is there a better solution for this problem?
either of the following will call setState with the updated state without modifying the current state.
https://redux.js.org/recipes/structuringreducers/immutableupdatepatterns#inserting-and-removing-items-in-arrays
using the spread operator:
edit.. actually, this is the hard way 8)
see https://redux.js.org/recipes/structuringreducers/immutableupdatepatterns#updating-an-item-in-an-array
this.setState(prevState => {
const idx = prevState.todos.findIndex(todo => todo.uuid === uuid);
return {
todos: [
...prevState.todos.slice(0, idx),
{
...prevState.todos[idx],
status: prevState.todos[idx].status === "complete" ? "incomplete" : "complete",
}
...prevState.todos.slice(idx + 1),
]
}
});
or using immer:
import produce from "immer";
this.setState(prevState => {
const idx = prevState.todos.findIndex(todo => todo.uuid === uuid);
return produce(prevState, draft => {
draft.todos[idx].status = prevState.todos[idx].status === "complete" ? "incomplete" : "complete"
});
});