React Redux reducer is overwriting values LOAD_SUCCESS? - reactjs

I have a profilesReducer that I want to use to store 1 or more user profiles in my redux store. As an example think Twitter, which needs to store my profile as well as other profiles.
Here is my profilesReducer.js:
import * as types from '../actions/actionTypes';
const initialState = []
export default function profilesReducer(state = initialState, action) {
switch (action.type) {
case types.LOAD_PROFILE_SUCCESS:
return [Object.assign({}, action.profile)]
case types.UPDATE_PROFILE_SUCCESS:
return [
...state.filter(profile => profile.id !== action.profile.id),
Object.assign({}, action.profile)
]
default:
return state;
}
}
The problem is LOAD is receiving more than one profile (distinguished by profile.id) but is overwriting the existing profile in the store instead of appending/updating.
I need LOAD_PROFILE to allow for more than 1 user's profile in the store. Any suggestions?

Spread existing state and just add new profile at the end:
case types.LOAD_PROFILE_SUCCESS:
return [...state, action.profile]
Object.assing({}, action.profile) is unnecessary, since all that it does here is copying action.profile into a new empty object {} and returns it.

Pretty sure your code needs to change to this.
import * as types from '../actions/actionTypes';
const initialState = []
export default function profilesReducer(state = initialState, action) {
switch (action.type) {
case types.LOAD_PROFILE_SUCCESS:
return [...Object.assign({}, action.profile)]
case types.UPDATE_PROFILE_SUCCESS:
return [
...state.filter(profile => profile.id !== action.profile.id),
Object.assign({}, action.profile)
]
default:
return state;
}
}
The way you had it, you were simply returning an array containing this one item in action.profile, but you need to add your item in to the array that already has the other items. You also cannot simply push as that would mutate the array. This line is all you need to change.
return [Object.assign({}, action.profile)]
Does not append
return [...Object.assign({}, action.profile)]
does append and does not mutate.

You can achieve what you want with either using Object.assign or the spread operator syntax, You need not use both
With Object.assign,
case types.LOAD_PROFILE_SUCCESS:
return Object.assign([], state, action.profile)
With Spread operator syntax
case types.LOAD_PROFILE_SUCCESS:
return [...state, action.profile]
However React docs advise you to use Spread syntax over Object.assign, See this post:
Using Object.assign in React/Redux is a Good Practice?

Edit
I have changed my solution, it includes a way to update an array.
Here is a CodeSandbox to test: https://codesandbox.io/s/zk9r6k1z4p
import * as types from '../actions/actionTypes';
const initialState = []
export default function profilesReducer(state = initialState, action) {
switch (action.type) {
case types.LOAD_PROFILE_SUCCESS:
return [...state, action.profile]
case types.UPDATE_PROFILE_SUCCESS:
return state.map( (profile) => {
if(profile.id !== action.profile.id) {
return profile;
}
return {
...profile,
...action.profile
};
});
default:
return state;
}
}

Related

If state contains object containing one property, should we still create a copy and then change it in reducer

I am learning React, and came across the term "reducer". I have an object that looks like this:
{
count: 0,
}
My question is:
Should we still create a copy and then change it in the reducer?
See my code below:
const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREASE':
return { ...state, count: state.count + 1 };
case 'DECREASE':
return { ...state, count: state.count - 1 };
default:
return state;
}
};
Should we still use ...state even if we have only one property count?
Or, is it okay to just change the value of count directly? Like this: return { count: state.count + 1 };.
This is not bad to keep the spread but if you're facing perf issues you can consider not using it. Since it's a good practice to conserve a solid state you maybe can externalize the concept in a function
const setCount => (state, newCount) => ({
// ...state, // If you want later spread state
count: newCount
})
const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREASE':
return setCount(state, state.count + 1);
case 'DECREASE':
return setCount(state, state.count - 1);
default:
return state;
}
};
Alternatively, if you have only one property, you maybe not need an object and use the state directly :
// This way `state = 0` instead of `{ count = 0}`
const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREASE':
return state + 1;
case 'DECREASE':
return state - 1;
default:
return state;
}
};
Looking at the redux doc : https://redux.js.org/basics/reducers#splitting-reducers : there is many ways to use state. Some of them are not even relative to state but comes from action directly. In all the cases, if you keep the reducer safe and well documented (if it's not the general usecases / standard), you're good :)
PS: I recommand you to read the native react useReducer fonctionnality : https://fr.reactjs.org/docs/hooks-reference.html#usereducer since it cover the main useCases and can replace easly the redux framework.

Update value in array in reducer

I have a reducer
const initialState = {
elements: [{"flag": false}, {"flag": false}]
};
const checkReducer = function (state = initialState, action) {
switch (action.type) {
case CHANGE_CHECK:
return {...state, //here I want update element by index, that pass in action};
I need to update an existing value in array elements, that I get by the index passed in action.
I can do it like this
state.elements[action.key] = {"flag": !action.flag}
but then I'll change existing state. according to the redux principles, I can't do it.
So I have to use spread operator and change new object. But I don't know how to use it this way.
Tried something like this
...state, elements[action.index]: {"flag": !action.flag}]
but it isn't worked. Is there a way to do what I want?
return {
...state,
elements: state.elements.map((element, index) => {
if (index === action.key) {
return {"flag": !action.flag}
}
return element
})
}
array#map will create a new array, and change only the item whose index match action.key.
If you find this process tedious, you could use libraries that let mutate your state while keeping the reducer returning new state. One of those is immer.

React Native redux reducers best way to push array

I have the following state and reducer but it's not pushing in the new array object.
const initialState = {
photos: [],
selectedPhoto:{},
photosTeamId:'',
photosProjectId:''
};
case actionTypes.PHOTOS_UPDATE:
return Object.assign({}, state, {
photos:action.data.photos,
photosTeamId:action.data.photosTeamId,
photosProjectId:action.data.photosProjectId
})
photos is not getting pushed but overwritten
Here's a more cleaner way using javascript spread syntax:
const initialState = {
photos: [],
selectedPhoto:{},
photosTeamId:'',
photosProjectId:''
};
case actionTypes.PHOTOS_UPDATE:
return {
...state,
photos: [...state.photos, ...actions.data.photos],
photosTeamId: action.data.photosTeamId,
photosProjectId: action.data.photosProjectId
}
case actionTypes.PHOTOS_UPDATE:
return {
...state,
photos: state.photos.concat(action.data.photos),
photosTeamId: action.data.photosTeamId,
photosProjectId: action.data.photosProjectId
};
Here's the spread operator […]. The spread operator can be used to take an existing array and add another element to it while still preserving the original array.
Example:
case actionTypes.PHOTOS_UPDATE:
return [
...state,
Object.assign({}, {
photos:action.data.photos,
photosTeamId:action.data.photosTeamId,
photosProjectId:action.data.photosProjectId
})
];

Redux reducer, check if value exists in state array and update state

So I've got an array chosenIds[] which will essentially hold a list of ids (numbers). But I'm having trouble accessing the state in my reducer to check whether the ID I parsed to my action is in the array.
const initialState = {
'shouldReload': false,
'chosenIds': [],
};
export default function filter(state = initialState, action) {
switch (action.type) {
case ADD_TYPE:
console.log(state.chosenIds, "Returns undefined???!!!");
// Check if NUMBER parsed is in state
let i = state.chosenIds.indexOf(action.chosenId);
//If in state then remove it
if(i) {
state.chosenIds.splice(i, 1);
return {
...state.chosenIds,
...state.chosenIds
}
}
// If number not in state then add it
else {
state.chosenIds.push(action.chosenId)
return { ...state.chosenIds, ...state.chosenIds }
}
I'm not to sure what's going on...But when I log state.chosenIds, it returns undefined? It doesn't even return the initial empty array [] .
Basically what this function is suppose to do is check to see if the action.chosenId is in the state.chosenIds, If it is, then remove the action.chosenId value, if it's not, then add the action.chosenId to the state.
I'm seeing a few different issues here.
First, you're using splice() and push() on the array that's already in the state. That's direct mutation, which breaks Redux. You need to make a copy of the array, and modify that copy instead.
Second, the object spread usage doesn't look right. You're using it as if "chosenIds" was an object, but it's an array. Also, you're duplicating the spreads. That's causing the returned state to no longer have a field named "chosenIds".
Third, Array.indexOf() returns -1 if not found, which actually counts as "truthy" because it's not 0. So, the current if/else won't do as you expect.
I would rewrite your reducer to look like this:
export default function reducer(state = initialState, action) {
switch(action.type) {
case ADD_TYPE:
let idAlreadyExists = state.chosenIds.indexOf(action.chosenId) > -1;
// make a copy of the existing array
let chosenIds = state.chosenIds.slice();
if(idAlreadyExists) {
chosenIds = chosenIds.filter(id => id != action.chosenId);
}
else {
// modify the COPY, not the original
chosenIds.push(action.chosenId);
}
return {
// "spread" the original state object
...state,
// but replace the "chosenIds" field
chosenIds
};
default:
return state;
}
}
another aproach with a standalone function:
export default function reducer(state = initialState, action) {
switch(action.type) {
case ADD_TYPE:
function upsert(array, item) {
// (1)
// make a copy of the existing array
let comments = array.slice();
const i = comments.findIndex(_item => _item._id === item._id);
if (i > -1) {
comments[i] = item;
return comments;
}
// (2)
else {
// make a copy of the existing array
let comments = array.slice();
comments.push(item);
return comments;
}
}
return {
...state,
comments: upsert(state.comments, action.payload),
};
default:
return state;
}
}

reducer: adding to array data

if i pull some data from an external source fro the initial state, then want to add additional information like for example 'liked'?
i've tried adding to the products array but its go messy, I'm thinking i should have an additional array for liked items then put the product id in this, the only thing is i need it to reflect in the product that it has been liked and I'm mapping the product data to the item.
whats the best way to go about this ?
const initialState = {
isFetching: false,
products: [],
};
should i add favs: [] ?
how would i reflect the liked state to my product as I'm mapping the products array to the product component? and the liked state is now in the favs?
i tried doing this to add it to the product array but it got really messy (something like this)
case ADD_LIKED:
state.products[action.index]['liked'] = true;
return state;
state.products[action.index]['liked'] = true;
The problem here is that you are mutating the state inside the reducer which is one of the things you should never do inside a reducer.
You'll find that writing functions which don't mutate the data are much easier if you break them down into smaller parts. For instance you can start to split your application up.
function productsReducer(products = [], action) {
// this reducer only deals with the products part of the state.
switch(action) {
case ADD_LIKED:
// deal with the action
default:
return products;
}
}
function app(state = {}, action) {
return {
isFetching: state.isFetching,
products: productsReducer(state.products, action)
}
}
In this case I would definitely want to write a little immutability helper.
function replaceAtIndex(list, index, replacer) {
const replacement = replacer(list[index]);
const itemsBefore = list.slice(0, index),
itemsAfter = list.slice(index + 1);
return [...itemsBefore, replacement, ...itemsAfter];
}
You can complement this with a generic function for changing objects in lists.
function updateInList(list, index, props) {
return replaceAtIndex(list, index, item => {
return { ...props, ...item };
});
}
Then you can rewrite your function in the immutable form
switch(action) {
case ADD_LIKED:
return updateInList(products, action.index, { liked: true });
default:
return products;
}
You could even get fancy by partially applying the function. This allows you to write very expressive code inside your reducers.
const updateProduct = updateInList.bind(this, products, action.index);
switch(action) {
case ADD_LIKED:
return updateProduct({ liked: true });
case REMOVE_LIKED:
return updateProduct({ liked: false });
default:
return products;
}

Resources