Changing state in Redux - reactjs

I am trying to add an element to an array in the state and change another array element's property. Say we have the following state structure:
{
menuItems: [{
href: '/',
active: true
}]
}
After dispatching the ADD_MENU_ITEM action I want to end up with this state:
{
menuItems: [{
href: '/new',
active: true
}, {
href: '/',
active: false,
}]
}
I have tried managing this in Redux reducers in several fashions:
function reducer(state = {}, action) {
switch (action.type) {
case ADD_MENU_ITEM: {
let menuItems = state.menuItems;
let newMenuItem = action.newMenuItem;
// First try
menuItems[0].active = false;
menuItems.unshift(newMenuItem);
state = Object.assign({}, state, { menuItems: menuItems });
// Second try
menuItems[0].active = false;
menuItems.unshift(newMenuItem);
state = Object.assign({}, state, {menuItems: Object.assign([], menuItems)});
// Third try
menuItems[0].active = false;
state = (Object.assign({}, state, {
menuItems: [
Object.assign({}, newMenuItem),
...menuItems
]
}));
// Fourth try
menuItems[0].active = false;
state = update(state, {
menuItems: {$unshift: new Array(newMenuItem)}
});
console.log(state);
return state;
}
}
}
In the fourth try, I am using React's Immutability Helpers but it never works. I logged the state to the console before returning the state and it logs correctly, but when logging inside the components which get re-rendered, the menuItems array does not add the first item, although the active member is set to false.
What could I be doing wrong?

The state in the reducer should be immutable and for this reason should not be modified. It is also recommended to flatten your object whenever possible.
In your scenario your initial state could be an array as such:
[{
href: '/',
active: true
}]
In your reducer, try returning a brand new array as follows:
function reducer(state = {}, action) {
switch (action.type) {
case ADD_MENU_ITEM: {
return [
action.newMenuItem,
...state.map(item => Object.assign({}, item, { active: false }))
];
}
}
}
More information about reducers can be found here: Redux Reducers Documentation
Helpful excerpt from the documentation:
It’s very important that the reducer stays pure. Things you should never do inside a reducer:
Mutate its arguments;
Perform side effects like API calls and routing transitions;
Calling non-pure functions, e.g. Date.now() or Math.random().
MORE INFO ADDED
In your reducer, and for all four tries, you are modifying the existing state before returning it.
This results in react-redux, when checking if your state has changed, not to see any changes as both the previous and next states are pointing to the same object.
Here are the lines I am referring to:
First Try:
// This line modifies the existing state.
state = Object.assign({}, state, { menuItems: menuItems });
Second Try:
// This line modifies the existing state.
state = Object.assign({}, state, {menuItems: Object.assign([], menuItems)});
Third Try:
// This line modifies the existing state.
state = (Object.assign({}, state, {
menuItems: [
Object.assign({}, newMenuItem),
...menuItems
]
}));
Fourth Try:
// This line modifies the existing state.
state = update(state, {
menuItems: {$unshift: new Array(newMenuItem)}
});

Related

When using useReducer to control a form, do I pass the previous state on every action?

My form has two inputs:
username (type="text")
email (type="email")
I also have a .json file as a "database" to do some validation. The submit button can only be clicked if the user enters a username that doesn't exist on the .json file. The thing is I don't know if I should pass the "...state" object on every action.type. I am getting some bugs that seem to go away when I use the "...state", but I don't understand why nor if I should always use it.
Here's my code:
const formReducer = (state, action) => {
switch (action.type) {
case "USERNAME_INPUT":
return {
...state,
usernameValue: action.payload,
};
case "API_RETURN_USERNAME":
return {
...state,
usernameValue: action.payload.match
? action.payload.username
: "",
usernameIsValid: !action.payload.match,
emailValue: action.payload.match ? action.payload.email : "",
emailIsValid: !action.payload.email,
apiReturned: true,
};
case "EMAIL_INPUT":
return {
...state,
emailValue: action.payload.value,
emailIsValid: action.payload.isValid,
formIsValid: action.payload.isValid && state.usernameIsValid,
apiReturned: false,
};
default:
return {
usernameValue: "",
emailValue: "",
usernameIsValid: false,
emailIsValid: false,
formIsValid: false,
apiReturned: false,
};
}
The reducer is always called with 2 arguments:
The previous state value and
The action (most often, an object)
The role of reducer function is to compute on these 2 values and generate the next value of state(read a new state object) and discard old state
All action may not change all keys of your state which is typically, and even in this particular case, a object.
We may only want to operate on a few keys of our state object.
So, we have two options, either manually copy all keys or use ES6 spread operator to spread the old state object and then overwrite the keys we want to change with the new value. This will preserve the keys we are not changing.
If you don't take either path, your state will become the object with only the keys you update and hence you may face unexpected behaviour
You should just about always shallow copy the previous state when updating a React state. Think of the useReducer as a very specialized version of the useState hook. Instead of returning the state and an updater function, i.e. [state, setState], it returns an array of the state and a dispatch function, i.e. [state, dispatch]. Please recall that state updates in React function components do not merge updates, so failing to copy any of the previous state will result in it not being included in the next state value, as if it were removed.
Functional Updates
Note
Unlike the setState method found in class components, useState
does not automatically merge update objects. You can replicate this
behavior by combining the function updater form with object spread
syntax:
const [state, setState] = useState({});
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
Another option is useReducer, which is more suited for managing
state objects that contain multiple sub-values.
From this basic understanding of state updates in function components, or if you are already familiar with "legacy" Redux reducer functions, it is trivial to see and understand why the previous state is copied forward.
Note also the default case typically doesn't have any effect on the state, it just returns the current state value.
The single exception I can think of is when you wanting to set the state to a completely new value like resetting back to initial state.
Example:
const formReducer = (state, action) => {
switch (action.type) {
case "USERNAME_INPUT":
return {
...state,
usernameValue: action.payload,
};
case "API_RETURN_USERNAME":
return {
...state,
usernameValue: action.payload.match
? action.payload.username
: "",
usernameIsValid: !action.payload.match,
emailValue: action.payload.match ? action.payload.email : "",
emailIsValid: !action.payload.email,
apiReturned: true,
};
case "EMAIL_INPUT":
return {
...state,
emailValue: action.payload.value,
emailIsValid: action.payload.isValid,
formIsValid: action.payload.isValid && state.usernameIsValid,
apiReturned: false,
};
case "RESET":
return action.payload;
default:
return state;
}
};
...
const initialState = {
usernameValue: "",
emailValue: "",
usernameIsValid: false,
emailIsValid: false,
formIsValid: false,
apiReturned: false,
}
...
const [state, dispatch] = useReducer(reducer, initialState);
...
const resetState = () => {
dispatch("RESET", { payload: initialState });
};

Redux Toolkit - I can't update Slice state?

I wanna update the state and tried few ways to do that, but I can't.
First, I got a problem with fetching state, as a result, I got proxy, not a state.
That is fixed by the current() function by the redux toolkit.
Next, where I have a problem is mutation main slice state.
Here is a reducer for mutating.
reducers: {
setUser: (state, payload) => {
console.log("before", current(state)); //init state
state.currentUser = {
...state.currentUser,
loggined: true,
};
console.log("after", current(state)); // here I have new state but...
},
clearUser: (state) => {},
},
As a result, as a second console log, I got to state what I want, when I want to change a page or just want to do something with new data in the state, by useSelector() redux function, as a result, I got old, not changed state.
Why has this happened?
Example of Slice state:
initialState: {
currentUser: {
loggined: false,
isAdmin: false,
jwt: false,
},
},
Thanks!
Reducers of createSlice use immer:
This object will be passed to createReducer, so the reducers may safely "mutate" the state they are given.
So you can either return a new object that is the new state or "mutate" it and not return it, from createReducer
you need to ensure that you either mutate the state argument or return a new state, but not both.
So you can do:
setUser: (state, payload) => {
//state here is an immer draft, do not use that to copy current state
console.log("before", current(state)); //init state
state.currentUser.loggined = true;
//not returning anyting
},
Not sure how you'd return a new state based on the old one since ...state makes a copy of the immer draft and not of the state. Can't even find examples of doing this unless it's an array.

How can you replace entire state in Redux Toolkit reducer?

EDIT: The solution is to return state after I replace it completely (return state = {...action.payload})! But why? I don't need to return it when I replace the fields individually.
I'm working with the Redux Toolkit, which simplifies some Redux boilerplate. One thing they do is use Immer to allow you to directly 'modify' state (in fact, you're not). It works fine, except I don't know how to replace my section of state entirely. For instance, I want to do something like this
const reducer = createReducer({ s: '', blip: [] }, {
[postsBogus.type]: (state, action) => {
state = { ...action.payload };
}
but state remains untouched. Instead I have to do this
[postsBogus.type]: (state, action) => {
state.s = action.payload.s;
state.blip = action.payload.blip;
}
Is there a way I can replace state entirely?
Yes, as you noted, you must return a new value to replace the state entirely.
Even in a "plain" Redux reducer, assigning state = newValue does nothing, because all that does is say that the local function variable named state is now pointing to a different value in memory. That does nothing to return a new value.
For Immer specifically, you can either mutate the contents of the Proxy-wrapped state value as long as it's an object or array, or you can return an entirely new value, but not both at once.
You can, but not in this way, when you say:
function x(y){
y = 4
}
You're mutating the function parameter, but not the state of redux,
you have two options to update this state of your redux store:
either to set state.your_state_name = something or, in your case, what you want to do is to return a new object, the new object is what the new state value is going to be.
simple example:
myReducerFunc: (state, action) => {
return {...action.payload }
},
another example:
const loggedInUserSlice = createSlice({
name: '$loggedInUser',
initialState: {
isLoggedIn: false,
},
reducers: {
loggedIn: (state, action) => {
return {
isLoggedIn: true,
...action.payload,
}
},
},
})

React componentWillReceiveProps will receive props not triggering when redux state updates

I've got a store which holds actions, these actions are suppose to be looped through each time a new one is added.
I've got a component MessageListView which resides in a parent called MessageView. When a new action is added to my socketActions.queue array, the componentWillRecieveProps is suppose to trigger, but it doesn't.
Here is what my reducer looks like:
/* Reducer */
const initialState = {
queue: [{
type: 'alert',
action: 'alert',
completed: false, // Set to true or just delete this object when the action has been completed
data: {
message: "the data object dynamically changes based on the type"
}
}]
};
export default (state = initialState, action) => {
switch (action.type) {
case ADD_ACTION:
let queue = state.queue
// action.data['completed'] = false
queue.push(action.data)
console.log("Just updated queue", action.data)
return {
...state,
queue: queue,
latestAction: new Date()
}
My component is connected up to the store like this:
function mapStateToProps(state){
console.log(state, "State inside messagelistview.js")
return {
actionsQueue: state.socketActions.queue,
latestAction: state.socketActions.latestAction
}
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators({ completeAction }, dispatch);
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(MessageListView);
So when I dispatch a new ADD_ACTION action, my state updates & redux-logger prints out the new and old state which is telling me that they're the same?! . I don't know why it'd be doing this after I've changed the latestAction value & queue array. This is why the componentWillRecieveProps isn't working, but I can't figure out why the state is the same?!
Not 100% certain if this would solve anything, but I think you aren't properly copying over state.queue into the completely new variable queue.
I'd suggest doing something like:
let queue = state.queue.slice()
... and see if anything changes? Right now your queue variable is still the same as the state.queue
You don't change queue identity in reducer. Try this code:
case ADD_ACTION:
let queue = state.queue
// action.data['completed'] = false
queue.push(action.data)
console.log("Just updated queue", action.data)
return {
...state,
queue: [...queue], // <-- here we copy array to get new identity
latestAction: new Date()
}
You should always shallow copy changed objets. See http://redux.js.org/docs/Troubleshooting.html#never-mutate-reducer-arguments
A connected component does a shallow check (===) whether the state is updated and renders wrapped component only if that check fails. In your case, you're mutating your queue causing the equality the check to pass.
It will work if you change your reducer like this:
state.queue = state.queue.concat(action.data);
or with ES6 syntax:
state = { ...state, queue: [...state.queue, action.data] };
The problem is that you are mutating your state when you do this:
let queue = state.queue
// action.data['completed'] = false
queue.push(action.data)
When you mutate the existing state directly, Redux doesn't detect a difference in state and won't notify your components that the store has changed.
So instead you've the following options to create a new queue array:
case ADD_ACTION:
return {
...state,
queue: state.queue.concat(action.data),
latestAction: new Date()
}
or with ES6 sugar:
case ADD_ACTION:
return {
...state,
queue: [...state.queue, action.data]
latestAction: new Date()
}

change value deep in redux state

context
I'm rendering a form with a dynamic set of text elements. I've normalised my state using normalizr principles so there is an array of elementIds and an object containing the element properties referenced by the elementIds (see initial state in code sample below).
aim
My aim is simply for the two rendered elements to be editable. I'm successfully dispatching an action CHANGE_ELEMENT_VALUE to my store using an onChange callback, and action.id (referencing the id of the changed element) and action.value (the new value) are available in the reducer (see code below).
problem
My problem is that the text fields aren't changing when I type, even though I can see state changing using the devtools redux extension. My understanding is that react is not recognising a state change because the change is deep in state, and I'm not successfully creating a new state object, I'm probably referencing old instances somehow.
reducer code
Below is my bungled attempt to force a new state object. I'm assuming it's not working because my components are not being re-rendered. It also seems very inelegant.
let initialState = {
isLoading: false,
data: {
elementIds: ['name', 'email'],
elements: {
'name': {'id': 'name', 'value':'ben'},
'email': {'id':'email', 'value':'ben#test.com'},
},
},
error: false
}
function formReducer(state = initialState, action = null) {
switch(action.type) {
case types.CHANGE_ELEMENT_VALUE:
let newData = Object.assign(state.data)
newData.elements[action.id].value = action.value
return {...state, data: newData}
default:
return state;
}
}
export default formReducer;
you can make use of immutability-helper npm package and update your values in your reducer
import update from 'immutability-helper';
let initialState = {
isLoading: false,
data: {
elementIds: ['name', 'email'],
elements: {
'name': {'id': 'name', 'value':'ben'},
'email': {'id':'email', 'value':'ben#test.com'},
},
},
error: false
}
function formReducer(state = initialState, action = null) {
switch(action.type) {
case types.CHANGE_ELEMENT_VALUE:
return update(state, {
data : {
elements: {
[action.id]: {
value: {
$set: 'new value'
}
}
}
}
})
default:
return state;
}
}
export default formReducer;
update() provides simple syntactic sugar around this pattern to make
writing this code easier. While the syntax takes a little getting used
to (though it's inspired by MongoDB's query language) there's no
redundancy, it's statically analyzable and it's not much more typing
than the mutative version.
Object.assign only operates one level deep; i.e. it does not recursively clone the entire object tree. So, your top-level object is cloned but it doesn't trigger a rerender since your reducer mutates a value deep in the cloned object.
I would recommend looking into the deep-extend package and updating your state as follows:
import extend from 'deep-extend';
...
return extend(state, {
elements: {
[key]: value
}
});

Resources