React / Redux: Correct way to update object in reducer - reactjs

I'm having trouble updating an object in my React / Redux reducer. The initial state is an object of Immutable. I'm stuck trying to update the object.
import Immutable from 'immutable';
import * as actionTypes from '../actions/actionTypes';
const initialState = Immutable.fromJS({
user: {
id: null,
name: null,
age: null
}
});
export default function addUserReducer(state = initialState, action) {
switch(action.type) {
case actionTypes.ADD_USER:
const user = {
id: action.id,
name: action.name,
age: action.age
}
return state.setIn(['user'], user);
default:
return state;
}
}
The state always returns a map with the values of id, name and age as null.
What's the correct way to update the state in my reducer?

Use merge function in immutable to change the state.It can be implemented like this
import Immutable from 'immutable';
import * as actionTypes from '../actions/actionTypes';
const initialState = Immutable.fromJS({
user: {
id: null,
name: null,
age: null
}
});
export default function addUserReducer(state = initialState, action) {
switch(action.type) {
case actionTypes.ADD_USER:
// This will update the state in the reducer if you are using immutable library
return state.merge({
id: action.id,
name: action.name,
age: action.age
});
default:
return state;
}
}

Your code looks ok, and when I run it in a repl it works.
I tested it by adding the following:
const newState = addUserReducer(undefined, {
type: 'ADD_USER', // I just assumed that's what your type resolves to.
id: 1,
name: 'FOO',
age: 100,
});
console.log(newState); // Logs "Map { "user": [object Object] }"
console.log(Immutable.Map.isMap(newState); // true
console.log(Immutable.Map.isMap(newState.get('user'))) //false
I suspect the reason it's not working for you is because your action's actual type does not actually equal actionTypes.ADD_USER. I would double check that case in the reducer runs. A simple log in that case should tell you.
Also, like other comments have said above, right now your ADD_USER case is setting the user as a NON-Immutable object, so instead modify the ADD_USER return statement like so:
state.set('user', Immutable.Map(user));
Also note that since 'user' is a top-level key, Maps .set method works just fine.

Related

Cannot assign to read only property 'url' of object '#<Object>'

I tried changing a key's value inside my object, but it seems like I have been mutating the state.
The issue happens in state dispatch
Code:
export function nameList(id, rank, sub) {
const { nameItem: name } = store.getState().nameListSlice; //gets the nameItem state
if (!name.length || name.length === 0) {
const newName = [
{
name: "Male",
url: getNameUrl("Male", id, rank, sub),
icon: "Male",
toolTip: "Male",
active: false,
idName: "male",
},
{
name: "Female",
url: getNameUrl("Female", id, rank, sub),
icon: "Female",
toolTip: "Female",
active: false,
idName: "female",
},
];
store.dispatch(updateDetails(newName)); //The issue happen here
} else {
return name.map((n) => {
n.url = getNameUrl(n.name, id, rank, sub);
return n;
});
}
}
// This function returns a url
function getNameUrl(type, id, rank, sub) {
switch (type) {
case "Male": {
return `/male/${id}/${rank}/${sub}`;
}
case "Female": {
return `/female/${id}/${rank}/${sub}`;
}
}
}
Reducer/Action: (redux toolkit)
export const nameListSlice = createSlice({
name: 'nameItem',
initialState,
reducers: {
updateDetails: (state,action) => {
state.nameItem = action.payload
},
},
})
export const { updateDetails } = nameListSlice.actions
export default nameListSlice.reducer
The error I get:
TypeError: Cannot assign to read only property 'url' of object '#<Object>'
This happens in the above code disptach - store.dispatch(updateDetails(newName));
Commenting this code, fixes the issue.
How to dispatch without this error ?
Also based on this Uncaught TypeError: Cannot assign to read only property
But still same error
RTK use immerjs underly, nameItem state slice is not a mutable draft of immerjs when you get it by using store.getState().nameListSlice. You can use the immutable update function createNextState to perform immutable update patterns.
Why you can mutate the state in case reducers are created by createSlice like state.nameItem = action.payload;?
createSlice use createReducer to create case reducers. See the comments for createReducer:
/**
* A utility function that allows defining a reducer as a mapping from action
* type to *case reducer* functions that handle these action types. The
* reducer's initial state is passed as the first argument.
*
* #remarks
* The body of every case reducer is implicitly wrapped with a call to
* `produce()` from the [immer](https://github.com/mweststrate/immer) library.
* This means that rather than returning a new state object, you can also
* mutate the passed-in state object directly; these mutations will then be
* automatically and efficiently translated into copies, giving you both
* convenience and immutability.
The body of every case reducer is implicitly wrapped with a call to
produce(), that's why you can mutate the state inside case reducer function.
But if you get the state slice outside the case reducer, it's not a mutable draft state, so you can't mutate it directly like n.url = 'xxx'.
E.g.
import { configureStore, createNextState, createSlice, isDraft } from '#reduxjs/toolkit';
const initialState: { nameItem: any[] } = { nameItem: [{ url: '' }] };
export const nameListSlice = createSlice({
name: 'nameItem',
initialState,
reducers: {
updateDetails: (state, action) => {
state.nameItem = action.payload;
},
},
});
const store = configureStore({
reducer: {
nameListSlice: nameListSlice.reducer,
},
});
const { nameItem: name } = store.getState().nameListSlice;
console.log('isDraft: ', isDraft(name));
const nextNames = createNextState(name, (draft) => {
draft.map((n) => {
n.url = `/male`;
return n;
});
});
console.log('nextNames: ', nextNames);
Output:
isDraft: false
nextNames: [ { url: '/male' } ]

Update a react-redux state too fast does not take the modification into account

I have an object stored in react-redux which contains multiple sub-objects, like:
MyObject =
{
{ id: 1, name: "object 1"}
{ id: 2, name: "object 2"}
...
}
This object can be updated very quickly multiple times, for example with a function like this:
function modifyMyObject() {
//Load the object from Redux and create a clone to be modified
let myObject = JSON.parse(JSON.stringify(this.props.myObject))
//Change the properties of my object
...
//Update the object on Redux
this.props.setMyObject(myObject)
}
However I noticed that if I call modifyMyObject() very quickly with different modifications, the object is not updated properly.
I guess that the state in redux does not have time to be updated before I try to make a new modification.
Here is the object slice :
import {createSlice} from '#reduxjs/toolkit'
const initialState = {
value: {},
}
export const myObjectSlice = createSlice({
name: 'object',
initialState,
reducers: {
setMyObject: (state, action) => {
state.value = action.payload
},
},
})
// Action creators are generated for each case reducer function
export const { setMyObject } = myObjectSlice.actions
export default myObjectSlice.reducer
Is there a better way to handle these quick changes? Or do you have a suggestion to improve this code? Thank you!
I'm posting here the solution to this problem which was suggested by HĂ„kenLid:
Method which is NOT working: in App.js, create a copy of the slice.state, modify it and save it to Redux:
function modifyMyObject() {
//Load the object from Redux and create a clone to be modified
let myObject = JSON.parse(JSON.stringify(this.props.myObject))
//Change the properties of my object
...
//Update the object on Redux
this.props.setMyObject(myObject)
}
The problem: the object is out of date when saved.
The solution: In app.js:
function modifyMyObject(newObject) {
this.props.updateMyObject(newObject) //call the function from the slice
}
In MyObjectSlice.js:
import {createSlice} from '#reduxjs/toolkit'
const initialState = {
value: {},
}
export const myObjectSlice = createSlice({
name: 'object',
initialState,
reducers: {
updateMyObject: (state, action) => {
let newObject = action.payload
//-> insert here the logic to update the object <-
...
state.value = newObject
},
},
})
// Action creators are generated for each case reducer function
export const { updateMyObject } = myObjectSlice.actions
export default myObjectSlice.reducer
Are you trying try/catch block to handle JSON.parse? I hope this below can help you:
function modifyMyObject() {
function parseJSONSafely(str) {
try {
return JSON.parse(str);
}
catch (e) {
console.err(e);
// Return a default object, or null based on use case.
return {}
}
}
//Load the object from Redux and create a clone to be modified
editedObject = parseJSONSafely(JSON.stringify(this.props.myObject))
if (editedObject !== {}) {
//Change the properties of my object
editedObject['proterty'] = /* ... */
//Update the object on Redux
this.props.setMyObject(editedObject)
}
}

Partial reset of state with createSlice

I'm having an action, which is supposed to do a partial reset on some state properties.
Before moving over to the redux-toolkit, I have achieved this with the following code:
initialState
const initialState = {
username: null,
dateOfBirth: null,
homeTown: null,
address: null,
postCode: null,
floor: null,
}
reducer
switch (action.type) {
...
case USER_SET_HOME_TOWN:
return {
...initialState,
homeTown: action.payload
username: state.userName,
dateOfBirth: state.dateOfBirth,
};
...
default:
return state;
}
USER_SET_HOME_TOWN is dispatched every time homeTown is changing and maintains the username and the dateOfBirth, whilst resting everything else back to the initialState.
Now with the redux toolkit and createSlice I'm trying to achieve a similar behaviour by writing something like:
createSlice - not working
const user = createSlice({
name: 'user',
...
reducers: {
...
userSetHomeTown: {
reducer: (state, action) => {
state = { ...initialState };
state.homeTown = action.payload;
state.username = state.payload;
state.dateOfBirth = state.dateOfBirth;
},
},
...
},
});
This isn't working, which, I guess, makes sense since state = { ...initialState}; resets the state and state.username/dateOfBirth: state.username/dateOfBirth; are kind of useless then and counter productive... or simply just wrong.
After changing this to:
createSlice - working
const user = createSlice({
name: 'user',
...
reducers: {
...
userSetHomeTown: {
reducer: (state, action) => ({
...initialState,
homeTown: action.payload,
username: state.payload,
dateOfBirth: state.dateOfBirth,
}),
},
...
},
});
It did work, but I'm still wondering if this is the recommended and right way of doing it.
Yes, that's correct.
Remember that Immer works by either mutating the existing state object (state.someField = someValue), or returning an entirely new value you constructed yourself immutably.
Just doing state = initialState is neither of those. All it does is point the local variable state to a different reference.
The other option here would be Object.assign(state, initialState), which would overwrite the fields in state and thus mutate it.

Updating an item in Redux using ImmutableJS given a key

Redux State:
let initialState = Immutable.fromJS({
budgetItems: [],
editingBudget: [[]]
});
Trying to update items in the budgetItems section, which is an array of objects.
The structure of the objects in the array is like:
let initBudget = {budgetCategory: '', budgetCost: '', budgetDate: ''};
My attempt:
case types.UPDATE_EXISTING_BUDGET:
return state.getIn(['budgetItems']).update(
state.getIn(['budgetItems']).findIndex(function(item) {
return item.get("budgetCategory") === action.payload.budgetCategory;
}), function(item) {
return item.set(action.payload.budgetCategory);
}
);
Trying to literally replace the entire object that I've found. The above code is working if I set a single key with a value, but not the entire object
To update an object or an array you'll need the updateIn method.
import { fromJS, List, Map } from immutable;
const initialState = fromJS({
data: List([Map({ foo: { bar: baz } } })],
});
functionFooReducer(state = initialState, action) {
switch (action.type) {
case UPDATE:
return state
.updateIn(['data.0.foo.bar'.split('.')], (), action.value);
// ...
}
}

How to replace part of the Redux state

I have a packageReducer which keeps the packageType and packageList related details. once the details are fetched from the server I need to replace the initial state values with the new values that are been fetched. As an example, if the packageLists are been fetched I need to replace only the "packageList"
Below is my PackageState reducer,
const initialState = {
packageList: packageListInitialState,
packageTypes: [{title: 'Select Package Type', value: ''}],
};
export default function packageState( state = initialState, action ) {
switch ( action.type ) {
case FETCH_PACKAGE_LIST_SUCCESS:{
return Object.assign( {}, state, action.payload );
}
case FETCH_PACKAGE_TYPES_SUCCESS:{
return Object.assign( {}, state, action.payload );
}
default:
return state;
}
}
The way I have implemented I think im replacing the entire state, Can someone let me know how I can achieve it?
Thank you.
You are not:
var state = {a: 1, b: 2, c: 3}
var newData = {a: 4, b: 5}
console.log(Object.assign( {}, state, newData )) // { a: 4, b: 5, c: 3 }
Properties in the target object will be overwritten by properties in the sources if they have the same key. Later sources' properties will similarly overwrite earlier ones. (docs)
So, as long as your payload contains the keys that you really want to update, you are safe. You can also do it in a simpler way if you use ES6's spread syntax (I'm assuming your payload looks like {packageList: data}):
const initialState = {
packageList: packageListInitialState,
packageTypes: [{title: 'Select Package Type', value: ''}],
};
export default function packageState( state = initialState, action ) {
switch ( action.type ) {
case FETCH_PACKAGE_LIST_SUCCESS:{
return {...state, ...action.payload};
}
case FETCH_PACKAGE_SETTINGS_SUCCESS:{
return {...state, ...action.payload};
}
default:
return state;
}
}
Under the assumption that the action you're trying to achieve this in is only the FETCH_PACKAGE_LIST_SUCCESS action, and the payload is the updated/fetched list, then you just need to return an object as shown below.
Since you're trying to return an object with only one of the two properties changed, then you can use the previous state's value for the unchanged property and update the other.
const initialState = {
packageList: packageListInitialState,
packageTypes: [{title: 'Select Package Type', value: ''}],
};
export default function packageState( state = initialState, action ) {
switch ( action.type ) {
case FETCH_PACKAGE_LIST_SUCCESS:{
return { packageList: action.payload, packageTypes: state.packageTypes }
}
case FETCH_PACKAGE_SETTINGS_SUCCESS:{
return Object.assign( {}, state, action.payload );
}
default:
return state;
}
}

Resources