Remove element from a nested array without using Immutability Helper - reactjs

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)
}
}
}
}

Related

Update deeply nested state object in redux without spread operator

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; },
...}

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 } }
]
}
}

Using object rest to delete nested object

I have a react app with some redux state that looks like:
{
shape1: {
constraints: {
constraint1: {
key: value
},
constraint2: {
key: value
}
}
},
shape2: {
constraints: {
constraint1: {
key: value
},
constraint2: {
key: value
}
}
}
}
I dispatch an action and want to delete one of the constraint objects, ie. constraint1 for shape1. Here is what my reducer looks like for this action, say I'm trying to delete constraint1 from shape1:
case DELETE_CONSTRAINT:
shape = action.payload; // ie. shape1, the parent of the constraint I
// am trying to delete
let {
[shape]: {'constraints':
{'constraint1': deletedItem}
}, ...newState
} = state;
return newState;
This removes the entire shape1 object from the state instead of just the individual constraint1 object. Where I am going wrong/ what is the best approach for doing this? I'd prefer to use object rest in order to be consistent with the rest of my code.
Thanks.
When using the rest syntax in destructuring to get a slice of the object, you'll get everything else on the same "level".
let {
[shape]: {'constraints':
{'constraint1': deletedItem}
}, ...newState
} = state;
In this case newState takes everything else is everything but [shape].
Since your state has multiple nesting levels, you'll have to extract the new constraints using destructuring and rest syntax, and then create a new state.
const state = {
shape1: {
constraints: {
constraint1: {
key: 'value'
},
constraint2: {
key: 'value'
}
}
},
shape2: {
constraints: {
constraint1: {
key: 'value'
},
constraint2: {
key: 'value'
}
}
}
};
const shape = 'shape1';
const constraint = 'constraint1';
// extract constraints
const {
[shape]: {
constraints: {
[constraint]: remove,
...constraints
}
}
} = state;
// create the next state
const newState = {
...state,
[shape]: {
...state[shape], // if shape contains only constraints, you keep skip this
constraints
}
}
console.log(newState);
In short, no not with an object - not with the spread operator.
You can do it other ways without mutating your state though, such as a filter, for example:
return state.filter((element, key) => key !== action.payload);
Consistency sidenote
As a sidenote - there is a vast difference between consistency in approach and style vs consistency of actual code. Don't feel the need to shoe horn something for consistency if it makes more logical sense to do it a different way. If it truely breaks the consistency of the application that other developers are working on, document why it's different.

Resources