Unable to convert deep cloning to immer - reactjs

I'm working on some legacy code that used to do full deep cloning of the redux state in a react application. Here's the original boilerplate, which is used as a basis for the reducers used in the application:
export default (initialState, handlers = {}, promiseActionTypes = []) =>
(state = initialState, action) => {
let nextState = _.cloneDeep(state)
promiseActionTypes.forEach((actionType) => {
if (action.type === `##REMOTE/${actionType}_REQUEST`) {
nextState.isFetching = true
}
if (action.type === `##REMOTE/${actionType}_SUCCESS`) {
nextState = {
...nextState,
data: _.get(action, 'data.storeData', action.data),
isFetching: false,
isInited: true,
}
}
if (action.type === `##REMOTE/${actionType}_FAILURE`) {
nextState.isFetching = false
}
})
if (handlers.hasOwnProperty(action.type)) {
nextState = handlers[action.type](nextState, action, _.cloneDeep(state))
}
return nextState
}
Those clone deeps are all big no-nos, so I'm trying to leverage immer's produce function to mutate drafted copies of the state before returning the new state.
Problem is, I've been unable to get everything in sync. Some pieces of state won't update correctly here or there. Here's my attempt at refactor so far:
import produce from 'immer'
export default (initialState, handlers = {}, promiseActionTypes = []) =>
(state = initialState, action) => {
return produce(state, (draft) => {
promiseActionTypes.forEach((actionType) => {
if (action.type === `##REMOTE/${actionType}_REQUEST`) {
draft.isFetching = true
}
if (action.type === `##REMOTE/${actionType}_SUCCESS`) {
draft.data = _.get(action, 'data.storeData', action.data)
draft.isFetching = false
draft.isInited = true
}
if (action.type === `##REMOTE/${actionType}_FAILURE`) {
draft.isFetching = false
}
return draft
})
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](draft, action, state)
}
return draft
})
}
I tried to unfreeze the touched objects, but still no dice. Is my implementation just off? Or am I misunderstanding how produce works?
Hell, do I ever need something like immer if I'm just trying to get those two lodash cloneDeep calls out of there?
EDIT: Here's an example of a custom handler that would invoke the final line:
LOCATION_CHANGE: (state, action) => {
// bootstrap
if (_.isUndefined(state.location)) {
state.location = action.location
}
state.next = {
location: action.location,
changed: action.changed,
}
state.isNavigating = true
return state
},
VIEW_ROUTE_MATCH: (state, action) => {
state.next = {
...state.next,
match: action.match,
view: action.view,
}
return state
},

I'd say you don't need Immer, in this instance, as you're not really making deep or complex changes, but it can still help tidy up parts. Immer's going to force three lines of code, so I stick with the spread operator if that'll keep it to one line.
export default (initialState, handlers = {}, promiseActionTypes = []) =>
(state = initialState, action) => {
let nextState = state;
promiseActionTypes.forEach((actionType) => {
if (action.type === `##REMOTE/${actionType}_REQUEST`) {
nextState = { ...nextState, isFetching: true };
}
if (action.type === `##REMOTE/${actionType}_SUCCESS`) {
nextState = produce(state, draft => {
draft.data = _.get(action, 'data.storeData', action.data);
draft.isFetching = false;
draft.isInited = true;
});
}
if (action.type === `##REMOTE/${actionType}_FAILURE`) {
nextState = { ...nextState, isFetching: false };
}
})
if (handlers.hasOwnProperty(action.type)) {
nextState = handlers[action.type](nextState, action);
}
return nextState
}
const handlers = {
LOCATION_CHANGE: (state, action) => {
return produce(state, draft => {
if (_.isUndefined(state.location)) {
draft.location = action.location;
}
draft.next = {
location: action.location,
changed: action.changed,
}
draft.isNavigating = true
});
},
VIEW_ROUTE_MATCH: (state, action) => {
return produce(state, draft => {
draft.match = action.match;
draft.view = action.view;
});
},
}

The first issue is that your original code really shouldn't have been doing a "deep clone" at all, and especially not at the top of the reducer. That means it was doing extra work for every dispatched action, even if it wasn't relevant, and also likely causing the UI to re-render even if the data's values hadn't really changed. So, as you said, a "big no-no". Based on that, yes, I'd say Immer is useful here.
Second, you shouldn't be using promiseActionTypes.map() conceptually, since you're trying to do some updates instead of returning a new array. Use .forEach() as the original code did.
Third, you haven't described which fields aren't being update properly, so it's a bit hard
Beyond that, the draft code looks okay. However, the handlers line looks suspicious in this case. My guess is that those additional handlers are probably doing their own immutable work, rather than attempting to "modify" the draft state value. Oh, and you aren't even passing the draft to them anyway. So, if you're expecting those to be generating the rest of the changes, you need to A) pass draft in to the handlers, and B) change them to "modify" the draft.

Related

react-redux state is always same and not chainging

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.

Redux adds duplicate array item when loaded from state

Hey everyone probably a simple question, basically I have a button when i click it fires an action and passes down the whole object that I concat to array if its not duplicate but strangely what happens because I save data to local-storage and after I load it from there it does not check for duplicate and duplicates the array item. My reducer code below maybe the error is there?
Searched as much as possible.
const initialState = {
favourites: []
};
const favourites = (state = initialState, action) => {
const { payload } = action;
switch (action.type) {
case actionTypes.ADD_FAVOURITES:
return {
...state,
favourites:
state.favourites.indexOf(payload) === -1
? state.favourites.concat(payload)
: state.favourites
};
default:
return state;
}
};
The issue here seems to be that state.favourites.indexOf(payload) === -1 is always true. This is because the function Array.prototype.findIndex() does not find identical objects.
You should use an alternate method of checking to see if the payload object is already in the favourites array. For example, you could try something like this:
const favourites = (state = initialState, action) => {
const { payload } = action;
switch (action.type) {
case actionTypes.ADD_FAVOURITES:
return {
...state,
favourites:
JSON.stringify(state.favourites).indexOf(JSON.stringify(payload)) === -1
? state.favourites.concat(payload)
: state.favourites
};
default:
return state;
}
};

React Native ShouldComponentUpdate not firing between Redux Props Changes

In optimizing my component for performance, I noticed that sequential shouldComponentUpdate calls were seemingly missing the prop change from my redux store. An example being:
In props:
uiBehavior: {
shouldShow: false,
}
Redux Action:
fireActionToShow()
Redux Reducer:
case ActionType.UPDATE_UI_BEHAVIOR: return {...state, shouldShow: true}
Then I'm seeing:
shouldComponentUpdate(nextProps) {
// Would expect to at some point see:
this.props.shouldShow === false
nextProps === true
// Instead, only seeing
this.props.shouldShow === false
nextProps === false
// then
this.props.shouldShow === true
nextProps === true
}
** Note that this clearly isn't the code, just an example
It seems to me that a prop change isn't causing a rerender attempt, or am I missing something?
Thanks.
Expanding for clarity, here is some of the real code:
*** the event in the Action updates the uiBehavior prop on the redux store.
componentDidUpdate(prevProps, prevState) {
const { uiBehavior } = this.props;
if (!_.isEqual(uiBehavior.lockAttributes, prevProps.uiBehavior.lockAttributes)) {
console.log('Lock has changed.'); // This never gets called
}
}
const mapStateToProps = function(state){
return {
uiBehavior: state.uiBehavior,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SlotMachine)
*** UPDATE
JS Call:
this.props.setGeneralUiKeyValue('lockAttributesInCart', !lockAttributes);
Action:
export const setGeneralUiKeyValue = (key, value) => { return { type: GENERAL_UI_UPDATE, key, value }}
Reducer:
export const uiBehavior = (state = {}, action) => {
let newUiState = {...defaultState, ...state}
switch (action.type) {
case uiActionTypes.GENERAL_UI_UPDATE:
newUiState.general = newUiState.general || {};
newUiState.general[action.key] = action.value;
return newUiState
default:
return newUiState;
}
return newUiState

Slice of Redux state returning undefined, initial state not working in mapStateToProps

I am trying to simply sort on the redux store data in mapStateToProps, similar to how it is being done in Dan Abramov's Egghead.io video: https://egghead.io/lessons/javascript-redux-colocating-selectors-with-reducers
My problem is, initially the state is returning undefined (as it is fetched asynchronously), so what would be the best way to deal with this? Current code is as follows (_ is the ramda library):
const diff = (a, b) => {
if (a.name < b.name) {
return -1
}
if (a.name > b.name) {
return 1
}
return 0
}
const mapStateToProps = (state) => {
return {
transactions: _.sort(diff, state.transactions.all),
expenditure: state.expenditure.all,
income: state.income.all
}
}
I thought that transactions.all should initially be an empty array (which would mean the code would work) because of the initial state set in the reducer:
const INITIAL_STATE = { transactions: { all: [] }, transaction: null }
export default function (state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_TRANSACTION:
return { ...state, transaction: action.payload.data }
case FETCH_TRANSACTIONS:
return { ...state, all: action.payload.data }
case EDIT_TRANSACTION:
return { data: action.data }
case ADD_TRANSACTION:
return { data: action.data }
case DELETE_TRANSACTION:
return { ...state }
default:
return state
}
}
Thanks in advance.
As you said, it is fetched asynchronously. Perhaps when the component rendered, data isn't ready yet which resulted to an undefined object.
const SampleComponent = (props) => {
if(props.transaction === undefined)
return <Spinner /> // Loading state
else
// your implementation
}
You can further make the code cleaner as explained by Dan himself in the docs here: http://redux.js.org/docs/advanced/AsyncActions.html
Managed to solve this, because in combine reducers, I had set transactions with the name transactions and then in the reducer, I essentially had the initial state set to transactions: { all: [] } }.
This was causing state.transactions.all to be undefined, as the correct state structure was actually state.transactions.transactions.all.
After updating the transactions reducer to:
const INITIAL_STATE = { all: [], transaction: null }
export default function (state = INITIAL_STATE, action) {
switch (action.type) {...
The initial empty transactions array prior to the promise returning meant the sort no longer causes an error, and is then correctly sorted on load.

Adding item 2 levels deep in Redux Reducer

I am trying to add notes to a task object but what I have so far adds it to all the tasks. When I try different ways, it doesn't compile. The Object.assign doesn't like coming after the .push()
When it adds to all task:
let taskReducer = function(tasks = [], action) {
switch (action.type) {
case 'ADD_NOTE':
return tasks.map((task) => {
const { notes } = task;
const { text } = action;
notes.push({
text,
id: notes.length,
})
return task.id === action.id ?
Object.assign({}, { task, notes }) : task
})
When it doesn't compile:
let taskReducer = function(tasks = [], action) {
switch (action.type) {
case 'ADD_NOTE':
return tasks.map((task) => {
return task.id === action.id ?
const { notes } = task;
const { text } = action;
notes.push({
text,
id: notes.length,
})
Object.assign({}, { task, notes }) : task
})
You almost never want to use Array.push() in a reducer, because that directly mutates the existing array, and direct mutations generally break UI updates (see the Redux FAQ). You could use push() on a new copy of the old array, but most examples don't use that approach. Most of the time, the suggested approach is to use const newArray = oldArray.concat(newValue), which returns a new array reference containing all the old items plus the new item.
Beyond that, keep in mind that when updating nested data immutably, every level of nesting needs to have a copy made and returned.
Haven't actually tested this, but I think your code needs to look roughly like this example:
let taskReducer = function(tasks = [], action) {
switch (action.type) {
case 'ADD_NOTE':
return tasks.map((task) => {
if(action.id !== task.id) {
return task;
}
const { notes } = task;
const { text } = action;
const newNotes = notes.concat({id : notes.length, text});
const newTask = Object.assign({}, task, {notes : newNotes});
return newTask;
}
default : return tasks;
}
}

Resources