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' } ]
Related
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)
}
}
I have a problem with my Redux reducer. In the selectedSongReducer the selectedSong is defined as null so it will not return an error, but I still get the following error: Error: Reducer "selectedSong" returned undefined during initialization. If the state passed to the reducer is undefined, you must explicitly return the initial state. The initial state may not be undefined. If you don't want to set a value for this reducer, you can use null instead of undefined.
Here is the reducer code:
import { combineReducers } from 'redux';
const songsReducer = () => {
return [
{ title: 'A' , length: '4:05' },
{ title: 'B' , length: '2:30' },
{ title: 'C' , length: '3:15' },
{ title: 'D' , length: '1:45' },
]
};
const selectedSongReducer = (selectedSong = null, action) => {
if(action.type = 'SONG_SELECTED') {
return action.payload;
}
return selectedSong;
}
export default combineReducers({
songs: songsReducer,
selectedSong: selectedSongReducer
})
Anybody any suggestions on where could be the problem?
You have accidentally used an assignment operator instead of comparing the values here
if(action.type = 'SONG_SELECTED'). It will always be executed.
I'm kind of new to React.js & Redux, so I have encountered a problem with Reducers.
I am creating a site that have a main "Articles" page, "Question & Answers" page, I created for each one a separate Reducer that both work just fine.
The problem is in "Main Page" which contains a lot of small different pieces of information, and I don't want to create each little different piece of information its on Reducer, so I am trying to create one Reducer which will handle a lot of very small different pieces of information, and I can't make that work, inside the main "Content" object, I put 2 Key Value Pairs that each have an array, one for each different information, one is "Features" info, and one for the "Header" info.
This is the error that I'm getting:
Uncaught TypeError: Cannot read property 'headerContent' of undefined
at push../src/reducers/ContentReducer.js.__webpack_exports__.default (ContentReducer.js:15)
I am not sure what's the problem, maybe my code is wrong or maybe my use of the spread operator, any solution?
I have added the necessary pages from my code:
ACTIONS FILE
export const addFeatureAction = (
{
title = 'Default feature title',
feature = 'Default feature',
} = {}) => ({
type: 'ADD_FEATURE',
features: {
id: uuid(),
title,
feature
}
})
export const addHeaderAction = (
{
title = 'Default header title',
head = 'Default header',
} = {}) => ({
type: 'ADD_HEADER',
header: {
id: uuid(),
title,
head
}
})
REDUCER FILE:
const defaultContentReducer = {
content: {
featuresContent: [],
headerContent: [],
}
}
export default (state = defaultContentReducer, action) => {
switch(action.type) {
case 'ADD_FEATURE':
return [
...state.content.featuresContent,
action.features
]
case 'ADD_HEADER':
return [
...state.content.headerContent,
action.header
]
default:
return state
}
}
STORE FILE:
export default () => {
const store = createStore(
combineReducers({
articles: ArticleReducer,
qnaList: QnaReducer,
content: ContentReducer
})
);
return store;
}
The reducer function is supposed to return the next state of your application, but you are doing a few things wrong here, you are returning an array, a piece of the state and not the state object, I would suggest you look into immer to prevent this sort of errors.
Simple fix:
export default (state = defaultContentReducer, action) => {
switch(action.type) {
case 'ADD_FEATURE':
return {...state, content: {...state.content. featuresContent: [...action.features, ...state.content.featuresContent]}}
// More actions are handled here
default:
return state
}
}
If you use immer, you should have something like this
export default (state = defaultContentReducer, action) => {
const nextState = produce(state, draftState => {
switch(action.type) {
case 'ADD_FEATURE':
draftState.content.featuresContent = [...draftState.content.featuresContent, ...action.features]
});
break;
default:
break;
return nextState
}
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.
I'm build a simple app that expands and collapses sections of content based on their state. Basically, if collapse = false, add a class and if it's true, add a different class.
I'm using Next.js with Redux and running into an issue. I'd like to update the state based on an argument the action is passed. It's not updating the state and I'm not sure why or what the better alternative would be. Any clarification would be great!
// DEFAULT STATE
const defaultState = {
membership: 'none',
sectionMembership: {
id: 1,
currentName: 'Membership',
nextName: 'General',
collapse: false
},
sectionGeneral: {
id: 2,
prevName: 'Membership',
currentName: 'General',
nextName: 'Royalties',
collapse: true
}
}
// ACTION TYPES
export const actionTypes = {
SET_MEMBERSHIP: 'SET_MEMBERSHIP',
MOVE_FORWARDS: 'MOVE_FORWARDS',
MOVE_BACKWARDS: 'MOVE_BACKWARDS'
}
// ACTION
export const moveForwards = (currentSection) => dispatch => {
return dispatch({ type: actionTypes.MOVE_FORWARDS, currentSection })
}
// REDUCERS
export const reducer = (state = defaultState, action) => {
switch (action.type) {
case actionTypes.SET_MEMBERSHIP:
return Object.assign({}, state, {
membership: action.membershipType
})
case actionTypes.MOVE_FORWARDS:
const currentId = action.currentSection.id
const currentName = "section" + action.currentSection.currentName
return Object.assign({}, state, {
currentName: {
id: currentId,
collapse: true
}
})
default: return state
}
}
The currentName variable is causing an issue for the state to not update. I want to be able to dynamically change each sections state, which is why I thought I'd be able have a variable and update state like this.
It seems you can't use a variable for the key in the key/value pair. Why is this? What's an alternative to dynamically updating state?
That is because JavaScript understands that you want to create a key named currentName not a key with the value of the variable currentName. In order to do what you want, you have to wrap currentName in brackets:
return Object.assign({}, state, {
[currentName]: {
id: currentId,
collapse: true
}
})
So it will understand that the key will be whatever currentName is.
It also right:
return Object.assign({}, state, {
[currentName]: Object.assign({}, state[currentName], {
id: currentId,
collapse: true
})
})