I am looking to update value(s) in a nested object based on user input. So for instance, if the user chooses to update their country and street, only those values will be passed as the action.payload to the reducer and updated (the rest of the state will be kept the same). I provide my initial state and reducer:
My state:
const initialState = {
userData: [
{
firstName: "",
lastName: "",
country: "",
address: {
street: "",
houseNumber: "",
postalCode: "",
}
}
],
error: null,
};
export default (state = initialState, action) => {
switch (action.type) {
case GET_USER_DATA:
return { ...state, userData: action.payload };
case UPDATE_USER_DATA:
return {
...state,
userData: [{
...state.userData,
...action.payload,
}]
};
default:
return state;
}
};
Any help would be great, thank you!!
It doesn't appear that you need the array wrapping the object. If that is true, remove it for simplicity. Then userData becomes a plain object, and your update becomes:
return {
...state,
userData: { // <- no wrapping array, just update the object
...state.userData,
...action.payload,
}
};
Since there is an array, you would need to destructure the object at the correct index.
return {
...state,
userData: [{
...state.userData[0], // <- destructure the object, not the array
...action.payload,
}]
};
If you do need the array, and there will be more than one object, you will also need to pass an identifier in the action payload in order to know which index to update, but with the current information given this is the most complete answer possible.
Related
Am looking into old code & I have a generic reducer that handles fetching off data and the initial state looks like this
const initialState: State<T> = {
status: STATUS.INIT,
data: undefined,
error: undefined,
}
Since I don't know what type the value is going to be beforehand data undefined
const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {
switch (action.type) {
case ACTION_TYPE.REQUEST:
return { ...initialState, status: STATUS.FETCHING }
case ACTION_TYPE.SUCCESS:
return { ...initialState, status: STATUS.FETCHED, data: action.payload }
case ACTION_TYPE.FAILURE:
return { ...initialState, status: STATUS.ERROR, error: action.payload }
default:
return state
}
then here we have the reducer handling data. I am encountering a problem though, I need to add a array of data without removing the old. How do I change the type ACTION_TYPE.SUCCESS. What should I do to chnage it to allow it?
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.
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.
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.
The error I'm getting is Cannot find name 'objectArray'.
interface StateInterface {
objects: {
objectArray: object[];
selected: object;
};
}
const InitialState: StateInterface = {
objects: {
objectArray: [],
selected: {},
},
};
const Reducer = (state: StateInterface, action: any) => {
switch (action.type) {
case 'SELECTED':
return {
...state,
objects: { ...state.objects, selected: action.value },
};
case 'ADD_OBJECT':
return {
...state,
objects: { ...state.objects, objectArray: objectArray.push(action.value )},
// ^---- Cannot find name 'objectArray'.ts(2304)
};
default:
return state;
}
};
I also tried
objects: { ...state.objects, objectArray: ...action.value )},
Only the state object is in scope at that point (provided as an argument to the reducer), try switching objectArray for state.objectArray at the point you're getting the error.
But also, you'll need to append the value immutably for it to be correct (a rule of reducers), so you'll need to make that whole line something like:
objects: { ...state.objects, objectArray: [...state.objectArray, action.value]},
To create a new array with both the old values and the new value you're adding.