When do I must use the spread operator in useReducer? - javascript

I've noticed that in many useReducer examples, the spread operator is used in the reducer like this:
const reducer = (state, action) => {
switch (action.type) {
case 'increment1':
return { ...state, count1: state.count1 + 1 };
case 'decrement1':
return { ...state, count1: state.count1 - 1 };
case 'increment2':
return { ...state, count2: state.count2 + 1 };
case 'decrement2':
return { ...state, count2: state.count2 - 1 };
default:
throw new Error('Unexpected action');
}
};
However in many of my practices, I removed ...state and had no issues at all. I understand that ...state is used to preserve the state of the remaining states, but would a reducer preserve those states already so the ...state is not needed?
Can someone give me some examples where ...state is a must and causes issues when removed with useReducer hook? Thanks in advance!

No, a reducer function alone would not preserve existing state, you should always be in the habit shallow copy existing state. It will help you avoid a class of state update bugs.
A single example I can think of when spreading the existing state may not be necessary is in the case where it isn't an object.
Ex: a "count" state
const reducer = (state = 0, action) => {
// logic to increment/decrement/reset state
};
Ex: a single "status" state
const reducer = (state = "LOADING", action) => {
// logic to update status state
};
Spreading the existing state is a must for any state object with multiple properties since a new state object is returned each time, in order to preserve all the existing state properties that are not being updated.
Edit 1
Can you give an example when NO shallow copying causing state update bugs?
const initialState = {
data: [],
loading: false,
};
const reducer = (state, action) => {
switch(action.type) {
case LOAD_DATA:
return {
...state,
loading: true,
};
case LOAD_DATA_SUCCESS:
return {
...state,
data: action.data,
loading: false
};
case LOAD_DATA_FAILURE:
return {
loading: false,
error: action.error,
};
default:
return state;
}
};
As can been seen in this example, upon a data load failure the reducer neglects to copy the existing state into the new state object.
const [state, dispatch] = useReducer(reducer, initialState);
...
useEffect(() => {
dispatch({ type: LOAD_DATA });
// logic to fetch and have loading failure
}, []);
return (
<>
...
{state.data.map(...) // <-- throws error state.data undefined after failure
...
</>
);
Any selector or UI logic that assumes state.data always exists or is always an array will fail with error. The initial render will work since state.data is an empty array and can be mapped, but upon a loading error state.data is removed from state.

Related

Which method to use when updating state inside React.useReducer

Lets imagine a simple reducer:
const [state, setState] = React.useReducer(myReducer, {})
And myReducer with one single case (simplified version):
const myReducer = (state, action) => {
switch (action.type) {
case 'UPDATE_STATE': {
// Code to update here...
}
}
}
Now, which of the following statements is best suited to update a state - and why:
NOTE: payload = state.
METHOD 1
case 'UPDATE_STATE': {
const updatedState = action.payload
updatedState.object = true
return updatedState
}
METHOD 2
case 'UPDATE_STATE': {
const updatedState = action.payload
updatedState.object= true
return { ...state, updatedState }
}
METHOD 3
case 'UPDATE_STATE': {
const updatedState = action.payload
updatedState.object= true
return { ...state, ...updatedState }
}
METHOD 4
case 'UPDATE_STATE': {
return ({ ...state, object: true })
}
I think you should Use method 4. You are not mutating the state here, which is the React way.
case 'UPDATE_STATE': {
return ({ ...state, object: true })
}
Note: Spread operator ... creates different references up to one level only.
EDIT: Based on comments, here is a simple example of how you could be mutating state, even with the simplest of objects if you are not using ....
let state = { object : true};
let payload = state;
state.object = false;
console.log(state);
console.log(payload);
Even with ... you might have deeply nested objects. They will have the same problem, unless you want to spread at every level. There are libraries to help you with deep objets like immutable.js.

How to reset a reducer which uses the same key but a different action?

So this is my current reducer:
import { Reducer } from 'redux';
import {
EventState,
LOAD_EVENTS,
LOAD_EVENT_BY_ID,
FETCH_MORE_EVENTS
} from '../types/eventTypes';
export const initialState = {
eventsList: [],
event: undefined,
isLastPage: false
};
const eventReducers: Reducer<EventState, any> = (
state = initialState,
action
) => {
switch (action.type) {
case LOAD_EVENTS:
return {
...state,
eventsList: action.eventsList
};
case FETCH_MORE_EVENTS:
return {
state,
eventsList: state.eventsList.concat(action.eventsList),
isLastPage: action.eventsList.length === 0
};
default:
return state;
}
};
export default eventReducers;
As you see both cases LOAD_EVENTS and FETCH_MORE_EVENTS share the key eventsList, on fetch more events I am calling state like this state instead of ...state because it seems to re init the state of the whole reducer. But, is that the proper way? I think that if this reducer grows up, that will be a bug.
So what can I do to clean that reducer properly to make? Like all I need is that LOAD_EVENTS fires then eventsList should get clear and fill out again by what LOAD_EVENTS brings. And basically I only need to reset the state of eventsList but rest should remain the same.
when you calling state like state instead of ...state, you aren't re-init the state, but storing the previous state inside the new state, like this example below:
state = {
eventsList: [...someEvents],
event: undefined,
isLastPage: false,
state: {
eventsList: [...someEvents],
event: undefined,
isLastPage: false,
state: {
eventsList: [...someEvents],
event: undefined,
isLastPage: false
}
}
};
This is not a good pattern/practice, only if is super necessary.
So the correct, it's reset the previous state with initialState when fetch more events.
export const initialState = {
eventsList: [],
event: undefined,
isLastPage: false
};
const eventReducers: Reducer<EventState, any> = (
state = initialState,
action
) => {
switch (action.type) {
case LOAD_EVENTS:
return {
...state,
eventsList: action.eventsList
};
case FETCH_MORE_EVENTS:
return {
...initialState,
eventsList: state.eventsList.concat(action.eventsList),
isLastPage: action.eventsList.length === 0
};
default:
return state;
}
};
But how you say, it's only need to reset the state of eventsList but rest should remain the same, you can keep the same to this reducer:
case LOAD_EVENTS:
return {
...state,
eventsList: action.eventsList
};
Because when you set eventsList like the example above, you are reset the eventsList and fill out again with new data. But don't forget the problem about the first example that I say.

React reducer that does not update the state

From a Redux tutorial I've been going through they allow you to add a place multiple times. I changed the reducer to reject duplicates. My question is, (see code), do I have to return the state if no updates are made or is there some other way of indicating no state is changed?
function placeReducer(state = initialState, action) {
switch (action.type) {
case ADD_PLACE:
const existing = state.places.find((item) => item.value == action.payload);
if (existing) {
return {...state};
}
return {
...state,
places: state.places.concat({
key: Math.random(),
value: action.payload
})
};
default:
return state;
}
}
Just return the state, no need to create a new copy.

Reducer and store state not in sync

Cannot work out what is going on here but basically i have a json file which has a load of products. im trying to then render the ones I want
here is my reducer:
export default(state = initialState, action) => {
switch(action.type){
case Types.SHOW_PRODUCTS: {
console.log('here1');
let productsToShow = data.filter(category => category.link === action.category)
const newState = [].concat(productsToShow[0].products)
return newState;
}
default:
console.log('here2');
return state;
}
}
when I log the state in my store, it says that productsToRender is an array of length 5 (this is correct)
however, when I log (this.props) in one of my components, it says that the length is 0
in the above reducer, the console.log('here 1') is the last console log being called, meaning that it is definitely returning products ( that is verified in the store state). so im not sure why it is then wiping it in that component?
in that component I call this
componentWillMount = () => {
this.props.showProducts(this.props.chosenCategory.category)
}
which passes in the chosen category so I now what products to render
however, logging this.props in the render method below, is showing it to be an empty array
of course I can post more code if necessary but any reason for this funky behaviour?
extra info:
interestingly when I do this:
default:
console.log('here2');
return [{name: 'prod'}];
}
and then log this.props, it now contains this array with this object???
The store should be immutable, that is, the value you return should be made immutable.
I assume you are adding only a single array in the store
Try changing the reducer like,
const initialState = [];
export default(state = initialState, action) => {
switch(action.type){
case Types.SHOW_PRODUCTS: {
console.log('here1');
let productsToShow = data.filter(category => category.link === action.category)
let newState = [...state,...productsToShow[0].products]
return newState;
}
default:
console.log('here2');
return state;
}
}

How to Update specific slice of redux state in React

I seem to have hit a snag when updating state using redux and react-redux. When I update an individual slice of state, all of the others get removed. I know the answer to this will be simple but I can't figure it out and haven't found anything else online.
So to clarify, here's my reducer:
const initialState = {
selectedLevel: null,
selectedVenue: null,
selectedUnitNumber: null,
selectedUnitName: null,
selectedYear: null
}
export default (state = initialState, action) => {
console.log('reducer: ', action);
switch (action.type){
case 'CHOOSE_LEVEL':
return action.payload;
case 'CHOOSE_VENUE':
return action.payload;
case 'CHOOSE_UNIT':
return action.payload;
case 'SHOW_COURSES':
return action.payload;
}
return state;
}
And my combine reducer:
export default combineReducers({
workshopSelection: WorkshopSelectReducer
});
So my initial state looks like this:
workshopSelection: {
selectedLevel: null,
selectedVenue: null,
selectedUnitNumber: null,
selectedUnitName: null,
selectedYear: null
}
But when I use one of my action creators, for example:
export function chooseVenue(venue){
return {
type: 'CHOOSE_VENUE',
payload: {
selectedVenue: venue
}
}
}
I end up with state looking like this:
workshopSelection: {
selectedVenue: 'London',
}
All of the rest of the state within this object that wasn't affected by this action creator has been completely wiped out. Instead, I just want all other entries to stay as they are with their original values - null in this example, or whatever other value has been assigned to them.
Hope that all makes sense.
Cheers!
You are basically replacing one object (previous state) with another one (your payload, which is also an object).
In terms of standard JS, this would be the equlivalent of what your reducer does:
var action = {
type: 'CHOOSE_VENUE',
payload: {
selectedVenue: venue
}
};
var state = action.payload;
The simplest way to fix this would be using Object spread properties:
export default (state = initialState, action) => {
switch (action.type){
case 'CHOOSE_LEVEL':
case 'CHOOSE_VENUE':
case 'CHOOSE_UNIT':
case 'SHOW_COURSES':
// Watch out, fall-through used here
return {
...state,
...action.payload
};
}
return state;
}
... but since this is still in experimental phase, you have to use some other way to clone previous properties and then override the new ones. A double for ... in loop could be a simple one:
export default (state = initialState, action) => {
switch (action.type){
case 'CHOOSE_LEVEL':
case 'CHOOSE_VENUE':
case 'CHOOSE_UNIT':
case 'SHOW_COURSES':
// Watch out, fall-through used here
const newState = {};
// Note: No key-checks in this example
for (let key in state) {
newState[key] = state[key];
}
for (let key in action.payload) {
newState[key] = action.payload[key];
}
return newState;
}
return state;
}
Keep your payload object as flat on actions creators as shown below...
export function chooseVenue(venue){
return {
type: 'CHOOSE_VENUE',
selectedVenue: venue
}
}
and modify your reducer as below (given example is for updating the venue, do the same for other cases too...)
export default (state = initialState, action) => {
let newState = Object.assign({}, state); // Take copy of the old state
switch (action.type){
case 'CHOOSE_LEVEL':
case 'CHOOSE_VENUE':
newState.selectedVenue = action.selectedVenue; // mutate the newState with payload
break;
case 'CHOOSE_UNIT':
case 'SHOW_COURSES':
default :
return newState;
}
return newState; // Returns the newState;
}

Categories