Update deeply nested state object in redux without spread operator - reactjs

I've been breaking my head for a week or something with this !!
My redux state looks similar to this
{
data: {
chunk_1: {
deep: {
message: "Hi"
}
},
chunk_2: {
something: {
something_else: {...}
}
},
... + more
},
meta: {
session: {...},
loading: true (or false)
}
}
I have an array of keys like ["path", "to", "node"] and some data which the last node of my deeply nested state object should be replaced with, in my action.payload.
Clearly I can't use spread operator as shown in the docs (coz, my keys array is dynamic and can change both in values and in length).
I already tried using Immutable.js but in vain.. Here's my code
// Importing modules ------
import {fromJS} from "immutable";
// Initializing State ---------
const InitialState = fromJS({ // Immutable.Map() doesn't work either
data: { ... },
meta: {
session: {
user: {},
},
loading: false,
error: "",
},
});
// Redux Root Reducer ------------
function StoreReducer(state = InitialState, action) {
switch (action.type) {
case START_LOADING:
return state.setIn(["meta"], (x) => {
return { ...x, loading: true };
});
case ADD_DATA: {
const keys = action.payload.keys; // This is a valid array of keys
return state.updateIn(keys, () => action.payload); // setIn doesn't work either
}
}
Error I get..
Uncaught TypeError: state.setIn (or state.updateIn) is not a function
at StoreReducer (reducers.js:33:1)
at k (<anonymous>:2235:16)
at D (<anonymous>:2251:13)
at <anonymous>:2464:20
at Object.dispatch (redux.js:288:1)
at e (<anonymous>:2494:20)
at serializableStateInvariantMiddleware.ts:172:1
at index.js:20:1
at Object.dispatch (immutableStateInvariantMiddleware.ts:258:1)
at Object.dispatch (<anonymous>:3665:80)
What I want ?
The correct way to update my redux state (deeply nested object) with a array containing the keys.

Please note that you are using an incredibly outdated style of Redux. We are not recommending hand-written switch..case reducers or the immutable library since 2019. Instead, you should be using the official Redux Toolkit with createSlice, which allows you to just write mutating logic in your case reducers (and thus also just using any helper library if you want to use one).
Please read Why Redux Toolkit is how to use Redux today.

you could use something like that:
import { merge, set } from 'lodash';
export default createReducer(initialState, {
...
[updateSettingsByPath]: (state, action) => {
const {
payload: { path, value },
} = action;
const newState = merge({}, state);
set(newState, path, value);
return newState; },
...}

Related

using spread operator (...) in mapStateToProps causes stale value to be passed to child component

I have code like this for my component which is a function component:
useEffect(() => {
if (errors['update'].error || successes['update'].success) {
setUpdateInProgress(false);
}
}, [errors['update'].error, successes['update'].success])
error and success properties are independent of one another, and when either of them is true, I want to call setUpdateInProgress.
in mapStateToProps I have code like this:
const mapStateToProps = (store) => {
return {
// other fields
errors: {...store.prop1.errors, ...store.prop2.errors},
successes: {...store.prop1.errors, ...store.prop2.errors};
}
These errors and successes objects are the props I need to send to my component in order for the useEffect to work. As is apparent, I want to combine properties from multiple places into one(prop1,prop2, ...).
Problem is, it's not working, and the useEffect is not called. To get it to work, I have to split the props like below:
const mapStateToProps = (store) => {
return {
// other fields
prop1Errors: store.prop1.errors,
prop2Errors: store.prop2.errors,
prop1Successes: store.prop1.successes,
prop2Successes: store.prop2.successes
}
Why would the spread operator lead to a stale value being set from the state whereas using the state value directly wouldn't?
Edit: For those interested, errors and successes have a structure like this:
errors: {
create: {
error: true,
message: "Error in create"
},
update: {
error: false,
message: ""
}
//... same thing for other actions like fetch, etc
}
//... same thing for successes
As for the reducer code, it's something like this:
function reducer(state, action) {
if(action === "UPDATE") {
return {...state, errors: { ...state.errors, update: { message: '', error: false }}, successes: { ...state.successes, update: { message: 'Update successful.', success: true }}}
}
}
Spread properties in object initializers copy their own enumerable properties from a provided object onto the newly created object. If the spread property already exists in the newly created object, it will be automatically replaced.
In other words, if you have 2 objects: const foo = {a: 1} and const bar = {a: 2, b: 1}. The result of {...foo, ...bar} would be {a: 2, b: 1}. So you can see that the assigned a property was the one from the last spread object.
This is exactly your case. You must consider deep merging since you have a multi-dimensional structure for errors and successes.
Based on your errors structure, I would do a function that merges the errors/successes as follows:
const mergeMessages = (type, ...messages) =>
{
return messages.reduce((carry, message) => {
for (let [key, value] of Object.entries(message)) {
if (!carry.hasOwnProperty(key) || value[type] === true) {
carry[key] = value;
}
}
return carry;
}, {});
}
Then you can use it like:
const mapStateToProps = (store) => {
return {
// other fields
errors: mergeMessages('error', store.prop1.errors, store.prop2.errors),
successes: mergeMessages('success', store.prop1.successes, store.prop2.successes),
}
}

Change state from reducer not from root state using React-Redux when data is inherited

Good afternoon. I am writing an application using react-redux and faced a dilemma. I have already re-thought it several times and can't choose how to organize the project and data structure correctly and conveniently. Design data by inheriting or composing data. I initially went along the path of composition, but I realized that it is inconvenient when there is a one-to-one relationship. I decided to change it to inheritance, because it seemed logical from the point of view of data organization, but there was a big difficulty with reducers, more precisely, I am confused that it turns out to be a single root reducer with a lot of actionTypeskeys . I remember about performance, when elements inherit a data chain from a common ancestor, that this is very bad. And yet I chose this path and I have a question: Please tell me if it is possible to split into several reducers for each level of nesting data. Example
onst initState: IPages = {
idActive: 0,
pages: [
{
id: 1,
title: `Tab #1`,
workspace: {
idActiveDraggableElements: [],
idActiveLines: [],
attributes: {
height: string,
width: string,
viewBox: [0, 0, 5000, 5000]
},
draggableElements: [], // more data
lines: [], // more data
}
},
]
}
Reducer:
export function pagesReducer(
state: IPages = initState,
action: IPageActionTypes
) {
switch (action.type) {
case "ADD_PAGE":
let uniqId = getUniqKeyIdOfArrayList(state.pages);
return {
...state,
pages: state.pages.concat({id:uniqId, title:`Вкладка - ${uniqId}`})
}
case "REMOVE_PAGE": return {
...state,
pages: state.pages.filter(item => item.id !== action.id)
}
case "CHOSE_PAGE": return {
...state,
idActive: action.id
}
case "RENAME_PAGE":
let indexPage = state.pages.findIndex(item => item.id === action.id);
state.pages[indexPage].title = action.title;
return {
...state
}
// ===================
// LONG LIST WHAT BAD...
// It's a bad idea to add editing to the `workspace` field and then `draggableElements`. `lines`
// ... but I understand that this will happen, because I don't know if there is another way.
default:
return state
}
}
Can I edit the `workspace' node without updating the entire application state?
Thanks you for any help.
For data modeling aspect for a 1-to-1 relationship, you can choose either to reference by id or to embed the data. It depends on your query pattern.
In your case which is embedding, you can make use of memoized selectors.
Ideally, since you have an idActive, update your pages data structure to be an object instead of a list.
Like so:
{
pages: {
'1': {
workspace: { ... },
}
}
}
Then for your reducer, think of it as slicing a tree (or nested attribute). Your reducer would then look something like:
function workspaceReducer(state, action) {
// TODO
}
function pagesReducer(state, action) {
switch (action.type) {
case 'UPDATE_WORKSPACE': {
const { id } = action;
const page = Object.assign({}, state.pages[id]);
return {
...state,
pages: {
...state.pages,
[id]: {
...page,
workspace: workspaceReducer(page.workspace, action)
}
}
}
}
}
}
Then to prevent unnecessary re-renders, using memoized selectors,
it would be like:
import { createSelector } from 'reselect';
const pages = state => state.pages;
const activePage = state => state.idActive;
const getActivePage = createSelector(
activePage,
pages,
(id, pages) => pages[id]
);
const getWorkspace = createSelector(
getActivePage,
page => page.workspace
);

ReactJS - Proper way for using immutability-helper in reducer

I have the following object which is my initial state in my reducer:
const INITIAL_STATE = {
campaign_dates: {
dt_start: '',
dt_end: '',
},
campaign_target: {
target_number: '',
gender: '',
age_level: {
age_start: '',
age_end: '',
},
interest_area: [],
geolocation: {},
},
campaign_products: {
survey: {
name: 'Survey',
id_product: 1,
quantity: 0,
price: 125.0,
surveys: {},
},
reward: {
name: 'Reward',
id_product: 2,
quantity: 0,
price: 125.0,
rewards: {},
},
},
}
And my reducer is listening for an action to add a reward to my object of rewards:
case ADD_REWARD:
return {
...state, campaign_products: {
...state.campaign_products,
reward: {
...state.campaign_products.reward,
rewards: {
...state.campaign_products.reward.rewards,
question: action.payload
}
}
}
}
So far so good (despite the fact that every object added is named "question")... its working but its quite messy. I've tried to replace the reducer above using the immutability-helper, to something like this but the newObh is being added to the root of my state
case ADD_REWARD:
const newObj = update(state.campaign_products.reward.rewards, { $merge: action.payload });
return { ...state, newObj }
return { ...state, newObj }
First, you must understand how the object shorthand works. If you're familiar with the syntax before ES2015, the above code translates to:
return Object.assign({}, state, {
newObj: newObj
});
Note how the newObj becomes a key and a value at the same time, which is probably not what you want.
I assume the mentioned immutability-helper is this library: https://www.npmjs.com/package/immutability-helper. Given the documentation, it returns a copy of the state with updated property based on the second argument.
You're using it on a deep property so that it will return a new value for that deep property. Therefore you still have to merge it in the state, so you have to keep the approach you've labelled as messy.
What you want instead is something like:
const nextState = update(state, {
$merge: {
campaign_products: {
reward: {
rewards: action.payload
}
}
}
});
return nextState;
Note how the first argument is the current state object, and $merge object is a whole object structure where you want to update the property. The return value of update is state with updated values based on the second argument, i.e. the next state.
Side note: Working with deep state structure is difficult, as you've discovered. I suggest you look into normalizing the state shape. If applicable, you can also split the reducers into sub-trees which are responsible only for the part of the state, so the state updates are smaller.

Vue watch array (or object) push same old and new values

I'm using Vuex and I've created a module called claims that looks like this:
import to from 'await-to-js'
import { functions } from '#/main'
import Vue from 'vue'
const GENERATE_TX_SUCCESS = 'GENERATE_TX_SUCCESS'
const GENERATE_TX_ERROR = 'GENERATE_TX_ERROR'
export default {
state: [ ],
mutations: {
[GENERATE_TX_SUCCESS] (state, generateTxData) {
state.push({ transaction: { ...generateTxData } })
},
[GENERATE_TX_ERROR] (state, generateTxError) {
state.push({ transaction: { ...generateTxError } })
}
},
actions: {
async generateTx ({ commit }, data) {
const [generateTxError, generateTxData] = await to(functions.httpsCallable('generateTx')(data))
if (generateTxError) {
commit(GENERATE_TX_ERROR, generateTxError)
} else {
commit(GENERATE_TX_SUCCESS, generateTxData)
}
}
},
getters: { }
}
Then, in the .vue component I do have this watch:
watch: {
claims: {
handler (newTxData, oldTxData) {
console.log(newTxData)
}
}
}
The problem I'm facing here is that newTxData is the same as oldTxData.
From my understanding, since this is a push and it detects the change, it's not one of these caveats: https://v2.vuejs.org/v2/guide/list.html#Caveats
So basically the problem is this one:
Note: when mutating (rather than replacing) an Object or an Array, the old value will be the same as new value because they reference the same Object/Array. Vue doesn’t keep a copy of the pre-mutate value.
https://v2.vuejs.org/v2/api/#vm-watch
Then my question is: how should I workaround this in the mutation?
Edit:
I tried also with Vue.set(state, state.length, generateTxData) but got the same behaviour.
Edit 2 - temporary solution - (bad performance-wise):
I'm adapting #matthew (thanks to #Jacob Goh) to my solution with vuexfire:
computed: {
...mapState({
claims: state => cloneDeep(state.claims)
})
},
This answer is based on this pretty smart answer by #matthew
You will need lodash cloneDeep function
Basically, create a computed value like this
computed: {
claimsForWatcher() {
return _.cloneDeep(this.claims);
}
}
What happens is, everytime a new value is pushed into claims, claimsForWatcher will become an entirely new object.
Therefore, when you watch claimsForWatcher, you won't have the problem where 'the old value will be the same as new value because they reference the same Object/Array' anymore.
watch: {
claimsForWatcher(oldValue, newValue) {
// now oldValue and newValue will be 2 entirely different objects
}
}
This works for ojects as well.
The performance hit is exaggerated for the real life use cases till you probably have hundreds (nested?) properties (objects) in your cloning object.
Either with clone or cloneDeep the clone takes 1/10 of a millisecond as reported with window.performance.now(). So measured under Node with process.hrtime() it could show even lower figures.
You can assign state to a new object:
mutations: {
[GENERATE_TX_SUCCESS] (state, generateTxData) {
state = [
...state,
{ transaction: { ...generateTxData } }
]
},
[GENERATE_TX_ERROR] (state, generateTxError) {
state = [
...state,
{ transaction: { ...generateTxError } }
]
}
}

Remove element from a nested array without using Immutability Helper

I have a state Object that looks like this:
//State Object
playlistDict: {
someId1: {
active: false,
id: 'someId',
name: 'someName',
pages: [ 'pageId1', 'pageId2', 'pageId3', etc ] // I want to remove an element from this array
},
someId2: {
active: false,
id: 'someId2',
name: 'someName2',
pages: [ 'pageId11', 'pageId22', 'pageId33', etc ] // I want to remove an element from this array
}
}
I'm trying to write a reducer that removes a page element given the index to remove without using immutability helper.
This is what I'm trying to do but my syntax is off and I'm not sure what's the correct way to write the reducer:
export function _removePageFromPlaylist( playlistId, pageIndex ) {
return { type: types.REMOVE_PAGE_FROM_PLAYLIST, playlistId, pageIndex };
}
case types.REMOVE_PAGE_FROM_PLAYLIST: {
let playlistCopy = Object.assign( {}, state.playlistDict[ action.playlistId ]);
playlistCopy.pages = Object.assign( {}, state.playlistDict[ action.playlistId ].pages );
playlistCopy.pages.splice( action.pageIndex, 1 );
return Object.assign({}, state, {
playlistDict: { ...state.playlistDict, playlistDict[ action.playlistId ]: playlistCopy } // doesn't like this syntax in particular
});
}
EDIT: In regards to people thinking it's the same as my other question, I'm asking this question because I'm trying to figure out if my reducer USING immutability helper is messing up my Firebase database, so I'm trying to figure out how to write the reducer WITHOUT using immutability helper so i can help eliminate what my bug is.
Use spread operator, and write it like this:
case types.REMOVE_PAGE_FROM_PLAYLIST: {
let playlistCopy = [...(state.playlistDict[ action.playlistId ].pages)];
playlistCopy.splice( action.pageIndex, 1 );
return {
...state,
playlistDict: {
...state.playlistDict,
[action.playlistId]: {
...state.playlistDict[action.playlistId],
pages: playlistCopy
}
}
}
}
Using Object Spread Operator:
case types.REMOVE_PAGE_FROM_PLAYLIST: {
return {
...state,
playlistDict: {
...state.playlistDict,
[action.playlistId]: {
...state.playlistDict[action.playlistId],
pages: [...state.playlistDict[action.playlistId].pages].splice(action.pageIndex, 1)
}
}
}
}

Resources