Spread omit doesn't omit property inside action handler - javascript

I feel i'm going crazy, the following code just doesn't work when used in reducer, however running it with the exact same variables in the console or playground works absolutely perfect.
[MutationTypes.DELETE_GOALS_SUCCESS]: (state, { payload }) => {
//payload is {deleted_goals: [1, 2, 3]}, goals is {1: {...}, 2: {...}, ... n: {...}}
const goals = { ...state.goals };
const newGoals = payload.deleted_goals.reduce((acc, id) => {
const { [id]: omitted, ...newAcc } = acc; //newAcc still contains "id" key
console.log(
"After spread",
"New goals:",
newAcc,
"Old goals:",
acc, //acc and newAcc are the same aside from different pointers
"Removed goal",
omitted,
);
return newAcc;
}, goals);
return {
...state,
goals: newGoals,
};
},
The const { [id]: omitted, ...newAcc } = acc; part is what just doesn't work as intended. newAcc for some reason still keeps containing id key, so it remains unchanged every iteration. The id key is included in goals object, i can log omitted object.
As i said i can run the exact same line of code anywhere else with the exact same variables and it will work perfectly. This might be something with redux or my implementation of reducer, however i just cannot imagine what can be wrong and how it can cause such consequences. State is just plain object, state.goal is also just plain object, i'm even making shallow copy of it. I can JSON.stringify them, copy paste somewhere else and then omit the same way i do here and it will work.
Any idea what might cause this strange interaction? There are multiple workarounds to do this without spread, like using delete operator, or constructing new object from scratch but i want to know why the hell can object become "immune" to spread destructuring.
I've tried to omit with spread on fresh object inside both action handler and reduce callback and it worked, looks like there is something with this particular object ( state.goals ). However it is just map like object structured like that: {id1: {goalwithid}, id2:{goalwithid2} ...} id1, id2 etc are numbers.
Just tried deepcloning object (replaced const goals = { ...state.goals }; with const goals = _.cloneDeep(state.goals) and it doesn't change anything.

Why not just do this:
[MutationTypes.DELETE_GOALS_SUCCESS]: (state, { payload }) => {
//payload is {deleted_goals: [1, 2, 3]}, goals is {1: {...}, 2: {...}, ... n: {...}}
const goals = state.goals;
const newGoals = Object.keys(goals).reduce((acc, key) => {
if(payload.deleted_goals.includes(parseInt(key))) {
return acc;
} else {
return (acc[key] = goals[key], acc);
}
}, {})
return {
...state,
goals: newGoals,
};
},

Related

How can I store items by id in Redux Store

I am using Redux in my React application. However items are always stored by index, like that =>
I want to store them by ids, like instead of 0 first item's index should be 41. How can I do that?
reducer.js
export function ratedPosts(state=[], action) {
enableES5()
return (
produce(state, draft => {
const rate = action.Rate
switch (action.type) {
case RATE_POST:
draft.unshift({postId: action.postId, rate: rate})
break
case RATE_POST_UPDATE:
draft.map(post => post.postId === action.postId).rate = rate
break
default:
return draft
}
})
)
}
You can't do that with arrays, but you can do that with objects. I see also that you are using Array.unshift to add new posts, keep in mind that arrays do not guarantee the sequence of the items, even though it works most of the time.
You'll need to convert your data structure to use objects instead of array, but in the getter function you could convert to an array so it can be more easily used in the frontend.
You can set an object ID programmatically using [ ]
let myObject = {}
const idOne = 'abc'
const idTwo = 'def'
draft[idOne] = "Hello" // draft.abc === "Hello"
draft[idTwo] = "World" // draft.def === "World"
draft === {
abc: "Hello",
def: "World"
}

Is there a way to traverse a possibly-self-containing object in JavaScript?

I want to descend an object in Javascript looking for a specific string. Unfortunately, this object is built in such a way that it'd be impossible to simply use the source and Ctrl-F for that string, and it's also built in such a way that recursive functions trying to descend it risk getting trapped inside of it forever.
Basically, this object contains itself. Not just once, but in very many areas. I cannot simply say "exclude these keys", as the object is obfuscated and therefore we'd be here all day listing keys, and once we were done we wouldn't have looked at all the data.
As well, I need to be able to descend __proto__ and prototype, as useful strings are hidden in there too. (But only for functions and objects.)
While I'd prefer something along the lines of findStuff(object, /string/ig), that may be hard, so any function that simply has areas clearly marked that the control flow falls to once it's found specific objects (function, string, etc.)
Thank you, and sorry for such a pain in the butt question.
Edit: In case it helps, I'm trying to traverse a compiled Construct2 runtime object. I'm not going to post the full thing here as it's not going to fit in any pastebin no matter how forgiving, and also I don't want to accidentally post resources I don't have the permission to provide. (Don't worry though, I'm not trying to pirate it myself, I'm simply trying to figure out some user-facing functionality)
You could use a WeakSet to keep track of the objects that were already traversed:
function traverseOnce(obj, cb) {
const visited = new WeakSet();
(function traverse(obj) {
for(const [key, value] of Object.entries(obj)) {
if(typeof value === "object" && value !== null) {
if(visited.has(value)) continue;
visited.add(value);
cb(value);
traverse(value);
}
}
})(obj);
}
Through the WeakSet you got O(1) lookup time, and are also sure that this will never leak.
Usable as:
const nested = { other: { a: 1 } };
nested.self = nested;
traverseOnce(nested, console.log);
// nested: { other, self }
// other: { a: 1 }
You could also use a Symbol to flag traversed objects, for that replace new WeakSet() with Symbol(), visited.has(value) with value[visited] and visuted.add(value) with value[visited] = true;
Any time you're traversing a potentially cyclical object, keeping a memo of already traversed objects and breaking if you've seen the current object before is a standard technique. You can use Set to do so.
Keep a list of objects you have recursed into, and then check each new object against that list.
const data = {
foo: {
bar: 1
},
one: 1,
jaz: {
hello: {
x: 1
}
}
};
data.bar = data.foo;
data.foo.foo = data.foo;
data.jaz.hello.foo = data;
function search_for_1() {
const seen = [];
search(data);
function search(object) {
Object.values(object).forEach(value => {
if (typeof value === "object") {
if (seen.includes(value)) {
console.log("Seen this already");
} else {
seen.push(value);
search(value);
}
} else {
if (value === 1) {
console.log("Found 1");
}
}
});
}
}
search_for_1();
Don't reinvent the wheel There are libraries for this kind of stuff.
We use object-scan for all our data processing. It's very powerful once you wrap your head around it. Here is how it would work for your questions
// const objectScan = require('object-scan');
const traverse = (data) => objectScan(['**'], {
filterFn: ({ key, value, parent }) => {
// do something here
},
breakFn: ({ isCircular }) => isCircular === true
})(data);
const circular = { name: 'Max', age: 5, sex: undefined, details: { color: 'black', breed: undefined } };
circular.sex = circular;
circular.details.breed = circular;
console.log(traverse(circular));
/* =>
[ [ 'details', 'breed' ],
[ 'details', 'color' ],
[ 'details' ],
[ 'sex' ],
[ 'age' ],
[ 'name' ] ]
*/
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan

React Redux - Reducer with CRUD - best practices?

I wrote simple reducer for User entity, and now I want to apply best practices for it, when switching action types and returning state. Just to mention, I extracted actions types in separate file, actionsTypes.js.
Content of actionsTypes.js :
export const GET_USERS_SUCCESS = 'GET_USERS_SUCCESS';
export const GET_USER_SUCCESS = 'GET_USER_SUCCESS';
export const ADD_USER_SUCCESS = 'ADD_USER_SUCCESS';
export const EDIT_USER_SUCCESS = 'EDIT_USER_SUCCESS';
export const DELETE_USER_SUCCESS = 'DELETE_USER_SUCCESS';
First question, is it mandatory to have actions types for FAILED case? For example, to add GET_USERS_FAILED and so on and handle them inside usersReducer?
Root reducer is:
const rootReducer = combineReducers({
users
});
There is code of usersReducer, and I put comments/questions inside code, and ask for answers (what are best practices to handle action types):
export default function usersReducer(state = initialState.users, action) {
switch (action.type) {
case actionsTypes.GET_USERS_SUCCESS:
// state of usersReducer is 'users' array, so I just return action.payload where it is array of users. Will it automatically update users array on initial state?
return action.payload;
case actionsTypes.GET_USER_SUCCESS:
// What to return here? Just action.payload where it is just single user object?
return ;
case actionsTypes.ADD_USER_SUCCESS:
// what does this mean? Can someone explain this code? It returns new array, but what about spread operator, and object.assign?
return [...state.filter(user => user.id !== action.payload.id),
Object.assign({}, action.payload)];
case actionsTypes.EDIT_USER_SUCCESS:
// is this ok?
const indexOfUser = state.findIndex(user => user.id === action.payload.id);
let newState = [...state];
newState[indexOfUser] = action.payload;
return newState;
case actionsTypes.DELETE_USER_SUCCESS:
// I'm not sure about this delete part, is this ok or there is best practice to return state without deleted user?
return [...state.filter(user => user.id !== action.user.id)];
default:
return state;
}
}
I'm not an experienced developer but let me answer your questions what I've learned and encountered up to now.
First question, is it mandatory to have actions types for FAILED case?
For example, to add GET_USERS_FAILED and so on and handle them inside
usersReducer?
This is not mandatory but if you intend to give a feedback to your clients it would be good. For example, you initiated the GET_USERS process and it failed somehow. Nothing happens on client side, nothing updated etc. So, your client does not know it failed and wonders why nothing happened. But, if you have a failure case and you catch the error, you can inform your client that there was an error.
To do this, you can consume GET_USERS_FAILED action type in two pleases for example. One in your userReducers and one for, lets say, an error or feedback reducer. First one returns state since your process failed and you can't get the desired data, hence does not want to mutate the state anyhow. Second one updates your feedback reducer and can change a state, lets say error and you catch this state in your component and if error state is true you show a nice message to your client.
state of usersReducer is 'users' array, so I just return
action.payload where it is array of users. Will it automatically
update users array on initial state?
case actionsTypes.GET_USERS_SUCCESS:
return action.payload;
This is ok if you are fetching whole users with a single request. This means your action.payload which is an array becomes your state. But, if you don't want to fetch all the users with a single request, like pagination, this would be not enough. You need to concat your state with the fetched ones.
case actionsTypes.GET_USERS_SUCCESS:
return [...state, ...action.payload];
Here, we are using spread syntax.
It, obviously, spread what is given to it :) You can use it in a multiple ways for arrays and also objects. You can check the documentation. But here is some simple examples.
const arr = [ 1, 2, 3 ];
const newArr = [ ...arr, 4 ];
// newArr is now [ 1, 2, 3, 4 ]
We spread arr in a new array and add 4 to it.
const obj = { id: 1, name: "foo, age: 25 };
const newObj = { ...obj, age: 30 };
// newObj is now { id: 1, name: "foo", age: 30 }
Here, we spread our obj in a new object and changed its age property. In both examples, we never mutate our original data.
What to return here? Just action.payload where it is just single user
object?
case actionsTypes.GET_USER_SUCCESS:
return ;
Probably you can't use this action in this reducer directly. Because your state here holds your users as an array. What do you want to do the user you got somehow? Lets say you want to hold a "selected" user. Either you can create a separate reducer for that or change your state here, make it an object and hold a selectedUser property and update it with this. But if you change your state's shape, all the other reducer parts need to be changed since your state will be something like this:
{
users: [],
selectedUser,
}
Now, your state is not an array anymore, it is an object. All your code must be changed according to that.
what does this mean? Can someone explain this code? It returns new
array, but what about spread operator, and object.assign?
case actionsTypes.ADD_USER_SUCCESS:
return [...state.filter(user => user.id !== action.payload.id), Object.assign({}, action.payload)];
I've already tried to explain spread syntax. Object.assign copies some values to a target or updates it or merges two of them. What does this code do?
First it takes your state, filters it and returns the users not equal to your action.payload one, which is the user is being added. This returns an array, so it spreads it and merges it with the Object.assign part. In Object.assign part it takes an empty object and merges it with the user. An all those values creates a new array which is your new state. Let's say your state is like:
[
{ id: 1, name: "foo" },
{ id: 2, name: "bar" },
]
and your new user is:
{
id: 3, name: "baz"
}
Here what this code does. First it filters all the user and since filter criteria does not match it returns all your users (state) then spread it (don't forget, filter returns an array and we spread this array into another one):
[ { id: 1, name: "foo"}, { id: 2, name: "bar" } ]
Now the Object.assign part does its job and merges an empty object with action.payload, a user object. Now our final array will be like this:
[ { id: 1, name: "foo"}, { id: 2, name: "bar" }, { id: 3, name: "baz" } ]
But, actually Object.assign is not needed here. Why do we bother merging our object with an empty one again? So, this code does the same job:
case actionsTypes.ADD_USER_SUCCESS:
return [...state.filter(user => user.id !== action.payload.id), action.payload ];
is this ok?
case actionsTypes.EDIT_USER_SUCCESS:
const indexOfUser = state.findIndex(user => user.id === action.payload.id);
let newState = [...state];
newState[indexOfUser] = action.payload;
return newState;
It seems ok to me. You don't mutate the state directly, use spread syntax to create a new one, update the related part and finally set your state with this new one.
I'm not sure about this delete part, is this ok or there is best
practice to return state without deleted user?
case actionsTypes.DELETE_USER_SUCCESS:
return [...state.filter(user => user.id !== action.user.id)];
Again, it seems ok to me. You filter the deleted user and update your state according to that. Of course there are other situations you should take into considiration . For example do you have a backend process for those? Do you add or delete users to a database? If yes for all the parts you need to sure about the backend process success and after that you need to update your state. But this is a different topic I guess.

How does spread operator work in an array vs. obj?

I'm learning Redux from this tutorial and I don't get how the spread operator below works in both the object and array. If ...state returns the same thing, how can it work in both situations? I thought it will just return an array, so it will work inside the SHUTTER_VIDEO_SUCCESS because it'll just spread whatever is inside the state into the new array in addition to the action.videos, but how will this work inside the SELECTED_VIDEO case? There is no key to place it in. The spread operator grabs the array not the key value pair from the default initialState right?
initialState.js
export default {
images: [],
videos: []
};
someComponent.js
import initialState from './initialState';
import * as types from 'constants/actionTypes';
export default function ( state = initialState.videos, action ) {
switch (action.type) {
case types.SELECTED_VIDEO:
return { ...state, selectedVideo: action.video }
case types.SHUTTER_VIDEO_SUCCESS:
return [...state, action.videos];
default:
return state;
}
}
UPDATE
Spread syntax allows you to spread an array into an object (arrays are technically objects, as is mostly everything in js). When you spread an array into an object, it will add a key: value pair to the object for each array item, where the key is the index and the value is the value stored at that index in the array. For example:
const arr = [1,2,3,4,5]
const obj = { ...arr } // { 0: 1, 1: 2, 2: 3, 3: 4, 4: 5 }
const arr2 = [{ name: 'x' }, { name: 'y' }]
const obj2 = { ...arr2 } // { 0: { name: 'x' }, 1: { name: 'y' } }
You can also spread strings into arrays and objects as well. For arrays, it will behave similarly as String.prototype.split:
const txt = 'abcdefg'
const arr = [...txt] // ['a','b','c','d','e','f', 'g']
For objects, it will split the string by character and assign keys by index:
const obj = { ...txt } // { 0:'a',1:'b',2:'c',3:'d',4:'e',5:'f',6:'g' }
So you may be getting data that sort of works when you spread an array into an object. However, if the example you gave is what you're actually using, you're going to run into problems. See below.
=============
In the case of reducers in redux, when you use the spread syntax with an array it spreads each item from your array into a new array. It's basically the same as using concat:
const arr = [1,2,3]
const arr2 = [4,5,6]
const arr3 = [...arr, ...arr2] // [1,2,3,4,5,6]
// same as arr.concat(arr2)
With an object, the spread syntax spreads key: value pairs from one object into another:
const obj = { a: 1, b: 2, c: 3 }
const newObj = { ...obj, x: 4, y: 5, z: 6 }
// { a: 1, b: 2, c: 3, x: 4, y: 5, z: 6 }
These are two ways to help keep your data immutable in your reducers. The spread syntax copies array items or object keys/values rather than referencing them. If you do any changes in nested objects or objects in arrays, you'll have to take that into account to make sure you get new copies instead of mutated data.
If you have arrays as object keys then you can spread the entire object into a new one and then override individual keys as needed, including keys that are arrays that need updating with spread syntax. For example, an update to your example code:
const initialState = {
images: [],
videos: [],
selectedVideo: ''
}
// you need all of your initialState here, not just one of the keys
export default function ( state = initialState, action ) {
switch (action.type) {
case types.SELECTED_VIDEO:
// spread all the existing data into your new state, replacing only the selectedVideo key
return {
...state,
selectedVideo: action.video
}
case types.SHUTTER_VIDEO_SUCCESS:
// spread current state into new state, replacing videos with the current state videos and the action videos
return {
...state,
videos: [...state.videos, ...action.videos]
}
default:
return state;
}
}
This shows updating a state object and specific keys of that object that are arrays.
In the example you give, you're changing the structure of your state on the fly. It starts as an array, then sometimes returns an array (when SHUTTER_VIDEO_SUCCESS) and sometimes returns an object (when SELECTED_VIDEO). If you want to have a single reducer function, you would not isolate your initialState to just the videos array. You would need to manage all of your state tree manually as shown above. But your reducer should probably not switch the type of data it's sending back depending on an action. That would be an unpredictable mess.
If you want to break each key into a separate reducer, you would have 3 (images, videos and selectedVideo) and use combineReducers to create your state object.
import { combineReducers } from 'redux'
// import your separate reducer functions
export default combineReucers({
images,
videos,
selectedVideos
})
In that case each reducer will be run whenever you dispatch an action to generate the complete state object. But each reducer will only deal with its specific key, not the whole state object. So you would only need array update logic for keys that are arrays, etc.
According to the tutorial:
create-react-app comes preinstalled with babel-plugin-transform-object-rest-spread that lets you use the spread (…) operator to copy enumerable properties from one object to another in a succinct way. For context, { …state, videos: action.videos } evaluates to Object.assign({}, state, action.videos).
So, that's not a feature of ES6. It uses a plugin to let you use that feature.
Link: https://babeljs.io/docs/plugins/transform-object-rest-spread/
An array is also a key/value-pair but the key is an index. It's using ES6 destructuring and the spread syntax.
Redux docs on the subject
You may also want to read up on ES6 property value shorthand (or whatever it is called):
ES6 Object Literal in Depth
Whenever you find yourself assigning a property value that matches a property name, you can omit the property value, it’s implicit in ES6.

"Updating" all state in redux app properly

I've met some trouble assigning a new object in the reducer of my app. My state contains 2 arrays :
{
elements: [],
constraints: []
}
Those elements are handled by 2 reducers :
elementsReducer
constraintsReducer
and combined like this:
let reducer = combineReducers({
elements: elementsReducer,
constraints: constraintsReducer
});
export default reducer
So, basically, an action is triggered, and my reducer is supposed to update all the state.elements array. I've tried several things and I can't update the whole elements array, only - in the best case - the first element.
My first idea was to do:
return Object.assign({}, state, {
elements: state.map((e) => {
return Object.assign({}, e, {
text: action.data[e.id][e.text]
})
})
});
action.data is an array containing a different text for each element. Basically, all I was to do is, on a special action, updating all the element array. But this syntax does not work as it creates a new array INSIDE the array "elements" of the store. It does not replace it. If I let this, the store becomes:
{
elements: [
elements: [...]
],
constraints: [...]
}
When I access the state in my reducer elementsReducer, it's only the "element" array and not the full state. After this issue, I've tried to do the following:
return state.map(function(e) {
return assign({}, e, {
text: action.data[e.id][e.text]
});
});
Now, I worked, but the ONLY element mapped is the first one. The other elements are simply not updating.
Do you have any idea to solve the issue?
Thanks everyone :)
Xelys
EDIT :
// code of elementsReducer
var assign = require('object-assign');
export default function elementsReducer(state = {}, action) {
switch (action.type) {
case 'ADD_ELEMENT':
return [...state,
{
name: action.name,
id: action.id,
committed: false,
text: action.text
}
]
case 'COMMIT_ELEMENT':
console.log('commit action')
return state.map(function(e) {
return e.id === action.id ?
assign({}, e, {committed: true}) :
e
});
case 'SAVE_DATA':
return state.map((e) => {
return Object.assign({}, e, {
text: action.data[e.id][e.text]
});
});
default:
return state;
}
}
Based on your code, I assumed your data structure is like below:
// state.element
stateElement = [
{ id:1, text: '1t' },
{ id:2, text: '2t' }
];
// Your action result
action = {
data: {
1: { text: 'new 1t' },
2: { text: 'new 2t' }
}
}
// Your new state.element
newData = data.map(function(e) {
return Object.assign({}, e, {
text: action.data[e.id].text
});
});
thanks for the answers.
#Ali Sepehri.Kh, yeah, my data structure is very similar. Actually, it's a little bit more complex, but I've simplified it to be more understable.
However, I figured out to solve the issue. I feel quite ashamed of creating a post on stackoverflow, because the error had nothing to do with redux.
The mapping function I've used was totally working. The issue was located is the action data. The function which created the action.data array was returning an array empty after the first element. I thought the issue was coming from the map() as it was for me the "most difficult" part of the fonction.
However, I've made a stupid mistake on the fonction creating the array of the action. I have misplaced a "return" inside a for loop, which explain that all the elements after the first one were empty.
Sorry for your time guys, and thanks for the help :).

Categories

Resources