I have built a cart app with this reducer in reactjs/redux:
const initialState = {
items: [],
cartOpen: false,
total: 0
}
const Cart = (state = initialState, action) => {
switch (action.type) {
case 'ADD_TO_CART':
let newstate = [...state, action.payload];
var newTotal = 0;
newstate.forEach(it => {
newTotal += it.item.price;
});
newstate.total = newTotal;
newstate.cartOpen =true
return newstate;
case 'TOGGLE_CART':
debugger;
return !state.cartOpen;
default:
return state
}
}
export default Cart;
I am trying to set the state for the cart ie open but when I check the logs the cart property is updated and not the cartOpen property?
Redux assumes that you never mutate the objects it gives to you in the
reducer. Every single time, you must return the new state object.
Even if you don't use a library like Immutable, you need to completely
avoid mutation.
case 'TOGGLE_CART':
return !state.cartOpen;
Doing ^^ this is mutating your state (corrupting your state object). When you don't guarantee immutability, Redux loses its predictability and efficiency.
To achieve immutable state, we can use vanilla Object.assign or its more elegant alternative object spread syntax.
case 'TOGGLE_CART':
return {
...state,
cartOpen: !state.cartOpen
}
Your reducer must always return the complete slice of the app's state for which it is responsible. For TOGGLE_CART, you are only returning the boolean value for openCart.
Instead, create a copy of the previous state object and only update the single property you want to change:
case 'TOGGLE_CART':
return Object.assign({}, state, {
cartOpen: !state.cartOpen
});
Related
In one of my application's reducers, I require to replace an entire array. Hence, I have written my reducer like this:
const arrayReducerDefaultState = []
const arrayReducer = (state = arrayReducerDefaultState, action) =>{
switch(action.type){
case "CHANGE_ARRAY":
return{
...state,
array: action.array
}
default:
return state
}
}
export default arrayReducer
When the action is called, I see the array is being updated (using Redux Dev Tools). But the application does not render again when the 'array' value is updated.
How do I change my reducer so that I can keep replacing my entire array while allowing the application to re-render when the store changes.
EDIT 1:
const arrayReducerDefaultState = {
array: []
}
const arrayReducer = (state = arrayReducerDefaultState, action) =>{
switch(action.type){
case "CHANGE_ARRAY":
return {
...state,
array: action.array
}
default:
return state
}
}
Since I was having problems working with the default state being an array, I made it such that it is an object with an array in it. 'array' now is not undefined but changing it still does not re-render the application.
Tour state is array, but you return object in "CHANGE_ARRAY" case.
const arrayReducerDefaultState = []
const arrayReducer = (state = arrayReducerDefaultState, action) =>{
switch(action.type){
case "CHANGE_ARRAY":
return action.array
default:
return state
}
}
export default arrayReducer
Actually you can have mistake in selector.
PS action has struct like object where must be type (required), payload (optinal) - there is you data, meta (optinal) - another additional inforamtion
The above solution works, but my problem was somewhere else.
After processing the action that comes to the reducer function, i change the type of action according to the operation i do. Would it be a problem if we think that the reducer functions should be pure? To ensure that the reducer function does not grow, and that the incoming data makes the excess change on the state once.
a simple example showing what i mean:
const lineReducer = (state = [], action) => {
switch (action.type) {
case 'ADD_LINE':
action.type = 'INC';
return [...state,action.payload];
case 'UPDATE_LINE':
return state.map(vehicle => vehicle);
case 'TAKE_VEHICLE_IN_LINE':
action.type = 'DEC';
return state.filter(vehicle => vehicle.id !== action.payload);
default:
return state;
}
}
const lineStatsReducer = (state = 0, action) => {
switch (action.type) {
case 'INC':
return state + 1;
case 'DEC':
return state - 1;
default:
return state;
}
}
compineReducer({
line: lineReducer,
stats: lineStatsReducer
});
In this way, my component, which deals with the number of vehicles, is not rendered whenever vehicle data comes.
Yeah, definitely do not mutate action objects like that. It goes against the principle that reducers are supposed to be pure functions, and it's also going to make it a lot harder to trace how and why state is actually updating.
Instead, you should have both reducers listening to the same action type, and independently updating their state appropriately in response. This is easier if you model actions as "events" conceptually.
Have a small problem with fetching and based on response updating an array inside my state in Redux.
First I have done the whole array update with forEach in actions (based on my initial state object) and sent it ready to reducer, it worked. Simple.
But then read tutorials that modifying should be done only in the reducer, and that action should only deal with getting the response. So I have tried doing it this way, two ways, both failed.
The payload i have dispatched to reducer in both cases was just the ready response i have got.
Can someone please enlighten me what went wrong and what's the correct way to do this in reducer?
Both approaches didn't work:
export const handleMusicCards = (state = musicState, action = {}) => {
switch (action.type) {
case REQUEST_MUSIC_SUCCESS:
return Object.assign({}, state, {
musicStateItemList: state.musicStateItemList
.forEach((el, i) => {
el.track = action.payload.message.body.track_list[i].track.track_name;
el.album = action.payload.body.track_list[i].track.album_name;
el.artist = action.payload.body.track_list[i].track.artist_name;
el.id = action.payload.body.track_list[i].track.track_id;
el.favClicked = false;
el.addedToFav = false;
}),
isLoading: false
});
}
}
export const handleMusicCards = (state = musicState, action = {}) => {
switch (action.type) {
case REQUEST_MUSIC_SUCCESS:
return Object.assign({}, state, {
musicStateItemList: state.musicStateItemList
.forEach((el, i) => {
return {
...el,
track: action.payload.message.body.track_list[i].track.track_name,
album: action.payload.message.body.track_list[i].track.album_name,
artist: action.payload.message.body.track_list[i].track.artist_name,
id: action.payload.message.body.track_list[i].track.track_id,
favClicked: false,
addedToFav: false,
}
}),
isLoading: false
});
}
}
I am not sure after reading it where the failure is occurring. A little more about redux conventions.
The action objects are only to describe what changes should be made to the state. The reducer is where the state should actually be changed. In redux, you never want to modify the state object, instead you want to copy it and return a new object with the changes, as described by the action objects.
So you might have a reducer case that looks something like this...
const reducer = (state, action) => {
switch (action.type) {
case NEW_RECORD_SUBMIT :
return {
...state,
newRecordStatus: action.status,
};
default :
return state;
}
};
It's solved now. Very silly mistake, wrong case in switch statement...Went for the second option I tried, with map()
I am learning Redux. I am kind of new so I dont want to develop a bad habit at the start which is why i am writing this.
The doc i am reading says that in the reducers we need to always set the state where we change it - in other words do the following:
const someReducer = (state=initState, {type, payload}) => {
switch(type) {
case 'CHANGE_VALUE_ONE' : {
state = {...state, valueOne: payload }
break
}
case 'CHANGE_VALUE_TWO' : {
state = {...state, valueTwo: payload }
break
}
default:
state = state
}
return state
}
My approach was creating a newState constant which is identical to the state we receive, make change to the dummy and return it - or in code:
const userReducer = (state= {name: null, age: null}, {type, payload}) => {
const newState = {...state}
switch(type) {
case 'CHANGE_VALUE_ONE' : {
newState.valueOne= payload
break
}
case 'CHANGE_VALUE_TWO' : {
newState.valueTwo= payload
break
}
}
return newState
}
I think the way i do it is cleaner - if i have a complicated state i can easily change what i need to change without mutating the original state.
With first snippet, you are violating the principle of pure reducer. That means instead of updating the value directly in state, create a new copy of it and do the changes in that.
As per Doc:
Reducers are just pure functions that take the previous state and an
action, and return the next state. Remember to return new state
objects, instead of mutating the previous state.
Which you are clearly violating by assigning new value to state, here:
state = {...state, valueOne: payload }
Note: Don't forget const newState = {...state}, will only create the shallow copy, so if you have nested state, this will also violate the principle.
In case of nested state, update it in this way:
var obj = { car: { model: '', num: '' } };
function update() {
return {
...obj,
car: {
...obj.car,
model: 'abc'
}
}
}
console.log('new obj = ', update());
console.log('old obj = ', obj);
I think you overcomplicate yourself. Here's a reducer that I use (as an example):
import * as types from '../actions/types';
import { Enums } from '../helpers/enums';
const initialState = {
testState: Enums.command.STOP
};
export default function(state = initialState, action) {
switch (action.type) {
case types.SET_CREEP_TEST_STATE:
return {...state, testState: action.testState};
case types.SET_CREEP_TEST_TIME:
return {...state, testime: action.testime};
default: return state;
}
};
It shouldn't be any more complex than that.
Always keep the state immutable, use pure function. And there is a performance issue for your approach.
Your approach is immutable. But remember, any dispatched action will go through all reducers. That means even the action is not related to your reducer, you still create a new shallow copy of it. It's not necessary. Don't do this
// This newState will be created by every action
const newState = {...state}
switch(type) {
case 'CHANGE_VALUE_ONE' : {
newState.valueOne= payload
break
}
case 'CHANGE_VALUE_TWO' : {
newState.valueTwo= payload
break
}
}
return newState
Your solution isn't immutable, the doc's one is. In more complex cases, I recommend to you use immer, the lib approach is similar to yours, but you will mutate a copy of the state and then immer will merge this copy with the actual state to create a new one. Helps you to work with nested data and bring immutability to reducers. Your case, with immer:
const userReducer = produce(state, draft => {
switch(type) {
case 'CHANGE_VALUE_ONE' : {
draft.valueOne= payload
break
}
case 'CHANGE_VALUE_TWO' : {
draft.valueTwo= payload
break
}
}
},{name: null, age: null})
I wonder if it is possible to update multiple properties of state with immer.js in one "call".
Say I have state:
export const initialState = {
isUserLogged: false,
menuIsClosed: false,
mobileMenuIsClosed: true,
dataArray: ["dog","cat"],
};
And action creator:
export function updateSearchPage(data) {
return {
type: UPDATE_SEARCH_PAGE,
payload: data
};
}
I then use that action creator in React component like this:
this.props.updateSearchPage({
isUserLogged: true,
menuIsClosed: true,
dataArray: ["dog","cat","owl"]
})
The idea is that I want to update several properties of state at the same time. But I dont know which properties it is in advance. I know how to do it with a simple reducer:
case UPDATE_SEARCH_PAGE:
return Object.assign({}, state, action.payload)
But how to update several properties of state with immer at the same time? When the state properties (which one should update) are unknown in advance.
You can cycle on action.payload like the following:
const yourReducer = (state, action) =>
produce(state, draft => {
switch (action.type) {
case UPDATE_SEARCH_PAGE:
Object.entries(action.payload).forEach(([k, v]) => {
draft[k] = v;
})
break;
// ... other
}
}
Also: remember that on recent versions of immer is perfectly legit to returns an object, so doing return Object.assign({}, state, action.payload) is still valid inside a produce call.
With ES6 you can do it this way:
export const state = produce((draft, action) => {
switch (type) {
case UPDATE_SEARCH_PAGE:
return {...draft, ...action.payload}
}
}, initialState)
In this case it works the same way as without Immer. All properties will be merged (shallow merge) into state. If you need to replace the state just return action.payload
Immer gives you a draft state that you can edit. Behind the scenes it uses ES6 proxies to discover what you changed and apply in an immutable way your edits to the original state.
Basically, you can do the exact same thing you do right now, but using the Immer api:
import produce from 'immer'
const newState = produce(this.state, draft => Object.assign({}, draft, payload))
If you, instead, know what properties are changed, you can do something like:
const newState = produce(this.state, draft => {
draft.propertyOne = 'newValue'
draft.propertyTwo = 42
})