Consider the following state:
const initState = {
id: {
data: null,
isFetching: false,
fetchingError: null
},
bookmarks: {
IDs: {
news: [],
opps: [],
posts: []
},
data: {
news: [],
opps: [],
posts: []
},
isFetching: false,
fetchingError: null
},
role: null,
membership: null,
}
How do I update just the posts array in the ÌDs array in the bookmarks array? I tried this:
case 'SET_USER_BOOKMARKED_POSTS':
return {
...state,
bookmarks: {
IDs: {
posts: action.payload
}
}
}
But when I log the state to the console the IDs array then only contains posts, while the opps and news arrays are not there anymore.
You need to destruct all inner structure:
case 'SET_USER_BOOKMARKED_POSTS':
return {
...state,
bookmarks: {
...state.bookmarks,
IDs: {
...state.bookmarks.IDs,
posts: action.payload
}
}
}
But this is not very convinient, better use lodash merge
You need use the spread operator for state.bookmarks.IDs also as when specifies keys after the spread operator, the value for the keys are overwritten.
case 'SET_USER_BOOKMARKED_POSTS':
return {
...state,
bookmarks: {
...state.bookmarks
IDs: {
...state.bookmarks.IDs,
posts: action.payload
},
}
}
The way we do it is a non-mutative method using Object.assign to basically point and update a field in the redux state. Something like:
return Object.assign({}, state.bookmarks, {
IDs: action.payload,
});
This will make sure you're not mutating the state, and returns the new bookmarks. You can adjust this to return the entire state if you'd like, it depends on if you need the other data on the update.
Related
I build an app in React with Redux and in my state I have a list of objects and I want to update one object from that list by unique id.
My object looks like:
{
id: '',
title: '',
description: '',
label: '',
}
My state:
const initialState = {
compare: dayjs().month(),
savedEvents: [],
}
When I push a new event in that list I use:
case 'events/setNewEvent':
return { ...state, savedEvents: [...state.savedEvents, action.payload] };
My problem is that I don't know to write the right code to update just one object by id sent from my form.
You can use combination of Array method map and spread operator
function updateOne(array, obj) {
return array.map((item) => {
if (obj.id === item.id) {
// update whatever you want
return {...item, title: obj.title };
} else {
return item;
}
})
}
reducer:
case 'events/setNewEvent':
return {
...state,
savedEvents: updateOne(state.savedEvents, action.payload)
};
I have the following code to update the currentScore of a rubricItem object. This works fine.
case SAVE_SCORELIST_SUCCESS:
const scoreItem = action.payload.scoreItem;
return {
...state,
loading: false,
editing: false,
rubricItems: {
...state.rubricItems,
[scoreItem.rubricItemId]: {
...state.rubricItems[scoreItem.rubricItemId],
currentScore: scoreItem.currentScore,
}
}
};
However, I may receive an array object holding scores for multiple rubricItems instead of updating a single rubricItem with a single scorItem as I did above.
I know I can use .map() to iterate through the array:
scoreItems.map(si=>{})
But, I do not know how I can integrate it into this:
case SAVE_SCORELIST_SUCCESS:
const scoreItems = action.payload.scoreItems;
return {
...state,
loading: false,
editing: false,
rubricItems: {
...state.rubricItems,
[scoreItems[x].rubricItemId]: {
...state.rubricItems[scoreItems[x].rubricItemId],
currentScore: scoreItems[x].currentScore,
}
}
};
Any ideas?
You can try this:
First you need to iterate over scoreItems and make a map object of updated score items.
Once you have done that, you can use the spread operator with the current score items in state.
case SAVE_SCORELIST_SUCCESS:
let updatedScoreItems = {};
action.payload.scoreItem.forEach(scoreitem => {
updatedScoreItems[scoreItem.rubricItemId] = {
...state.rubricItems[scoreItem.rubricItemId],
currentScore: scoreItem.currentScore,
}
})
return {
...state,
loading: false,
editing: false,
rubricItems: {
...state.rubricItems,
...updatedScoreItems
}
};
Instead of mapping over scoreItem, map over the rubricItems which will be cleaner.
const updatedRubricItems = items.rubricItems.map(rubricItem => {
const scoreForRubric = scoreItems.find(si => si.rubricItemId === rubricItem.id);// i assume you have some id for your rubric item
if(scoreForRubric){
return {...rubricItem, currentScore: scoreForRubric.currentScore}
}else {
return rubricItem
}
});
return {
...state,
loading: false,
editing: false,
rubricItems: updatedRubricItems
};
I'm trying some app in react redux and i have a problem with updating (push, remove, update) the nested array in state.
I have some object called service like this:
{
name: 'xzy',
properties: [
{ id: 1, sName: 'xxx'},
{ id: 2, sName: 'zzz'},
]
}
Whatever I did (in case of adding property to collection) in the reducer with the properties collection generate problem that all properties got same values as the last I had recently added -> Added property object is in service properties collection but the action replace all values in all properties in this collection.
My reducer:
export function service(state = {}, action) {
switch (action.type) {
case 'ADD_NEW_PROPERTY':
console.log(action.property) // correct new property
const service = {
...state, properties: [
...state.properties, action.property
]
}
console.log(service); // new property is pushed in collection but all properties get same values
return service
default:
return state;
}
}
I have tried some solution with immutability-helper library and it generate the same problem:
export function service(state = {}, action) {
case 'ADD_NEW_PROPERTY':
return update(state, {properties: {$push: [action.property]}})
default:
return state;
}
For example when I add new property { id: 1, sName: 'NEW'} to example above I will get this state:
{
name: 'xzy',
properties: [
{ id: 1, sName: 'NEW'},
{ id: 1, sName: 'NEW'},
{ id: 1, sName: 'NEW'}
]
}
Can someone help? :)
Make a copy of action.property as well. Whatever is dispatching this action, it could be reusing the same object.
export function service(state = {}, action) {
switch (action.type) {
case 'ADD_NEW_PROPERTY':
console.log(action.property) // correct new property
const service = {
...state,
properties: [
...state.properties,
{ ...action.property }
]
}
console.log(service); // new property is pushed in collection but all properties get same values
return service
default:
return state;
}
}
I'd recommend you to use Immutable data https://facebook.github.io/immutable-js/docs/#/List
import { fromJS, List } from 'immutable';
const initialState = fromJS({
propeties: List([{ id: 1, sName: 'xyz' }]
}
function reducer(state = initialState, action) {
case ADD_NEW_PROPERTY:
return state
.update('properties', list => list.push(action.property));
// ...
}
Your service reducer should probably look somewhat like this:
// Copy the state, because we're not allowed to overwrite the original argument
const service = { ...state };
service.properties.append(action.property)
return service
You should always copy the state before returning it.
export default function(state = {}, action) {
switch(action.type) {
case 'GET_DATA_RECEIVE_COMPLETE': {
const data = action.firebaseData;
const newState = Object.assign({}, state, {
data
});
return newState
}
default:
return state;
}
}
I'm trying to update array inside object in my reducer.
const initialState = {
group: {
name: "",
date: "",
description: "",
users: [],
posts: []
},
morePosts: false,
groups: []
};
export function groups(state = initialState, action) {
switch (action.type) {
.......
case REQUEST_MORE_POSTS:
{
return {
...state,
group:{
...state.group,
posts: [
...state.group.posts,
...action.payload.posts
]
},
morePosts: action.payload.morePosts
}
}
case ADD_NEW_POST:
{
return {
...state,
group:{
...state.group,
posts: [
action.payload,
...state.group.posts
]
}
}
}
........
default:
return state;
}
}
Unfortunately in both cases I get an error:
It works when I extract posts out of my group object but I need it inside.
I can't figure out what I've done wrong here. Can someone point me to the right direction?
Here is an action creator for adding new post.
export function addPost(url, payload) {
return function(dispatch) {
axios.post(url + "php/addPostGroup.php", {payload}).then(response => {
dispatch({
type: ADD_NEW_POST,
payload: response.data.post
})
})
}
}
response.data.post is a simple object.
I've added console.log() before dispatch. This is how my response looks like:
Alright I solved it. Before fetching any data my state.group.posts array was for some reason treated as undefined. I had to manually declare it as empty array using
var posts = state.group.posts != undefined ? state.group.posts:[];
I'm working on a shopping cart and I'm trying to wrap my head around two problems with my app:
Adding items to the store is overwriting previous items in the store:
Initial state:
const initialState = {
items: {},
showCart: false
};
Add to Cart Reducer:
Problem: This works for adding an item to the cart, but when I go to add another item in the cart, it overwrites the previous item. Why would that be / How do I preserve the items in the previous state?
let addToCartState = {...state,
items: {
[action.id]: {
id: action.id,
color: action.product_selection.color,
size: action.product_selection.size,
quantity: 1
}
},
showCart: true
}
return state.merge(addToCartState);
Remove All From Cart Reducer:
Problem: This seems to work, but I can't seem to grab data from the state map. I can't seem to call "state.cart.items" (see mapStateToProps) like I can on my other states.
let removeFromCartState = {...state,
items: {
...state.items
},
showCart: true
}
function mapStateToProps(state) {
console.log(state.cart);
console.log("🙃");
return { products: state.products, items: state.cart.items }
}
state.cart:
Map {size: 8, _root: ArrayMapNode, __ownerID: undefined, __hash: undefined, __altered: false}
size: 8
__altered: false
__hash: undefined
__ownerID: undefined
_root: ArrayMapNode
entries: Array(8)
0: Array(2)
0: "items"
1: Map
size: 0
...
^ No items now (size: 0, was 1 after the previous reducer); do I need to use something like fromJS to parse this now or should I not have to do that?
Edit - combineReducers:
import {combineReducers} from 'redux';
import app from './appReducer';
import products from './productsReducer';
import cart from './cartReducer';
import user from './userReducer';
export default combineReducers({
app: app,
products: products,
cart: cart,
user: user
});
The root of the problem is that you're treating Immutable.js objects like regular JavaScript objects instead of using the built-in Immutable.js features intended for the tasks you're performing.
Problem: This works for adding an item to the cart, but when I go to add another item in the cart, it overwrites the previous item. Why would that be / How do I preserve the items in the previous state?
Let's take a look at your code:
let addToCartState = { ...state,
items: { [action.id]: { /* ... */ } },
showCart: true
};
The spread operator (...) does a "shallow" merge. What your code is doing, essentially, is this:
let addToCartState = shallowCopy(state);
addToCartState.items = { [action.id]: { /* ... */ } };
addToCartState.showCart = true;
In other words, it "overwrites the previous item" because you're replacing the items property with a new object with only one item. One solution is to merge items yourself:
const addToCartState = { ...state,
items: { ...state.items,
[action.id]: { /* ... */ },
},
showCart: true,
};
...but since you're using Immutable.js, you shouldn't do that. You should use its built-in mergeDeep method:
function addToCart(prevState, action) {
const addToCartState = {
items: {
[action.id]: {
color: action.product_selection.color,
// ...
},
},
showCart: true,
};
return prevState.mergeDeep(addToCartState);
}
let state = Immutable.fromJS({ items: {} });
console.log('Original state:', state);
console.log('Add blue thing');
state = addToCart(state, {
id: '123',
product_selection: { color: 'blue' },
});
console.log('State is now:', state);
console.log('Add green thing');
state = addToCart(state, {
id: '456',
product_selection: { color: 'green' },
});
console.log('State is now:', state);
.as-console-wrapper{min-height:100%}
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.min.js"></script>
Problem: This seems to work, but I can't seem to grab data from the state map. I can't seem to call "state.cart.items" (see mapStateToProps) like I can on my other states.
state is not a "plain" JavaScript object, it's an Immutable.Map. You can't access its values like ordinary object properties. One solution is convert it to a plain object using toJS, then retrieve its properties (and sub-properties) like usual. An alternative, which will be preferable if your state object is potentially large, is to retrieve the values using Immutable.js' get and getIn (for "deep" properties). With the latter you'll have to use toJS on the individual values if they're also Immutable objects. You can see both approaches below.
function mapStateToProps(state) {
const obj = state.toJS();
return { products: obj.products, items: obj.cart.items };
}
// or...
function mapStateToPropsAlt(state) {
return {
products: state.get('products').toJS(),
items: state.getIn(['cart', 'items']).toJS(),
};
}
const state = Immutable.fromJS({
products: [ '¯\\_(ツ)_/¯' ],
cart: {
items: {
'123': { id: '123', color: 'blue', /* ... */ },
},
},
});
console.log('mapStateToProps(state) =>', mapStateToProps(state));
console.log('mapStateToPropsAlt(state) =>', mapStateToPropsAlt(state));
.as-console-wrapper{min-height:100%}
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.min.js"></script>