Redux reducer state mutations and restoring initial state - javascript

What's the best way to restore initial state?
For this example let's say I can edit a car by dispatching the following actions:
dispatch(actions.editModel('Civic'));
dispatch(actions.editType({
make: 'Ford',
model: 'Focus'
}));
dispatch(actions.restoreInitialState());
My reducer looks like this:
const initialState = {
id: '1',
vehicle: 'car',
type: {
make: 'Honda',
model: 'Accord'
},
license: 'abc'
}
export default createReducer({
[actions.editType]: (state, payload) => ({
...state,
type: payload // payload is an object
}),
[actions.editModel]: (state, payload) => ({
...state,
type: {
...state.type,
model: payload // payload is a string
}
}),
[actions.restoreInitialState]: (state) => ({
state: initialState // initial state has nested objects
})
}, initialState)
Is there a risk I am mutating my state or restoring my initial state incorrectly?
This might be overkill but I was thinking of editing my reducers like this:
export default createReducer({
[actions.editType]: (state, payload) => ({
...state,
type: {
...payload // payload is an object
}
}),
[actions.editModel]: (state, payload) => ({
...state,
type: {
...state.type,
model: payload // payload is a string
}
}),
[actions.restoreInitialState]: (state) => ({
state: {
...initialState // initial state has nested objects
}
})
}, initialState)
Is there a difference when I'm passing an object through the payload vs just referencing my initial state? (Plus my initial state contains nested objects)

You've got a great question. And to answer this, you need to think about why its so important to avoid mutating data in React. On every change in state - React does a shallow comparison of the updated virtual DOM with the old virtual DOM. And in this shallow comparison - when it comes across objects - it only checks the address of the object. So - as long as you have a new address for the parent - the DOM will update correctly.
Now, everytime you return from the reducer - as long as you are returning a new object with the updated state - return {... state} or an object with a different address - eg. return initialState - it's perfect. You don't need to worry about mutations. This is true even if you have a nested object within the state. As long as you change the address of the parent - the DOM will update correctly. So feel free to use the code like you did in the first case. You don't need to spread over the nested objects. Your payload will anyway have a different address.
The only thing to be weary about is doing something like this:
case [actions.editModel]:
const updatedState = state
updatedState.model = payload;
return updatedState;
In this case, the state object gets passed by reference to updatedState - that means both of them will share the same address. And since you're returning updatedState - the address hasn't changed and the DOM won't update correctly/consistently.

You can simply do this:
[actions.restoreInitialState]: () => initialState;

Related

Redux | Why this store's parameter type changes after second click?

I'm trying to push a new value in the store's state. It works fine the first time I click on the button "Add item", but the second time I got the following error: "state.basket.push is not a function". I configure the action to console log the state and got the following results:
1st click: {...}{basketItems: Array [ "44" ]}
2nd click: Object {basketItems: 0 }
Why the variable type is changing from array to an int?
Here is the code for the rendered component:
function Counter({ basketItems,additem }) {
return (
<div>
<button onClick={additem}>Add item</button>
</div>
);
}
const mapStateToProps = state => ({
basketItems: state.counterReducer.basketItems,
});
const mapDispatchToProps = dispatch => {
return {
additem: ()=>dispatch({type: actionType.ADDITEM, itemName:'Dummy text' }),
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter);
And the reducer looks like this:
import {ADDITEM} from "../actions/types";
const initialState = { basket: [], };
export default function reducer(state = initialState, action) {
switch (action.type) {
case ADDITEM:
console.log(state);
// let newBasket = state.basket.push('44');
return {
...state,
basket: state.basket.push('44')
};
default:
return state;
}
}
I'm copying the state before updating the basket to prevent weird behaviors.
There's two problems here:
state.basket.push() mutates the existing state.basket array, which is not allowed in Redux
It also returns the new size of the array, not an actual array
So, you're not doing a correct immutable update, and you're returning a value that is not an array.
A correct immutable update here would look like:
return {
...state,
basket: state.basket.concat("44")
}
Having said that, you should really be using our official Redux Toolkit package, which will let you drastically simplify your reducer logic and catch mistakes like this.

Redux initial state gets mutated even when using Object.assign

This is a simple replication of a problem i encounter in an actual app.
https://jsfiddle.net/zqb7mf61/
Basically, if you clicked on 'Update Todo" button, the text will change from "Clean Room" to "Get Milk". "Clean Room" is a value in the initial State of the reducer. Then in my React Component, I actually try to clone the state and mutate the clone to change the value to "Get Milk" (Line 35/36). Surprisingly, the initial State itself is also mutated even though I try not to mutate it (as seen in line 13 too).
I am wondering why Object.assign does not work for redux.
Here are the codes from the jsFiddle.
REDUX
const initState = {
task: {id: 1, text: 'Clean Room'}
}
// REDUCER
function todoReducer (state = initState, action) {
switch (action.type) {
case 'UPDATE_TODO':
console.log(state)
let newTodo = Object.assign({}, state) // here i'm trying to not make any changes. But i am surpise that state is already mutated.
return newTodo
default:
return state;
}
}
// ACTION CREATORS:
function updateTodo () {
return {type: 'UPDATE_TODO'};
}
// Create Store
var todoStore = Redux.createStore(todoReducer);
REACT COMPONENT
//REACT COMPONENT
class App extends React.Component{
_onSubmit = (e)=> {
e.preventDefault();
let newTodos = Object.assign({}, this.props.todos) // here i clone the redux state so that it will not be mutated, but i am surprise that it is mutated and affected the reducer.
newTodos.task.text = 'Get Milk'
console.log(this.props.todos)
this.props.updateTodo();
}
render(){
return (
<div>
<h3>Todo List:</h3>
<p> {this.props.todos.task.text} </p>
<form onSubmit={this._onSubmit} ref='form'>
<input type='submit' value='Update Todo' />
</form>
</div>
);
}
}
// Map state and dispatch to props
function mapStateToProps (state) {
return {
todos: state
};
}
function mapDispatchToProps (dispatch) {
return Redux.bindActionCreators({
updateTodo: updateTodo
}, dispatch);
}
// CONNECT TO REDUX STORE
var AppContainer = ReactRedux.connect(mapStateToProps, mapDispatchToProps)(App);
You use Object.assign in both the reducer as in the component. This function only copies the first level of variables within the object. You will get a new main object, but the references to the objects on the 2nd depth are still the same.
E.g. you just copy the reference to the task object around instead of actually creating a new task object.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Deep_Clone
Apart from that it would be better to not load the whole state into your component and handle actions differently. Lets just solve this for now. You will have to create a new task object in your onSubmit instead of assigning a new text to the object reference. This would look like this:
newTodos.task = Object.assign({}, newTodos.task, {text: 'Get Milk'})
Furthermore to actually update the store, you will have to edit your reducer as you now assign the current state to the new state. This new line would look like this:
let newTodo = Object.assign({}, action.todos)

Pass by reference assignment mutates redux state

I have an app with a menu of items, and at some point a user may edit the values of the items. When the user does so, I create a copy of the item in a seperate state branch instead of changing the original menu items. So my reducer looks like this:
const menuReducer = (state = [], action) => {
switch (action.type) {
case ADD_ITEM:
return [...state, {id: action.itemId, propA: action.itemPropA, propB: action.itemPropB}]
}
}
const editingMenuItem = (state = {}, action) => {
switch (action.type) {
case SET_EDIT_ITEM:
return {id: action.id, propA: action.itemPropA, propB: action.itemPropB}
case EDIT_ITEM:
return {id: state.id, propA: action.itemPropA, propB: action.itemPropB}
}
}
Someone selects that they want to edit an item, and this causes the dispatchEditItem thunk to trigger and create a copy in the state tree:
const dispatchEditItemThunk = itemId => (dispatch, getState) => {
const item = _.find(getState().menu, ['id', itemId]);
dispatch(setEditItem(item.id, item.propA, item.propB))
}
Then when someone wants to edit a prop, the editingThunk is dispatched:
const editingThunk = (itemId, propName) => (dispatch, getState) => {
let activeItem = getState().editingMenuItem;
// someValue is generated here
activeItem[propName] = someValue
dispatch(editItem(activeItem.propA, activeItem.propB))
}
The problem with this is that when activeItem[propName] = someValue happens, this changes the value of the item contained in the menuReducer array. I'm assuming because everything is pass by reference, and all the references lead back to the original value in the menuReducer. However, this isn't the way I would expect this to work. My assumption would be that calling getState would return a deep copy of the state, and not allow for these kinds of accidental mutations.
Is this a bug? If it isn't, is there a preferred way of writing thunks that avoids this kind of situation? In my real use case, the structure of the props in the menuItem is very complex, and it is handy to create an activeItem in the thunk and mutate it's values before dispatching to the state tree. Is doing this bad?
That's not a bug and mutating state object is highly discouraged. You can create a deep copy of an object using Object.assign and JSON.stringify methods as described here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign (Examples section).
If Redux was to create a deep copy of state on each dispatch call it could be more secure but also much slower.

redux - reducer state is blank

I am trying to replicate something similar to the TodoList example in the the redux docs' basic example. The second reducer receives an array - styleItems = [{... ... }, {... ...}] - and then calls the first function to act on each of the individual objects.
I provide an initialState to the app container via the following, as shown in containers/app.js. However, the state passed to the styleItems reducer seems to be a blank array - each and every time.
However, react renders the UI based on the initial config, and react dev-tools shows the state structure as expected. Is the redux store somehow seeing the same thing as react?
containers/app.js
function starterInfo(state) {
return {
// The ID of this particular object
id: 12345,
// Various keys and theri css values
styleItems: [
{
pk: 31,
order: 1,
label: 'Caption text color',
css_identifier: '.caption-text',
css_attribute: 'color',
css_value: '#FFFFFF'
},
{
pk:23,
order: 2,
label: 'Caption link color',
css_identifier: '.caption-link',
css_attribute: 'color',
css_value: '#FEFEFE'
}
],
// Network state info
currently_fetching: false,
currently_posting: false
}
}
export default connect(starterInfo)(App)
reducers/index.js
// This handles a single styleItem object within the array
function change_css(state = {}, action){
switch (action.type){
case actions.CHANGE_CSS:
if (state.order !== action.order){
return state
}
return {
...state,
css_value
}
default:
return state
}
}
// This handles the styles array in the global state
function styleItems(state = [], action){
switch(action.type){
case actions.CHANGE_CSS:
const foobar = state.map(styleItem =>
change_css(styleItem, action)
)
return foobar
default:
return state
}
}
The short answer is that you're not passing the initial state quite right. The first argument to the connect function for the React Redux bindings is mapStateToProps. The point of this function is to take the state that already exists in your app and map it to props for your component. What you're doing in your starterInfo function is kind of just hard-coding what the state is for your component. Because you're returning a plain object React doesn't really know the difference so it works just fine, but Redux doesn't yet know about your app state.
Instead, what you should do is provide your initial state directly to the reducers, like this:
const intialStyleItemsState = [
{
pk: 31,
order: 1,
label: 'Caption text color',
css_identifier: '.caption-text',
css_attribute: 'color',
css_value: '#FFFFFF'
},
{
pk:23,
order: 2,
label: 'Caption link color',
css_identifier: '.caption-link',
css_attribute: 'color',
css_value: '#FEFEFE'
}
];
function styleItems(state = intialStyleItemsState, action){ ...
And eventually, because you're splitting your reducers up you'll need to combine them back together again with Redux's combineReducers utility, provide that root reducer to your store and go from there.
You can also pass the initial state using the redux function createstore that take as argument createStore(reducer, [initialState]) http://rackt.org/redux/docs/api/createStore.html
Let’s say you have two reducers
change_css(state = {}, action)
function styleItems(state = [], action)
If you use comibneReducer to initialize your state
var reducer = combineReducers({
css: change_css,
items: styleItems
})
Now
var store = createStore(reducer)
console.log(store.getState())
Your store will contain { css: {}, items: [] }
Now if you want to initialize the state you can pass the initial state as the second argument of the createStore function.
createStore(reducer, {css:{some properties},items:[{name:"obj1"},{name:"obj2"},{name:"obj3"}]})
Now you store will contain the initial state. {css:{some properties,items:[{name:"obj1"},{name:"obj2"},{name:"obj3"}]}
You can feed this state from server for example and set it as initial state of your application

Is this the correct way to delete an item using redux?

I know I'm not supposed to mutate the input and should clone the object to mutate it. I was following the convention used on a redux starter project which used:
ADD_ITEM: (state, action) => ({
...state,
items: [...state.items, action.payload.value],
lastUpdated: action.payload.date
})
for adding an item - I get the use of spread to append the item in the array.
for deleting I used:
DELETE_ITEM: (state, action) => ({
...state,
items: [...state.items.splice(0, action.payload), ...state.items.splice(1)],
lastUpdated: Date.now()
})
but this is mutating the input state object - is this forbidden even though I am returning a new object?
No. Never mutate your state.
Even though you're returning a new object, you're still polluting the old object, which you never want to do. This makes it problematic when doing comparisons between the old and the new state. For instance in shouldComponentUpdate which react-redux uses under the hood. It also makes time travel impossible (i.e. undo and redo).
Instead, use immutable methods. Always use Array#slice and never Array#splice.
I assume from your code that action.payload is the index of the item being removed. A better way would be as follows:
items: [
...state.items.slice(0, action.payload),
...state.items.slice(action.payload + 1)
],
You can use the array filter method to remove a specific element from an array without mutating the original state.
return state.filter(element => element !== action.payload);
In the context of your code, it would look something like this:
DELETE_ITEM: (state, action) => ({
...state,
items: state.items.filter(item => item !== action.payload),
lastUpdated: Date.now()
})
The ES6 Array.prototype.filter method returns a new array with the items that match the criteria. Therefore, in the context of the original question, this would be:
DELETE_ITEM: (state, action) => ({
...state,
items: state.items.filter(item => action.payload !== item),
lastUpdated: Date.now()
})
Another one variation of the immutable "DELETED" reducer for the array with objects:
const index = state.map(item => item.name).indexOf(action.name);
const stateTemp = [
...state.slice(0, index),
...state.slice(index + 1)
];
return stateTemp;
Deleting an item using redux in different ways.
Method 1: In that case is used createSlice( .. )
const { id } = action.payload; // destruct id
removeCart: (state, action) =>{
let { id } = action.payload;
let arr = state.carts.filter(item => item.id !== parseInt(id))
state.carts = arr;
}
Method 2: In that case is used switch (... ), spread-operator
const { id } = action.payload; // destruct id
case actionTypes.DELETE_CART:
return {
...state,
carts: state.carts.filter((item) => item.id !== payload)
};
For both methods initialized this state:
initialState: {
carts: ProductData, // in productData mocked somedata
}
The golden rule is that we do not return a mutated state, but rather a new state. Depending on the type of your action, you might need to update your state tree in various forms when it hits the reducer.
In this scenario we are trying to remove an item from a state property.
This brings us to the concept of Redux’s immutable update (or data modification) patterns. Immutability is key because we never want to directly change a value in the state tree, but rather always make a copy and return a new value based on the old value.
Here is an example of how to delete a nested object:
// ducks/outfits (Parent)
// types
export const NAME = `#outfitsData`;
export const REMOVE_FILTER = `${NAME}/REMOVE_FILTER`;
// initialization
const initialState = {
isInitiallyLoaded: false,
outfits: ['Outfit.1', 'Outfit.2'],
filters: {
brand: [],
colour: [],
},
error: '',
};
// action creators
export function removeFilter({ field, index }) {
return {
type: REMOVE_FILTER,
field,
index,
};
}
export default function reducer(state = initialState, action = {}) {
sswitch (action.type) {
case REMOVE_FILTER:
return {
...state,
filters: {
...state.filters,
[action.field]: [...state.filters[action.field]]
.filter((x, index) => index !== action.index)
},
};
default:
return state;
}
}
To understand this better, make sure to check out this article: https://medium.com/better-programming/deleting-an-item-in-a-nested-redux-state-3de0cb3943da

Categories

Resources