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;
}
}
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 this Redux store and reducer
const INITIAL_WORK = {
departments: []
}
const works = (state = INITIal_WORK, action) => {
switch(action.type){
case 'ADD_DEPARTMENT':
return {
...state,
departments: [...state.department, action.item]
}
default:
return state
}
}
In departments work people so I want to this people was inside a single department works people. So after fetch data from db I want my store look like this:
const INITIAL_WORK = {
departments: [
{
id: 1,
name: "First department",
people: [
{
id: 1,
name: "Johna Wayne"
},
{
id: 2,
name: "Jessica Biel"
},
{
id: 3,
name: "Bratt Pitt"
}
]
},
{
id: 2,
name: "second department",
people: [
{
id: 4,
name: "Salma Hayek"
},
{
id: 5,
name: "Sylvester Stallone"
}
]
}
]
}
Is it possible create case in reducer which will be added people inside people array inside single department? How can I do that?
Yes, absolutely.
Let us assume your API returns payload as following.
{
departmentId: `id of the department`
id: `person id`
name: `person name`
}
const INITIAL_WORK = {
departments: []
}
const works = (state = INITIal_WORK, action) => {
switch(action.type){
case 'ADD_DEPARTMENT':
return {
...state,
departments: [...state.department, action.item]
}
case 'ADD_PERSON':
const { departmentId, id, name } = action.item
const departments = [...state.departments];
departments[departmentId].people.push({id, name})
return {
...state,
departments
}
default:
return state
}
}
Combining Immer.js and Redux will be helpful for this case.
Here is a simple example of the difference that Immer could make in practice.
// Reducer with inital state
const INITAL_STATE = {};
// Shortened, based on: https://github.com/reactjs/redux/blob/master/examples/shopping-cart/src/reducers/products.js
const byId = (state = INITAL_STATE, action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
return {
...state,
...action.products.reduce((obj, product) => {
obj[product.id] = product
return obj
}, {})
}
default:
return state
}
}
After using Immer, our reducer can be expressed as:
import produce from "immer"
// Reducer with inital state
const INITAL_STATE = {};
const byId = produce((draft, action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
action.products.forEach(product => {
draft[product.id] = product
})
}
}, INITAL_STATE)
let mergePeople=[];
INITIAL_WORK.departments.filter((cur,index)=>{
cur.people.filter((person)=>{
mergePeople.push(person);
})
}
)
console.log(mergePeople);
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:[];
In my application i am displaying person data.
I have a scenario where I can input new person. So far I have got this:
Reducer:
export default function (state = [], action) {
console.log("Reducing");
switch (action.type) {
case 'ADD':
console.log("ADDING!");
return action.payload;
break;
}
return state;
}
Action:
export const addPerson = (person) => {
console.log("addPerson Action Fired! ", person);
return {
type: 'ADD',
payload: person
}
};
I have a reducer with few person data which i am showing in my application but i am blocked at this point of adding new person. So far i can see my input in console log but can't figure out how can i add it to the state.
Help would be very much appreciated.
This is the reducer where i am showing the data from.
export default function () {
return ["abc","def","ghi","jkl"]
}
Now i am displaying a list of these elements from the array(each are one person). When i add one it will be added with these persons and show me the updated list.
You should do the following in your reducer:
export default function (state = [], action) {
console.log("Reducing");
switch (action.type) {
case 'ADD':
return [
...state,
action.payload,
]
}
return state;
}
In this way, you add the person to your existing array. Otherwise you changes your current state value from an array to an object.
Let's consider your person object value is the following and you dispatch it:
{
type: 'ADD',
payload: {
id: 1,
name: 'John Doe',
}
}
With your previous code, you would have the current value:
// First step
return action.payload;
// Second step
return { id: 1, name: 'John Doe' };
With the corrected code:
// First step
return [
...state,
action.payload,
]
// Second step
return [
...[],
{ id: 1, name: 'John Doe' },
]
// Third step
return [{ id: 1, name: 'John Doe' }];
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>