Update nested object within Redux store - javascript

I am trying to toggle a particular element in an array that's in an array that's in an array in redux state, and then have it reflect the change in the component that is using that state.
I am trying to change the complete state in set to true but nothing happens with the way my code is written right now.
export const initialState: WorkoutList = {
workouts: [
{
id: "newId",
name: "Leg Day",
date: new Date(),
duration: 60,
started: false,
completed: false,
exercises: [
{
id: "e1",
name: "Squats",
completed: false,
sets: [
{
id: "e1s1",
reps: 12,
weight: 60,
completed: false,
},
{
id: "e1s2",
reps: 8,
weight: 60,
completed: false,
},
{
id: "e1s3",
reps: 10,
weight: 60,
completed: false,
},
],
},
{
id: "e2",
name: "Leg Press",
completed: false,
sets: [
{
id: "e2s1",
reps: 12,
weight: 60,
completed: false,
},
{
id: "e2s2",
reps: 8,
weight: 60,
completed: false,
},
{
id: "e2s3",
reps: 10,
weight: 60,
completed: false,
},
],
},
],
},
],
selectedWorkout: {
id: "newId",
name: "Leg Day",
date: new Date(),
duration: 60,
started: false,
completed: false,
exercises: [
{
id: "e1",
name: "Squats",
completed: false,
sets: [
{
id: "e1s1",
reps: 12,
weight: 60,
completed: false,
},
{
id: "e1s2",
reps: 8,
weight: 60,
completed: false,
},
{
id: "e1s3",
reps: 10,
weight: 60,
completed: false,
},
],
},
{
id: "e2",
name: "Leg Press",
completed: false,
sets: [
{
id: "e2s1",
reps: 12,
weight: 60,
completed: false,
},
{
id: "e2s2",
reps: 8,
weight: 60,
completed: false,
},
{
id: "e2s3",
reps: 10,
weight: 60,
completed: false,
},
],
},
],
},
selectedExercise: {
id: "e1",
name: "Squats",
completed: false,
sets: [
{
id: "e1s1",
reps: 12,
weight: 60,
completed: false,
},
{
id: "e1s2",
reps: 8,
weight: 60,
completed: false,
},
{
id: "e1s3",
reps: 10,
weight: 60,
completed: false,
},
],
},
};
const workoutListReducer = (
state = initialState,
action: WorkoutActionTypes
): WorkoutList => {
switch (action.type) {
case TOGGLE_SET_COMPLETE:
const updatedWorkoutState = state.workouts.map((workout) => {
if (workout.id === state.selectedWorkout?.id) {
workout.exercises?.map((exercise) => {
if (exercise.id === state.selectedExercise?.id) {
exercise.sets?.map((set) => {
if (set.id === action.payload.id) {
// console.log(set, !set.completed);
return { ...set, completed: true };
}
return set;
});
}
return exercise;
});
}
return workout;
});
const updatedSelectedWorkoutState = updatedWorkoutState.find(
(workout) => workout.id === state.selectedWorkout?.id
);
const updatedSelectedExerciseState = updatedSelectedWorkoutState?.exercises?.find(
(workout) => workout.id === state.selectedExercise?.id
);
console.log(updatedSelectedExerciseState);
return {
...state,
workouts: updatedWorkoutState,
selectedWorkout: updatedSelectedWorkoutState,
selectedExercise: updatedSelectedExerciseState,
};
default:
return state;
}
};
export default workoutListReducer;

I believe the issue is that you're returning the same instances of workout objects. You should be cloning the "selected workout" object within the workouts so react can detect that something has been changed.
There are 2 usages of the map function that are not being properly applied. The map should be used to generate a new array that is returned by the map function. But in 2 cases you use the map ignoring its result.
Your code should be doing something like this (the condition has been inverted to make the problematic code clearer):
const updatedWorkoutState = state.workouts.map((workout) => {
// Happy path, returns same instance
if (workout.id !== state.selectedWorkout?.id) {
return workout;
}
// Obtain the updated exercises for the selected workout
const updatedExercises = workout.exercises?.map((exercise) => {
// just return the exercise we don't care about as-is:
if (exercise.id !== state.selectedExercise?.id) {
return exercise;
}
// get the updated array of sets
const updatedExerciseSets = exercise.sets?.map((set) => {
if (set.id === action.payload.id) {
// console.log(set, !set.completed);
return { ...set, completed: true };
}
return set;
});
// Clone the selected exercise setting the updated array of sets:
return {
...exercise,
sets: updatedExerciseSets
};
});
// Now the selected workout should be cloned and its exercises array replaced
// with the updated one:
return {
...workout,
exercises: updatedExercises
};
});
This makes things a bit more verbose, but there are a few libraries that you could use to help you with the immutability like Immer

Your code used the map function which returns a value, however, your code has no provision for this. Also, removed all the question marks(?). With just a little tweak to your code see a working option below
...
// The initialState part of your code goes here
...
const workoutListReducer = (
state = initialState,
action: WorkoutActionTypes
): WorkoutList => {
switch (action.type) {
case TOGGLE_SET_COMPLETE:
// create a copy of workout state
const tempWorkoutState = [...state.workouts];
const updatedWorkoutState = tempWorkoutState.map((workout) => {
if (workout.id === state.selectedWorkout?.id) {
// Update workout.exercises with the result of the map function
workout.exercises = workout.exercises.map((exercise) => {
if (exercise.id === state.selectedExercise.id) {
// Update exercise.sets with the result of the map function
exercise.sets = exercise.sets.map((set) => {
if (set.id === action.payload.id) {
// console.log(set, !set.completed);
return { ...set, completed: true };
}
return set;
});
}
return exercise;
});
}
return workout;
});
const updatedSelectedWorkoutState = updatedWorkoutState.find(
(workout) => workout.id === state.selectedWorkout.id
);
const updatedSelectedExerciseState = updatedSelectedWorkoutState.exercises.find(
(workout) => workout.id === state.selectedExercise.id
);
console.log(updatedSelectedExerciseState);
return {
...state,
workouts: updatedWorkoutState,
selectedWorkout: updatedSelectedWorkoutState,
selectedExercise: updatedSelectedExerciseState,
};
default:
return state;
}
};
export default workoutListReducer;

Related

Dealing with looping through nested arrays

I have data array, which has nested arrays inside (level1arr, leve21arr ...)
const data = [
{
level1arr: [
{
level2arr: [{ id: 1, isValid: true }, { id: 2, isValid: true }, { id: 3, isValid: true }],
},
{
level2arr: [{ id: 4, isValid: true }, { id: 5, isValid: true }, { id: 6, isValid: true }],
},
],
},
{
level1arr: [
{
level2arr: [{ id: 7, isValid: true }, { id: 8, isValid: true }, { id: 9, isValid: true }],
},
{
level2arr: [{ id: 10, isValid: true }, { id: 11, isValid: true }, { id: 12, isValid: true }],
},
],
},
];
I also have another array:
const invalidIds = [2,5]
I want to find elements with apecyfic id and change isValid property to false.
Is it better way than iteratinf over multiple nested arrays, like that:
data.forEach(lvl1 => {
lvl1.level1arr.forEach(lvl2 => {
lvl2.level2arr.forEach(element => {
// further nesting
});
});
})
Such iterating over multiple arrays is not good for performance. What is the best way to handle such case with nested arrays?
If it were nested arrays, you could use Array.prototype.flat(). However, you have a mix of nested objects and arrays. You will have to write a custom "flattener" for this data structure. Check this answer for details: how to convert this nested object into a flat object?
You can use recursion until you reach the level you need. Here's one way to do it.
const data = [{
level1arr: [{
level2arr: [{
id: 1,
isValid: true
}, {
id: 2,
isValid: true
}, {
id: 3,
isValid: true
}],
},
{
level2arr: [{
id: 4,
isValid: true
}, {
id: 5,
isValid: true
}, {
id: 6,
isValid: true
}],
},
],
},
{
level1arr: [{
level2arr: [{
id: 7,
isValid: true
}, {
id: 8,
isValid: true
}, {
id: 9,
isValid: true
}],
},
{
level2arr: [{
id: 10,
isValid: true
}, {
id: 11,
isValid: true
}, {
id: 12,
isValid: true
}],
},
],
},
];
const invalidIds =[2,5]
const findId = (object, key, value) => {
if (Array.isArray(object)) {
for (const obj of object) {
findId(obj, key, value);
}
} else {
if (object.hasOwnProperty(key) && object[key] === value) {
object.isValid = false;
return object
}
for (const k of Object.keys(object)) {
if (typeof object[k] === "object") {
findId(object[k], key, value);
}
}
}
}
invalidIds.forEach(id => findId(data, "id", id))
console.log(data)

Filter array of objects by another object's values

I would like to filter an array of objects based on the values in another object. I am trying to map() the array and filter() inside the function so that I am able to get a new array with only the desired (active) fields.
I am trying to get the filter on the Object.entries(), I try to compare the keys and check if the values of the active filters are true, but I am not getting it right.
const records = [
{
id: 1,
name: "first",
date: "05/02"
},
{
id: 2,
name: "second",
date: "06/02"
},
{
id: 3,
name: "third",
date: "07/02"
},
{
id: 4,
name: "fourth",
date: "08/02"
}
];
const active = {
id: true,
name: false,
date: true
};
const result = records.map((record) => {
return Object.entries(record).filter((entry) => {
Object.entries(active).forEach((activeEntry) => {
return activeEntry[1] && activeEntry[0] === entry[0];
});
});
});
console.log(result);
This is the desired outcome
const desiredOutcome = [
{
id: 1,
date: "05/02"
},
{
id: 2,
date: "06/02"
},
{
id: 3,
date: "07/02"
},
{
id: 4,
date: "08/02"
}
];
You can filter over the entries of the object and convert it back to an object with Object.fromEntries.
const records=[{id:1,name:"first",date:"05/02"},{id:2,name:"second",date:"06/02"},{id:3,name:"third",date:"07/02"},{id:4,name:"fourth",date:"08/02"}];
const active = {
id: true,
name: false,
date: true
};
const res = records.map(x =>
Object.fromEntries(
Object.entries(x).filter(([k])=>active[k])));
console.log(res);
Simply filter the keys by the key existing and then use Object.fromEntries to go back to an object
const records = [
{
id: 1,
name: "first",
date: "05/02"
},
{
id: 2,
name: "second",
date: "06/02"
},
{
id: 3,
name: "third",
date: "07/02"
},
{
id: 4,
name: "fourth",
date: "08/02"
}
];
const active = {
id: true,
name: false,
date: true
};
const result = records.map((record) => {
return Object.fromEntries( Object.entries(record).filter(([key,value]) => active[key]));
});
console.log(result);

Transform Nested Object Data Structure into an Array of Objects- JavaScript

I am having trouble with this problem taking an object and reformatting it to a new data structure. I need to take the beginning object and do the following: sort by group first, then label and exclude "active: false" records.
var beginning = {
Sister: {
1: { id: 1, name: 'Jesse Steven', active: false },
2: { id: 2, name: 'Zena Wong', active: true },
3: { id: 3, name: 'Katie Johnson', active: true },
},
Brother: {
10: { id: 10, name: 'Jeff Jacobs', active: true },
11: { id: 11, name: 'Mark Matha', active: false },
12: { id: 12, name: 'Kyle Ford', active: true },
},
Friend: {
20: { id: 20, name: 'Jim Dobbs', active: true },
}
};
After, it should looks like this:
var final = [
{ label: 'Jeff Jacobs', value: 10, group: 'Brother' },
{ label: 'Kyle Ford', value: 12, group: 'Brother' },
{ label: 'Jim Dobbs', value: 20, group: 'Friend' },
{ label: 'Katie Johnson', value: 3, group: 'Sister' },
{ label: 'Zena Wong', value: 2, group: 'Sister' }
];
Like this?
It's still missing a sort, but that can be easily remedied.
let beginning = {
Sister: {
1: { id: 1, name: 'Jesse Steven', active: false },
2: { id: 2, name: 'Zena Wong', active: true },
3: { id: 3, name: 'Katie Johnson', active: true },
},
Brother: {
10: { id: 10, name: 'Jeff Jacobs', active: true },
11: { id: 11, name: 'Mark Matha', active: false },
12: { id: 12, name: 'Kyle Ford', active: true },
},
Friend: {
20: { id: 20, name: 'Jim Dobbs', active: true },
}
};
let relations = Object.keys(beginning)
let final = relations.map(function(relation){
let num_keys = Object.keys(beginning[relation])
return num_keys.map(function(num_key){
beginning[relation][num_key]["group"] = relation
return beginning[relation][num_key]
})
})
.reduce(function(a, b){//flattens the returned array of arrays
return a.concat(b);
})
.filter(function(a){//filters out only active
return a["active"]
})
.map(function(a){//clean up some data
return {
label: a["name"],
value: a["id"],
group: a["group"]
}
})
console.log(final)
EDIT: Added sorting as the initial requirements asked for.
You can do this a number of ways, including for / in loops or some fancy stuff with ES2015, but a relatively simple functional example solution would be the following:
var activePeople = Object.keys(beginning).map(person => {
return Object.keys(beginning[person]).map(num => {
return (!!beginning[person][num].active) ? {
label: beginning[person][num].name,
value: beginning[person][num].id,
group: person
} : null
}).filter(i => !!i)
})
// flatten nested arrays
var final = [].concat.apply([], activePeople).sort((p1, p2) => {
if (p1.group < p2.group) {
return -1
} else if (p1.group > p2.group) {
return 1
}
if (p1.label < p2.label) {
return -1
}
return 1
})
I can propose faster one code:
"use strict";
let beginning = {
Sister: {
1: { id: 1, name: 'Jesse Steven', active: false },
2: { id: 2, name: 'Zena Wong', active: true },
3: { id: 3, name: 'Katie Johnson', active: true },
},
Brother: {
10: { id: 10, name: 'Jeff Jacobs', active: true },
11: { id: 11, name: 'Mark Matha', active: false },
12: { id: 12, name: 'Kyle Ford', active: true },
},
Friend: {
20: { id: 20, name: 'Jim Dobbs', active: true },
}
};
let groups = Object.keys(beginning).sort();
let final = [];
for (let i = 0, max = groups.length; i < max; i++) {
let keys = Object.keys(beginning[groups[i]]);
for (let j = 0, max2 = keys.length; j < max2; j++) {
let item = beginning[groups[i]][keys[j]];
if (item['active'] ) {
final.push({
label: item['name'],
value: keys[j],
group: groups[i]
});
}
}
}
console.log(final);

Javascript function- transform from one data structure to another

I'm trying to build a JS function to convert the data structure in the form of
'start' to the form of 'expected'.
Using JS map() method, how would I do this for the following associative array-
const start = {
Clients: {
171: { id: 171, name: 'John Smith', active: false },
172: { id: 172, name: 'Jacob Jacobson', active: true },
1441: { id: 1441, name: 'Eric Ericsson', active: true },
},
Caregivers: {
1: { id: 1, name: 'John Johnson', active: true },
37: { id: 37, name: 'James Jameson', active: false },
15: { id: 15, name: 'Aaron Aaronson', active: true },
},
Doctors: {
1147: { id: 1147, name: 'Doc Docson', active: true },
},
Hospitals: {
115: { id: 115, active: false, name: "St. Mary's" },
},
Applicants: {
17345: { id: 17345, name: 'Bob Bobson', active: true },
17346: { id: 17346, name: 'Jeff Jeffson', active: false },
17347: { id: 17347, name: 'Frank Frankson', active: true },
17348: { id: 17348, name: 'Bill Billson', active: true },
},
};
needs to be converted to-
const expected = [
{ label: 'Bill Billson', value: 17348, group: 'Applicants' },
{ label: 'Bob Bobson', value: 17345, group: 'Applicants' },
{ label: 'Frank Frankson', value: 17347, group: 'Applicants' },
{ label: 'Aaron Aaronson', value: 15, group: 'Caregivers' },
{ label: 'John Johnson', value: 1, group: 'Caregivers' },
{ label: 'Eric Ericsson', value: 1441, group: 'Clients' },
{ label: 'Jacob Jacobson', value: 172, group: 'Clients' },
{ label: 'Doc Docson', value: 1147, group: 'Doctors' },
];
.map() can't be used directly on Objects; instead you'll need to use Object.keys
const start = {
Clients: {
171: { id: 171, name: 'John Smith', active: false },
172: { id: 172, name: 'Jacob Jacobson', active: true },
1441: { id: 1441, name: 'Eric Ericsson', active: true }
},
Caregivers: {
1: { id: 1, name: 'John Johnson', active: true },
37: { id: 37, name: 'James Jameson', active: false },
15: { id: 15, name: 'Aaron Aaronson', active: true }
},
Doctors: {
1147: { id: 1147, name: 'Doc Docson', active: true }
},
Hospitals: {
115: { id: 115, active: false, name: "St. Mary's" }
},
Applicants: {
17345: { id: 17345, name: 'Bob Bobson', active: true },
17346: { id: 17346, name: 'Jeff Jeffson', active: false },
17347: { id: 17347, name: 'Frank Frankson', active: true },
17348: { id: 17348, name: 'Bill Billson', active: true }
}
};
// Get an array of properties in 'start'
// then use Array.reduce() to loop over each item
const expected = Object.keys(start).reduce( (res, gKey) => {
// gKey = 'group' name
// gVal = 'group' value
let gVal = start[gKey];
// loop over each item in the 'group'
Object.keys(gVal).forEach(iKey => {
// iKey = 'group.item' name
// iVal = 'group.item' value
let iVal = gVal[iKey];
// if the value's .active property is truthy
if (iVal.active) {
// format the result as desired and add it to the result array
res.push({
label: iVal.name,
value: iKey,
group: gKey
});
}
});
// return the result array
return res;
// start the .reduce() with an empty array
}, []);
console.log(expected);
To loop over an object, you can either use a for ... in loop, or use Object.keys to get an array of keys. For ... in will include inherited properties, so you may need to manually filter them out. Object.keys only returns own properties, so there's no need to do the filtering (but it also isn't appropriate if you need inherited properties)
Example with for ... in:
for (var prop in start) {
if (start.hasOwnProperty(prop)) {
// logs out 'Clients', then 'Caregivers', then 'Doctors', then 'Hospitals', then 'Applicants'
console.log(prop);
}
}
Example with Object.keys:
//produces array ['Clients', 'Caregivers', 'Doctors', 'Hospitals', 'Applicants']
var keys = Object.keys(start);
So if you wanted to use .map, you can start with this, and fill it in to do whatever you desire:
Object.keys(start)
.map(key => {
//do something with start[key]
//perhaps you could get Object.keys(start[key]) and loop over that as well.
});
My solution without 'forEach':
function transform(data) {
Object.entries(data).map(item => Object.values(item[1])
.map(i => i.group = item[0]))
.reduce(( acc, cur ) => acc.concat(cur), [])
.filter(item => item.active === true)
.sort((a, b) => a.group - b.group)
.map(item => {
let expected = {};
expected.label = item.name;
expected.value = item.id;
expected.group = item.group;
});
return expected;
}

Chai-related error message: "AssertionError: expected undefined to deeply equal"

I wrote a function that when given a list of objects and an
id, returns the same list, but with the corresponding object marked active
(all other objects should not be active).
const list = [
{ id: 1, active: false },
{ id: 2, active: false },
{ id: 3, active: true },
{ id: 4, active: false }
];
function markActive(list, value) {
list.forEach((id) => {
if (id.active = (id.id === value)) {
return true;
} else {
return false;
}
});
}
markActive(list, 2);
console.log(list)
Returns:
[ { id: 1, active: false },
{ id: 2, active: false },
{ id: 3, active: false },
{ id: 4, active: true } ]
It's working like a charm, except when I run "npm run [filename]" I get an error message:
Running Tests for [filename].
------------
[ { id: 1, active: false },
{ id: 2, active: false },
{ id: 3, active: false },
{ id: 4, active: true } ]
markActive
1) Case 1 (Given Sample)
2) Case 2 (String IDs)
0 passing (16ms)
2 failing
1) markActive Case 1 (Given Sample):
AssertionError: expected undefined to deeply equal [ { id: 1,
active: false },
{ id: 2, active: true },
{ id: 3, active: false },
{ id: 4, active: false } ]
at Function.assert.deepEqual
(node_modules/chai/lib/chai/interface/assert.js:216:32)
at Context.it (tests/test_02.js:23:12)
2) markActive Case 2 (String IDs):
AssertionError: expected undefined to deeply equal [ { id: '1',
active: false },
{ id: '2', active: true },
{ id: '3', active: false },
{ id: '4', active: false } ]
at Function.assert.deepEqual
(node_modules/chai/lib/chai/interface/assert.js:216:32)
at Context.it (tests/test_02.js:40:12)
Any idea what I'm doing wrong? Here's the code that sets up the tests:
const chai = require("chai");
const sinon = require("sinon");
const assert = chai.assert;
const markActive = require("../answers/02.js");
describe("markActive", () => {
it("Case 1 (Given Sample)", () => {
var list = [
{ id: 1, active: false },
{ id: 2, active: false },
{ id: 3, active: true },
{ id: 4, active: false }
];
var newList = markActive(list, 2);
var targetList = [
{ id: 1, active: false },
{ id: 2, active: true },
{ id: 3, active: false },
{ id: 4, active: false }
];
assert.deepEqual(newList, targetList);
});
it("Case 2 (String IDs)", () => {
var list = [
{ id: "1", active: false },
{ id: "2", active: false },
{ id: "3", active: true },
{ id: "4", active: false }
];
var newList = markActive(list, "2");
var targetList = [
{ id: "1", active: false },
{ id: "2", active: true },
{ id: "3", active: false },
{ id: "4", active: false }
];
assert.deepEqual(newList, targetList);
});
});
Your function isn't returning anything, so any variables you try to set to the result will be set as undefined.
To fix this, simply add a return statement to the end of your function.
function markActive(list, value) {
list.forEach((id) => {
if (id.active = (id.id === value)) {
return true;
} else {
return false;
}
});
return list; // return the updated list
}
NOTE: It's worth mentioning that because the array is referenced, you're modifying the values in-place. This is why the array you defined outside the function still had updated results even though you weren't logging the returned value. This can have unintended side effects if you were to run the markActive() function several times on the same list. If you want a new list to be returned, look into ways of copying and deep copying arrays in Javascript.

Categories

Resources