Beginner Redux/React - Add single item if not in state - javascript

I'm trying to add a single item to my nested Redux state, if that item isn't already in the list.
I don't care about doing it in plain JS, I want the Redux sauce ofc.
My state looks like this:
state: {
data: {
list: []
},
},
This is what I tried in my reducer:
return {
...state,
data: {
...state.data,
list: [action.payload.list, ...state.data.list]
}
}
Problem is, sometimes the item I want to add is already in the array, therefore I have a duplicate in my list (which I don't want).
How can I add an item to my list while checking first if this item is
already there?

You can use some method. It will return true or false and stop iterating over the array as soon as there is a match. Example:
case SOME_ACTION: {
// iterate over the list and compare given item to item from action
const existsInArray = state.data.list.some(l => l.id === action.payload.list.id)
if(existsInArray) {
return state;
}
return {
...state,
data: {
...state.data,
list: [action.payload.list, ...state.data.list]
}
}
}
EDIT:
You wrote in a comment that if the element exists in the array you want to make that it will be the first item in the list. What I would do is filter out this element (if exists) and then put the item back from the action as the first element:
case SOME_ACTION: {
// filter out the element
const copyOfArray = state.data.list.filter(l => l.id !== action.payload.list.id)
// put to the beginning of the array
copyOfArray.unshift(action.payload.list);
return {
...state,
data: {
...state.data,
list: copyOfArray
}
}
}

Element won't be duplicated and always at 0 index,
return {
...state,
data: {
...state.data,
list: [].concat(action.payload.list, state.data.list.filter(val=>val.id==action.payload.list.id))
}
}

Related

What is the most comfortable/conventional way to modify a specific item in an array, in a React component state?

I often find my self struggling with manipulating a specific item in an array, in a React component state. For example:
state={
menus:[
{
id:1,
title: 'something',
'subtitle': 'another something',
switchOn: false
},
{
id:2,
title: 'something else',
'subtitle': 'another something else',
switchOn: false
},
]
}
This array is filled with objects, that have various properties. One of those properties is of course a unique ID. This is what i have done recentely to edit a "switchOn" property on an item, according to its ID:
handleSwitchChange = (id) => {
const newMenusArray = this.state.menus.map((menu) => {
if (menu.id === id) {
return {
...menu,
switchOn: !menu.switchOn
};
} else {
return menu;
};
})
this.setState(()=>{
return{
menus: newMenusArray
}
})
}
As you can see, alot of trouble, just to change one value. In AngularJS(1), i would just use the fact that objects are passed by reference, and would directly mutate it, without any ES6 hustle.
Is it possible i'm missing something, and there is a much more straightforward approach for dealing with this? Any example would be greatly appreciated.
A good way is to make yourself a indexed map. Like you might know it from databases, they do not iterate over all entries, but are using indexes. Indexes are just a way of saying ID A points to Object Where ID is A
So what I am doing is, building a indexed map with e.g. a reducer
const map = data.reduce((map, item) => {
map[item.id] = item;
return map;
}, {})
now you can access your item by ID simply by saying
map[myId]
If you want to change it, you can use than object assign, or the ... syntax
return {
// copy all map if you want it to be immutable
...map
// override your object
[id]: {
// copy it first
...map[id],
// override what you want
switchOn: !map[id].switchOn
}
}
As an helper library, I could suggest you use Immutable.js, where you just change the value as it were a reference
I usually use findIndex
handleSwitchChange = (id) => {
var index = this.state.menu.findIndex((item) => {
return item.id === id;
});
if (index === -1) {
return;
}
let newMenu = this.state.menu.slice();
newMenu[index].switchOn = !this.state.menu[index].switchOn;
this.setState({
menu: newMenu
});
}

Merge array with nested objects if id matches or add to the end

I am trying to assign clone/merge object, that has array where id matches or add to an end:
newState = Object.assign({}, state, {
data: {
newest: {
result: action.payload.result,
list: action.payload.items,
request: action.payload.items
},
itemList: [
...state.data.itemList,
{
id: action.payload.id,
list: action.payload.items,
request: action.payload.items
}
]
}
});
In this case ...state.data.itemList is an array with objects where I want to find an existing object with ID and merge list + request nested objects. However, if there is no object with that ID I want to add it to the list.
Current approach always adds it to the end, which of course is not what I want.
Thanks.
Don't feel pressured into making the new state in one operation. Here would be my implementation. I would check my array for the current id using Array#some. Here are some fun ES6 tricks too.
const newState = {...state}
const {result, items, id, items} = action.payload
const data = {newest: {result, list: items, request: items}}
const hasId = newState.data.itemList.some(item => item.id === id)
if (!hasId) {
newState.data.itemList.push({id, list: items, request: items})
}
return {...newState, data}

Update single value in item array | react redux

I have a todo list and want to set the state of that item in the array to "complete" if the user clicks on "complete".
Here is my action:
export function completeTodo(id) {
return {
type: "COMPLETE_TASK",
completed: true,
id
}
}
Here is my reducer:
case "COMPLETE_TASK": {
return {...state,
todos: [{
completed: action.completed
}]
}
}
The problem I'm having is the new state does no longer have the text associated of that todo item on the selected item and the ID is no longer there. Is this because I am overwriting the state and ignoring the previous properties? My object item onload looks like this:
Objecttodos: Array[1]
0: Object
completed: false
id: 0
text: "Initial todo"
__proto__: Object
length: 1
__proto__: Array[0]
__proto__: Object
As you can see, all I want to do is set the completed value to true.
You need to transform your todos array to have the appropriate item updated. Array.map is the simplest way to do this:
case "COMPLETE_TASK":
return {
...state,
todos: state.todos.map(todo => todo.id === action.id ?
// transform the one with a matching id
{ ...todo, completed: action.completed } :
// otherwise return original todo
todo
)
};
There are libraries to help you with this kind of deep state update. You can find a list of such libraries here: https://github.com/markerikson/redux-ecosystem-links/blob/master/immutable-data.md#immutable-update-utilities
Personally, I use ImmutableJS (https://facebook.github.io/immutable-js/) which solves the issue with its updateIn and setIn methods (which are more efficient than normal objects and arrays for large objects with lots of keys and for arrays, but slower for small ones).
New state does no longer have the text associated of that todo item on
the selected item and the ID is no longer there, Is this because I am
overwriting the state and ignoring the previous properties?
Yes, because during each update you are assigning a new array with only one key completed, and that array doesn't contain any previous values. So after update array will have no previous data. That's why text and id's are not there after update.
Solutions:
1- Use array.map to find the correct element then update the value, Like this:
case "COMPLETE_TASK":
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id ? { ...todo, completed: action.completed } : todo
)
};
2- Use array.findIndex to find the index of that particular object then update that, Like this:
case "COMPLETE_TASK":
let index = state.todos.findIndex(todo => todo.id === action.id);
let todos = [...state.todos];
todos[index] = {...todos[index], completed: action.completed};
return {...state, todos}
Check this snippet you will get a better idea about the mistake you are doing:
let state = {
a: 1,
arr: [
{text:1, id:1, completed: true},
{text:2, id:2, completed: false}
]
}
console.log('old values', JSON.stringify(state));
// updating the values
let newState = {
...state,
arr: [{completed: true}]
}
console.log('new state = ', newState);
One of the seminal design principles in React is "Don't mutate state." If you want to change data in an array, you want to create a new array with the changed value(s).
For example, I have an array of results in state. Initially I'm just setting values to 0 for each index in my constructor.
this.state = {
index:0,
question: this.props.test[0].questions[0],
results:[[0,0],[1,0],[2,0],[3,0],[4,0],[5,0]],
complete: false
};
Later on, I want to update a value in the array. But I'm not changing it in the state object. With ES6, we can use the spread operator. The array slice method returns a new array, it will not change the existing array.
updateArray = (list, index,score) => {
// updates the results array without mutating it
return [
...list.slice(0, index),
list[index][1] = score,
...list.slice(index + 1)
];
};
When I want to update an item in the array, I call updateArray and set the state in one go:
this.setState({
index:newIndex,
question:this.props.test[0].questions[newIndex],
results:this.updateArray(this.state.results, index, score)
});

"Updating" all state in redux app properly

I've met some trouble assigning a new object in the reducer of my app. My state contains 2 arrays :
{
elements: [],
constraints: []
}
Those elements are handled by 2 reducers :
elementsReducer
constraintsReducer
and combined like this:
let reducer = combineReducers({
elements: elementsReducer,
constraints: constraintsReducer
});
export default reducer
So, basically, an action is triggered, and my reducer is supposed to update all the state.elements array. I've tried several things and I can't update the whole elements array, only - in the best case - the first element.
My first idea was to do:
return Object.assign({}, state, {
elements: state.map((e) => {
return Object.assign({}, e, {
text: action.data[e.id][e.text]
})
})
});
action.data is an array containing a different text for each element. Basically, all I was to do is, on a special action, updating all the element array. But this syntax does not work as it creates a new array INSIDE the array "elements" of the store. It does not replace it. If I let this, the store becomes:
{
elements: [
elements: [...]
],
constraints: [...]
}
When I access the state in my reducer elementsReducer, it's only the "element" array and not the full state. After this issue, I've tried to do the following:
return state.map(function(e) {
return assign({}, e, {
text: action.data[e.id][e.text]
});
});
Now, I worked, but the ONLY element mapped is the first one. The other elements are simply not updating.
Do you have any idea to solve the issue?
Thanks everyone :)
Xelys
EDIT :
// code of elementsReducer
var assign = require('object-assign');
export default function elementsReducer(state = {}, action) {
switch (action.type) {
case 'ADD_ELEMENT':
return [...state,
{
name: action.name,
id: action.id,
committed: false,
text: action.text
}
]
case 'COMMIT_ELEMENT':
console.log('commit action')
return state.map(function(e) {
return e.id === action.id ?
assign({}, e, {committed: true}) :
e
});
case 'SAVE_DATA':
return state.map((e) => {
return Object.assign({}, e, {
text: action.data[e.id][e.text]
});
});
default:
return state;
}
}
Based on your code, I assumed your data structure is like below:
// state.element
stateElement = [
{ id:1, text: '1t' },
{ id:2, text: '2t' }
];
// Your action result
action = {
data: {
1: { text: 'new 1t' },
2: { text: 'new 2t' }
}
}
// Your new state.element
newData = data.map(function(e) {
return Object.assign({}, e, {
text: action.data[e.id].text
});
});
thanks for the answers.
#Ali Sepehri.Kh, yeah, my data structure is very similar. Actually, it's a little bit more complex, but I've simplified it to be more understable.
However, I figured out to solve the issue. I feel quite ashamed of creating a post on stackoverflow, because the error had nothing to do with redux.
The mapping function I've used was totally working. The issue was located is the action data. The function which created the action.data array was returning an array empty after the first element. I thought the issue was coming from the map() as it was for me the "most difficult" part of the fonction.
However, I've made a stupid mistake on the fonction creating the array of the action. I have misplaced a "return" inside a for loop, which explain that all the elements after the first one were empty.
Sorry for your time guys, and thanks for the help :).

State Object structure changed

I´ve written a small toDo-list app with Database interaction. Everything works fine, except that when I try to remove a toDo-list item from my state, the object-state structure is changed.
Therefore I´m not longer able to render my ToDo-list on runtime.
I assume that just a small change in my reducer code is required, but I´m a little bit to experienced with ES6 & Redux to get it on my own.
This is my Reducer Code:
export default (state = {items: []}, action) => {
switch (action.type){
case 'RECEIVE_POSTS':
return Object.assign({}, state, {
items: action.items
});
case 'ADD_ITEM':
return Object.assign({}, state, {
items: [
...state.items,
action.items
]
});
case 'REMOVE_ITEM':
return Object.assign({}, state, {
items: [
state.items.filter((items, id) => id !== action.id)
]
});
default:
return state
}
}
And my deleteItem - Action
export default function deleteItem(id){
console.log("test")
return (dispatch, getState) => {
return dispatch (removeItem(dispatch, id))
return dispatch (receiveDelete(id))
}
}
function removeItem(dispatch, id){
return dispatch => {
$.ajax({
method: "POST",
url: "http://localhost:444/localWebServices/removeTask.php",
data: {id: id}
}).success(function(msg){
console.log(msg)
dispatch(receiveDelete(id))
});
}
}
function receiveDelete(id){
console.log("receive add")
return {
type: 'REMOVE_ITEM',
id: id
}
}
And Here a two Images of the Object Structure before and after I deleted an Item.
To make this clear. Everything works (the list is rendered), but after I deleted an Item the State structure is changed -> List not rendered. I don´t get any Error msg.
Before delete:
After delete:
Any help would be great!
Update:
I have changed my Code and do not return an array anymore. According to the dev console the state seems to be correct now, but it´s not rendering. Does this code goes in the right direction?
return Object.assign({}, state, {
items: state.items.filter((items, id) => id !== action.id)
});
Arguments passed to filter callback are arrayElement, index, array so try changing your callback to .filter(item => item.id !== action.id)

Categories

Resources