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.
Related
the title may be misleading but here is what happened:
reducer.js:
// initial state
const initialState = {
notes: [
{
content: "reducer defines how redux store works",
important: true,
id: 1,
},
{
content: "state of store can contain any data",
important: false,
id: 2,
},
],
filter: "IMPORTANT",
};
// reducer
const noteReducer = (state = initialState, action) => {
switch (action.type) {
case "NEW_NOTE":
console.log("state", state);
return state.notes.concat(action.data);
// ...
}
const generateId = () => Math.floor(Math.random() * 1000000);
// action
export const createNote = (content) => {
return {
type: "NEW_NOTE",
data: {
content,
important: false,
id: generateId(),
},
};
};
in index.js:
const reducer = combineReducers({
notes: noteReducer,
filter: filterReducer,
});
const store = createStore(reducer, composeWithDevTools());
//dispatch a note from index.js
//it works here
store.dispatch(
createNote("combineReducers forms one reducer from many simple reducers")
);
returns in the console.log("state", state); in reducer.js:
state
{notes: Array(2), filter: 'IMPORTANT'}
filter: "IMPORTANT" // 'filter' is here
notes: (2) [{…}, {…}]
[[Prototype]]: Object //prototype is object
Here createNote is successful.
However, when creating a new note through:
const NewNote = (props) => {
const dispatch = useDispatch();
const addNote = (event) => {
event.preventDefault();
const content = event.target.note.value;
event.target.note.value = "";
// createNote does not work here
dispatch(createNote(content));
};
return (
<form onSubmit={addNote}>
<input name="note" />
<button type="submit">add</button>
</form>
);
};
Here the console.log("state", state); returns:
state
(3) [{…}, {…}, {…}]
0: {content: 'reducer defines how redux store works', important: true, id: 1}
1: {content: 'state of store can contain any data', important: false, id: 2}
2: {content: 'combineReducers forms one reducer from many simple reducers', important: false, id: 824517}
length: 3
// 'filter' is missing
[[Prototype]]: Array(0) // state prototype changed to array
In which the filter is gone from the state, so the creation is not successful.
In short, store.dispatch( createNote("...") ); works but not dispatch(createNote(content));.
The reason seems to be that noteReducer received different states. But in both cases filter is not specified.
I wonder why this happens and how to solve it?
as we know when you are using a reducer the reducer takes 2 parameters one for initial stat and the other for action.
any action will be run in the reducer you need to save the old state
by used spread operator {...state}
case "NEW_NOTE":
console.log("state", state);
{...state, notes: state.notes.concat(action.data)}
found the issue.
noteReducer should be:
const noteReducer = (state = initialState, action) => {
switch (action.type) {
case "NEW_NOTE":
return { ...state, notes: state.notes.concat(action.data) };
//...
}
just found out that above is a wrong fix. the right one is actually is:
const noteReducer = (state = initialState.notes, action) => {
otherwise noteReducer is changing the filter as well. but it should be changing the 'note' part only.
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 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.
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 new to Redux so please bear with me. I am wondering if something like the following is possible and/or optimal and if so, how do you update the nested object values in the reducer?
const initialState = {
banner: {
backgroundColor: 'black',
text: 'Some Title',
textColor: 'white',
image: null
},
nav: {
mainOpen: false,
authOpen: false,
}
...
}
And then in a reducer something like this does not seem to work...
export default function reducer(state = initialState, action = {}) {
const {type, data} = action;
switch (type) {
case SET_BANNER_TEXT:
return {
...state,
banner.text: data //<-- ****THIS FAILS WITH UNEXPECTED TOKEN!!!****
}
...
}
Or is it better to to have a 'banner' reducer, a 'nav' reducer and so on??
TIA!
I'm a bit new to redux too, so I can't speak too much to 'best practice'. I would lean towards a new reducer personally, since you have have specific actions like SET_BANNER_TEXT (color, img, etc?) that modify a specific portion of your state tree and nothing else. Making your reducers simple by breaking them apart, (even if there are a lot of them), will make things easier to trace down the road.
Syntactically you could achieve what you're trying to do with something like:
export default function reducer(state = initialState, action = {}) {
const {type, data} = action;
switch (type) {
case SET_BANNER_TEXT:
const newBannerState = Object.assign({}, state.banner, {text: data});
return Object.assign({}, state, {banner: newBannerState});
}
Since you're updating a key of an object, try using this to update the key
const state = {
...initialState,
banner: {
...initialState.banner, // extend
text: 'newText'
}
}
which translates to
var state = _extends({}, initialState, {
banner: _extends({}, initialState.banner, {
text: 'newText'
})
});
in ES5
check this jsbin
edit: as stated in the comment below, it will overwrite the whole banner object if the code is used above. You can try the other answer in this thread using the Object.assign() and clone the banner object. If you still want to use spread operators, I updated my answer.
I think it is also better to have specific reducers for deeply nested state.And I will write it something like this
export function rootReducer(state = initialState, action) {
switch (action.type) {
case SET_BANNER_TEXT:
case SET_BANNER_BG:
return Object.assign({}, state,
{
banner: bannerReducer(state.banner, action)
}
);
// ...
default:
return state;
}
}
function bannerReducer(state, action) {
switch (action.type) {
case SET_BANNER_TEXT:
return Object.assign({}, state, {text: action.payload});
case SET_BANNER_BG:
return Object.assign({}, state, {backgroundColor: action.payload});
// ...
default:
return state;
}
}