How to use object spread reduxjs/toolkit - reactjs

I'm trying to implement #reduxjs/toolkit into my project and currently I'm facing problem with using spread operator
const initialState: AlertState = {
message: '',
open: false,
type: 'success',
};
const alertReducer = createSlice({
name: 'alert',
initialState,
reducers: {
setAlert(state, action: PayloadAction<Partial<AlertState>>) {
state = {
...state,
...action.payload,
};
},
},
});
When I'm assigning state to new object my store doesn't update.
But if I change reducer to code
state.message = action.payload.message || state.message;
state.open = action.payload.open || state.open;
But that's not very handy. So is there way to use spread operator?

In this case, you should return the new object from the function instead:
setAlert(state, action: PayloadAction<Partial<AlertState>>) {
return {
...state,
...action.payload,
};
},
You can examine more examples in the docs.
Also, be aware of this rule:
you need to ensure that you either mutate the state argument or return a new state, but not both.

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' } ]

react-redux state is always same and not chainging

I'm using redux and my reducer function is called in every time the dispatch called but the state is not updating. and there is no difference between the first state and the next state.
ArtclesReducer.ts
const defaultState: Articles = {
articles: [{token: "teken", title: "text", featureImageUrl: ""}],
}
export const articlesReducer: Reducer<Articles, any> = (state = defaultState, action: ArticlesActionTypes) => {
let nextState: Articles = {
articles: state.articles,
}
switch (action.type) {
case ADD_ARTICLES :
let allArticles = [...state.articles, ...action.payload]
return {
articles: [{title: "", token: "", featureImageUrl: ""}, {
title: "",
token: "",
featureImageUrl: ""
}, {title: "", token: "", featureImageUrl: ""}, {title: "", token: "", featureImageUrl: ""}]
}
case UPDATE_ARTICLE:
console.log("in update article")
for (let i = 0; i < nextState.articles.length; i++) {
if (nextState.articles[i].token == action.payload.token) {
nextState.articles[i] = action.payload;
break;
}
}
break;
case DELETE_ARTICLE:
console.log("in delete article")
nextState.articles = nextState.articles.filter(value => {
return value.token != action.payload;
})
break;
default:
}
return nextState;
}
as shown up I return a non-empty state.
as you see the state it becomes the same and not updating
Redux Toolkit
If you are unsure about how to update the state without mutating it, you can save yourself a lot of frustration by using Redux Toolkit. The toolkit makes it so you can write the code as if you were mutating the state (it handles the immutability issue behind the scenes).
Here's how this reducer would look with the createReducer utility:
const articlesReducer = createReducer(defaultState, {
[ADD_ARTICLES]: (state, action) => {
// We don't return anything. We just mutate the passed-in draft state.
state.articles.push(action.payload);
},
[UPDATE_ARTICLE]: (state, action) => {
// Find which article we are updating
const index = state.articles.findIndex(
article => article.token === action.payload.token
);
// Replace that index with the new article from the payload
state.articles[index] = action.payload;
},
[DELETE_ARTICLE]: (state, action) => {
// We replace the articles array with a filtered version
state.articles = state.articles.filter(
article => article.token === action.payload
);
}
});
Most people don't use createReducer directly because there is an even better utility createSlice that creates the action names and action creator functions for you!
Vanilla Redux
Of course you can still do this the "old-fashioned" way. But you need to be sure that you never mutate any part of the state and that every case returns a complete state.
nextState.articles[i] = action.payload is actually a mutation even though nextState is a copy because it is a shallow copy so the articles property points to the same array as the current state.
I do not recommend this approach unless you are confident that you know what you are doing, but I want to include a correct version to show you how it is done.
export const articlesReducer: Reducer<Articles, any> = (state = defaultState, action: ArticlesActionTypes) => {
switch (action.type) {
case ADD_ARTICLES:
return {
...state,
articles: [...state.articles, ...action.payload]
};
case UPDATE_ARTICLE:
return {
...state,
articles: state.articles.map((article) =>
article.token === action.payload.token ? action.payload : article
)
};
case DELETE_ARTICLE:
return {
...state,
articles: state.articles.filter((article) =>
article.token !== action.payload
)
};
default:
return state;
}
};
Note: Writing ...state like you see in most examples is technically not necessary here since articles is the only property in your state so the there are no other properties to be copied by ...state. But it might be a good idea to include it anyways in case you want to add additional properties in the future.

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.

Redux Toolkit: 'Cannot perform 'set' on a proxy that has been revoked'

I'm trying to recreate a Memory-like game with React. I'm using Redux Toolkit for state management, but I'm having trouble with one use case.
In the selectCard action, I want to add the selected card to the store, and check if there's already 2 of them selected. If so, I want to empty the selected array after a delay.
const initialState : MemoryState = {
cards: [],
selected: [],
}
const memorySlice = createSlice({
name: 'memory',
initialState: initialState,
reducers: {
selectCard(state: MemoryState, action: PayloadAction<number>) {
state.selected.push(action.payload);
if (state.selected.length === 2) {
setTimeout(() => {
state.selected = [];
}, 1000);
}
}
}
});
The cards get selected just fine, but when I select 2 I get this error after 1 sec:
TypeError: Cannot perform 'set' on a proxy that has been revoked, on the line state.selected = [];
I'm new to this stuff, how do I access the state after a delay? Do I have to do it asynchronously? If so, how?
As stated in their documentation, don't perform side effects inside a reducer.
I would add the setTimeout when dispatching the action instead:
// so the reducer:
...
if (state.selected.length === 2) {
state.selected = [];
}
...
// and when dispatching
setTimeout(() => {
dispatch(selectCard(1))
}, 1000)
I ran into this issue too. I solved it by using the 'side effect' code into a function and then used the result of it in the reducer
const initialState : MemoryState = {
cards: [],
selected: [],
}
const memorySlice = createSlice({
name: 'memory',
initialState: initialState,
reducers: {
selectCard(state: MemoryState, action: PayloadAction<number>) {
state.selected = action.payload
}
}
});
export const { selectCard } = memorySlice.actions
export const sideEffectFunc = (param) => (dispatch) => {
let selected = []
selected.push(action.payload);
if (selected.length === 2) {
setTimeout(() => {
selected = [];
}, 1000);
}
dispatch(selectCard(selected));
};
Don't pay attention to the logic of the function (haven't tested it and it might be wrong) but I wanted to show the way we could handle 'side effect' code when using redux toolkit

Modifying state for specific item in array in redux reducer

I can't quite wrap my head around the boilerplate of redux. I looked up common patterns for immutable modifying of state but issue is, all these patterns simply push towards the end and not for a specific index.
Before I'll go into actual code, here's what the structure of the state looks like for better imagination (pseudo-code):
state = {
quizMenu: {...},
quizEditor: Array<Question>,
> type Question = {
id: number,
question: string,
questionOptions: Array<QuestionOption>,
}
> type QuestionOption = {
id: number,
optionText: string,
isValid: boolean,
}
}
Hopefully it makes sense. I have created an action for adding questions, which works fine. Now I'm trying to create an action for adding option to an already existing question, but I can't wrap my head around how to in the nested arrays of objects.
Here's how my action in question is defined:
const AQO = 'ADD_QUESTION_OPTION';
/*
* #param questionId - ID of the question we're accesssing in quizEditor array
* #param id - id of the option we're adding (handled in component)
*/
const actionAddQuestionOption = createAction(
AQO,
(questionId: number, id: number) => ({
payload: {
id,
optionText: 'New option',
isValid: false,
questionId,
},
})
);
Now my reducer is the following way:
const reducer = createReducer({//...}, {
[actionAddQuestionOption.type]: (state, action) => ({
...state,
quizEditor: [...state.quizEditor][action.payload.questionId].questionOptions.push({
id: action.payload.id,
optionText: action.payload.optionText,
isValid: action.payload.isValid,
})
})
}
This just ends up in this monster type-error: https://pastebin.com/raw/pBbnxcQp
But I'm pretty sure I'm accessing the Array inside the array of objects incorrectly.
quizEditor: [...state.quizEditor][action.payload.questionId].questionOptions
Does anyone know what would be the proper way of going about accessing it? Much appreciated!
Since you are using redux-toolkit which has immer built in you can just mutate the state directly and it will transform it into an immutable update internally
const reducer = createReducer({
[actionAddQuestionOption.type]: (state, { payload: { questionId, ...option }}) => {
const question = state.questionquizEditor(question => question.id === questionId)
question.questionOptions.push(option)
}
})
The way to make it an immuable update is like this
const reducer = createReducer({
[actionAddQuestionOption.type]: (state, { payload: { questionId, ...option } }) => ({
...state,
quizEditor: state.quizEditor.map(question =>
(question.id === questionId
? {
...question,
questionOptions: [...question.questionOptions, option],
}
: question)),
}),
})
the push method of Array returns the new length of the array not the array itself. What you can do is just concat the new object to the array which in turn will return the new array with the new question option.
[...state.quizEditor][action.payload.questionId].questionOptions.concat({
id: action.payload.id,
optionText: action.payload.optionText,
isValid: action.payload.isValid,
})
Furthermore, we have to modify only that property in the state with our new array:
const reducer = createReducer({
//...}, {
[actionAddQuestionOption.type]: (state, action) => {
const quizEditor = [...state.quizEditor];
quizEditor[action.payload.questionId].questionOptions = quizEditor[
action.payload.questionId
].questionOptions.concat({
id: action.payload.id,
optionText: action.payload.optionText,
isValid: action.payload.isValid
});
return {
...state,
quizEditor
};
}
});
Thanks to immer in redux toolkit we can make it more readable:
const reducer = createReducer({
//...}, {
[actionAddQuestionOption.type]: (state, action) => {
const question = state.quizEditor[action.payload.questionId];
question.questionOptions = [
...question.questionOptions,
{
id: action.payload.id,
optionText: action.payload.optionText,
isValid: action.payload.isValid
}
];
return state;
}
});

Resources