I have a scenario
{
data:'',
skus: [
{ id: 1, ......}
{ id: 2, ......}
{ id: 3, ......}
]
api_first:'',
}
I have that schema and want to setState in somewhere skus on selected sku item and return changed item to original array
this.setState(produce(prevstate =>
prevstate.data.sku.obj="change"
))
this works for me
I'd recommend to use functional setState and map:
const updateSku = (skuId, data) => {
this.setState(prevState => ({
...prevState,
skus: prevState.skus.map(sku => {
if (sku.id === skuId) {
return {...sku, ...data}
} // else
return sku
})
}))
}
State immutability is important sometimes devs mutate states those are complex with multiple nested levels. You can always update state with simple javascript object update stratigy but I would suggest you to use immerjs. It reduces the code and makes it much more cleaner and easy to understand what is going to change. It helps a lot in redux reducers where a complex state needs to be updated with mutation
Here is example
https://immerjs.github.io/immer/docs/example-setstate
/**
* Classic React.setState with a deep merge
*/
onBirthDayClick1 = () => {
this.setState(prevState => ({
user: {
...prevState.user,
age: prevState.user.age + 1
}
}))
}
/**
* ...But, since setState accepts functions,
* we can just create a curried producer and further simplify!
*/
onBirthDayClick2 = () => {
this.setState(
produce(draft => {
draft.user.age += 1
})
)
}
Using immerjs, it will be
const updateSku = (skuId, data) => {
this.setState(produce(draft => {
const sku = draft.skus.find(s => s.id === skusId);
Object.assign(sku, data);
}));
}
What I have understood from your explanation is that when the SKU item gets changed you want to update the state Skus.
Here I've provided a solution for the same please try to relate with your example.
let's assume you have the following react component.
import React, { Component } from "react";
export class Sku extends Component {
constructor(props) {
super(props);
this.state = {
data: "",
skus: [
{ key: "key1", value: "value1" },
{ key: "key2", value: "value2" },
{ key: "key3", value: "value3" },
],
APIFirst: "",
};
}
handleSkuChange = (data) => {
this.setState(({ skus }) => {
const newSkus = skus.map(sku => (sku.key === data.key ? { ...sku, ...data } : sku));
return { skus: newSkus };
});
};
render() {
const { data, skus, APIFirst } = this.state;
const newSku = { key: 'key2', value: 'newSku' };
console.log("states =>", data, skus, APIFirst);
return (
<button type="button" onClick={() => this.handleSkuChange(newSku)}>'Change sku'</button>
);
}
}
The handleSkuChange function will work like it,
const skus = [
{ key: "key1", value: "value1" },
{ key: "key2", value: "value2" },
{ key: "key3", value: "value3" },
];
const handleSkuChange = (data) => (
skus.map(sku => (sku.key === data.key) ? { ...sku, value: "newValue" } : sku));
const newSku = { key: 'key2', value: 'newSku' };
console.log('old skus', skus);
console.log('new skus', handleSkuChange(newSku));
Related
I have an observable that I'd like to modify before it resolves, either using a map pipe or something similar to ensure that all ids within the groups array are unique. If cats is encountered twice, the second occurrence should become cats-1, cats-2 etc. These fields are being used to populate a HTML id attribute so I need to ensure they are always unique.
{
title: 'MyTitle',
description: 'MyDescription',
groups: [
{
id: 'cats',
title: 'SomeTitle'
},
{
id: 'dogs',
title: 'SomeTitle'
},
{
id: 'octupus',
title: 'SomeTitle'
},
{
id: 'cats',
title: 'SomeTitle'
},
]
}
Using an RxJs observable my code looks like the following:
getGroups() {
return this.http.get(ENDPOINT_URL)
}
I was able to achieve this using a map operator with a set but part of me feels like this isn't the correct pipe for this as the array is nested.
getGroups() {
return this.http.get(ENDPOINT_URL).pipe(
map(data => {
const groupIds = new Map();
data.groups.map(group => {
if (!groupIds.get(group.id)) {
groupIds.set(group.id, 1)
} else {
const updatedId = (groupIds.get(group.id) || 0) + 1;
groupIds.set(group.id, updatedId);
group.id = `${group.id}-${updatedId}`
}
return group
}
return data;
}
)
}
Is there a more efficient way to make this operation using a more appropriate pipe? I am worried this can become quite inefficient and significantly delay rendering of content while the observable resolves the conflicts. As of today I am unable to modify the actual content returned from the API so that is not an option unfortunately.
You could try something like this:
import { of, map } from 'rxjs';
import { findLastIndex } from 'lodash';
of({
title: 'MyTitle',
description: 'MyDescription',
groups: [
{
id: 'cats',
title: 'SomeTitle',
},
{
id: 'dogs',
title: 'SomeTitle',
},
{
id: 'cats',
title: 'SomeTitle',
},
{
id: 'octupus',
title: 'SomeTitle',
},
{
id: 'cats',
title: 'SomeTitle',
},
],
})
.pipe(
map((data) => ({
...data,
groups: data.groups.reduce((acc, group) => {
const lastElementIndex = findLastIndex(acc, (accGroup) => accGroup.id.startsWith(group.id));
if (lastElementIndex === -1) {
return [...acc, group];
}
const lastElement = acc[lastElementIndex];
const lastNameNumerator = lastElement.id.split('-')[1];
return [
...acc,
{
...group,
id: `${group.id}-${lastNameNumerator ? +lastNameNumerator + 1 : 1}`,
},
];
}, []),
}))
)
.subscribe(console.log);
Stackblitz: https://stackblitz.com/edit/rxjs-kcxdcw?file=index.ts
If the only requirement is to have the ids be unique, you could ensure uniqueness by appending the array index to each element's id.
getGroups() {
return this.http.get(ENDPOINT_URL).pipe(
map(data => {
const groups = data.groups.map(
(g, i) => ({...g, id: `${g.id}-${i}`})
);
return { ...data, groups };
})
);
}
Output of groups:
// groups: Array[5]
// 0: Object
// id : "cats-0"
// title : "SomeTitle"
//
// 1: Object
// id : "dogs-1"
// title : "SomeTitle"
//
// 2: Object
// id : "cats-2"
// title : "SomeTitle"
//
// 3: Object
// id : "octupus-3"
// title : "SomeTitle"
//
// 4: Object
// id : "cats-4"
// title : "SomeTitle"
Here's a little StackBlitz.
Honestly what you have is probably fine. Here's another method that's slightly simpler. It first uses reduce to create an object literal of groups. If you were open to external dependencies you could use Ramda's groupWith function to produce the same result. Then it uses flatMap to flatten the groups. If there is only one item in the array then it is returned as is, otherwise the elements are mutated with the new ids.
getGroups() {
return this.http.get(ENDPOINT_URL).pipe(
map(data => Object.values(
data.groups.reduce((acc, cur) => {
(acc[cur.id] || (acc[cur.id] = [])).push(cur);
return acc;
},
{} as Record<string | number, [] as GroupType[])
).flatMap(grp => (grp.length === 1)
? grp
: grp.map((x, i) => ({ ...x, id: `${x.id}-${i + 1}`)))
)
}
Another one
map((data:any) => {
//create an array in the way [{id:"cats",data:[0,3]}{id:"dogs",data:[1]..]
const keys=data.groups.reduce((a:any,b:any,i:number)=>{
const el=a.find(x=>x.id==b.id)
if (el)
el.data=[...el.data,i]
else
a=[...a,({id:b.id,data:[i]})]
return a
},[])
//loop over groups, if keys.data.length>1 ...
data.groups.forEach((x,i)=>{
const el=keys.find(key=>key.id==x.id)
if (el.data.length>1)
x.id=x.id+'-'+(el.data.findIndex(l=>l==i)+1)
})
return data;
})
Or
map((data:any) => {
//create an object keys {cats:[0,3],dogs:[1]....
const keys=data.groups.reduce((a:any,b:any,i:number)=>{
if (a[b.id])
a[b.id]=[...a[b.id],i]
else
a[b.id]=[i]
return a
},{})
//loop over groups, if keys[id].length>0 ...
data.groups.forEach((x,i)=>{
if (keys[x.id].length>1)
x.id=x.id+'-'+(keys[x.id].findIndex(l=>l==i)+1)
})
return data;
})
I have this Redux store and reducer
const INITIAL_WORK = {
departments: []
}
const works = (state = INITIal_WORK, action) => {
switch(action.type){
case 'ADD_DEPARTMENT':
return {
...state,
departments: [...state.department, action.item]
}
default:
return state
}
}
In departments work people so I want to this people was inside a single department works people. So after fetch data from db I want my store look like this:
const INITIAL_WORK = {
departments: [
{
id: 1,
name: "First department",
people: [
{
id: 1,
name: "Johna Wayne"
},
{
id: 2,
name: "Jessica Biel"
},
{
id: 3,
name: "Bratt Pitt"
}
]
},
{
id: 2,
name: "second department",
people: [
{
id: 4,
name: "Salma Hayek"
},
{
id: 5,
name: "Sylvester Stallone"
}
]
}
]
}
Is it possible create case in reducer which will be added people inside people array inside single department? How can I do that?
Yes, absolutely.
Let us assume your API returns payload as following.
{
departmentId: `id of the department`
id: `person id`
name: `person name`
}
const INITIAL_WORK = {
departments: []
}
const works = (state = INITIal_WORK, action) => {
switch(action.type){
case 'ADD_DEPARTMENT':
return {
...state,
departments: [...state.department, action.item]
}
case 'ADD_PERSON':
const { departmentId, id, name } = action.item
const departments = [...state.departments];
departments[departmentId].people.push({id, name})
return {
...state,
departments
}
default:
return state
}
}
Combining Immer.js and Redux will be helpful for this case.
Here is a simple example of the difference that Immer could make in practice.
// Reducer with inital state
const INITAL_STATE = {};
// Shortened, based on: https://github.com/reactjs/redux/blob/master/examples/shopping-cart/src/reducers/products.js
const byId = (state = INITAL_STATE, action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
return {
...state,
...action.products.reduce((obj, product) => {
obj[product.id] = product
return obj
}, {})
}
default:
return state
}
}
After using Immer, our reducer can be expressed as:
import produce from "immer"
// Reducer with inital state
const INITAL_STATE = {};
const byId = produce((draft, action) => {
switch (action.type) {
case RECEIVE_PRODUCTS:
action.products.forEach(product => {
draft[product.id] = product
})
}
}, INITAL_STATE)
let mergePeople=[];
INITIAL_WORK.departments.filter((cur,index)=>{
cur.people.filter((person)=>{
mergePeople.push(person);
})
}
)
console.log(mergePeople);
I am doing this:
case LOAD_PAGES:
return {
...state,
pages: [...state.pages, action.pages],
};
And I have a component that every time I enter to it, it send the same data to the store so I am getting lots of duplicate data.
The pages array looks like this:
pages: [
{
key: 0,
menuName: 'Home',
pageType: 'HomePage',
dataIndex: 'HomePage0'
},
{
key: 1,
menuName: 'Employer Chat',
pageType: 'EmployerChat',
dataIndex: 'EmployerChat1'
},
]
This is the React component:
const handlePageLoad = () => {
if (siteById.data) {
siteById.data.pages.map((p, index) => {
return loadPagesAction({
key: index,
menuName: p.menuName,
pageType: p.pageType,
dataIndex: p.pageType + index,
});
});
}
};
useEffect(() => {
if (siteById.data.pages.length) {
handlePageLoad();
}
}, []);
Any ideas?
Here's "smart" way to filter duplicates.
function filterDuplicates(array, areEqual) {
return array.filter((item, pos) => {
return array.findIndex((other) => areEqual(item, other)) == pos;
});
}
console.log(
filterDuplicates([
{ key: 1, name: 'test' },
{ key: 2, name: 'apple' },
{ key: 1, name: 'test' },
], (a, b) => a.key == b.key)
);
Pass array to first argument and equality comparer to second argument of filterDuplicates. I got this idea from that answer.
TypeScript
function filterDuplicates<T>(array: T[], areEqual: ((a: T, b: T) => boolean)): T[] {
return array.filter((item: T, pos: number) => {
return array.findIndex((other: T) => areEqual(item, other)) == pos;
});
}
You can do something like this :
Check if there is an existing page in your state or not, if return same or else push
case LOAD_PAGES:
return {
...state,
pages: state.pages.findIndex(page => page.key === action.pages.key) >= 0 ?
state.pages :
[...state.pages, action.pages]
};
There are many ways to solve your problem and here is how I would approach it:
Instead of using an array, store your pages in an object and use the already defined keys as keys for the object. You can use Object.values(store.pages) or Object.entries(store.pages) to get an array of the pages the way you did before.
Simply using set, the duplicated element can be omitted.
return { ...state, arr: Array.from(new Set([...state.arr, ...newArr]))};
I’ve got a patients array in state. If the patient.room matches roomStates.room then I would like to set the patient.roomStatus to be roomStates[key].name. My function is as follows but I can’t see why its not working.
Patient array
const patients = [
{name: "Jay”, room: "room01", roomStatus: ""},
{name: "Leo”, room: "room02", roomStatus: ""},
{name: "Jim", room: "room05", roomStatus: ""}
]
const roomState = {
room01: {type: "info", room: "room01", name: "Stopped"},
room02: {type: "normal",room: "room02", name: "InRoom"},
room05: {type: "normal", room: "room05",name: "InRoom"},
}
handleRoomStateChange(roomStates) {
Object.keys(roomStates).map((key) => {
this.state.patients.map(patient => {
if (patient.room === roomStates[key].room) {
this.setState({ ...patient, roomStatus: roomStates[key].name});
}
})
});
}
Do not set state on every loop iteration, save the data into array and then set it once the loop is done executing:
handleRoomStateChange = roomStates => {
const patients = Object.keys(roomStates).map(key => {
return this.state.patients.map(patient => {
if (patient.room === roomStates[key].room) {
return {
...patient,
roomStatus: roomStates[key].name
};
}
return patient;
});
});
this.setState({patients});
};
Edit: that actually returns a nested array, to get a proper data structure extra map can be avoided:
handleRoomStateChange = roomStates => {
const patients = this.state.patients.map(patient => {
const roomKey = Object.keys(roomStates).find(key => key === patient.room);
return {
...patient,
roomStatus: roomStates[roomKey].name
}
});
this.setState({ patients });
}
I'm trying to create a set of reducers in order to change an attribute of all objects in a nested list.
The input payload looks like the following:
const payload = [
{
name: "Peter",
children: [
{
name: "Sarah",
children: [
{
name: "Sophie",
children: [
{
name: "Chris"
}
]
}
]
}
]
}
];
I now want to change the name attribute of all elements and child elements.
const mapJustNickname = elem => {
return {
...elem,
nickname: elem.name + "y"
};
};
How do I use this map function recursively on all child elements?
I found a way to do this by putting the the recursion within the same mapping function.
const mapToNickname = (elem) => {
return {
nickname: elem.name +'y',
children: elem.children && elem.children.map(mapToNickname)
}
}
console.log(payload.map(mapToNickname));
But I'd like to have the mapping of the name separated from the recursion (for reasons of keeping the mapping functions as simple as possible) and being able to chain them later. Is it somehow possible to do this with two reducers and then chaining them together?
Let's start by rigorously defining the data structures:
data Person = Person { name :: String, nickname :: Maybe String }
data Tree a = Tree { value :: a, children :: Forest a }
type Forest a = [Tree a]
type FamilyTree = Tree Person
type FamilyForest = Forest Person
Now, we can create mapTree and mapForest functions:
const mapTree = (mapping, { children=[], ...value }) => ({
...mapping(value),
children: mapForest(mapping, children)
});
const mapForest = (mapping, forest) => forest.map(tree => mapTree(mapping, tree));
// Usage:
const payload = [
{
name: "Peter",
children: [
{
name: "Sarah",
children: [
{
name: "Sophie",
children: [
{
name: "Chris"
}
]
}
]
}
]
}
];
const mapping = ({ name }) => ({ name, nickname: name + "y" });
const result = mapForest(mapping, payload);
console.log(result);
Hope that helps.
Create a recursive map function that maps an item, and it's children (if exists). Now you can supply the recursiveMap with a ever transformer function you want, and the transformer doesn't need to handle the recursive nature of the tree.
const recursiveMap = childrenKey => transformer => arr => {
const inner = (arr = []) =>
arr.map(({ [childrenKey]: children, ...rest }) => ({
...transformer(rest),
...children && { [childrenKey]: inner(children) }
}));
return inner(arr);
};
const mapNickname = recursiveMap('children')(({ name, ...rest }) => ({
name,
nickname: `${name}y`,
...rest
}));
const payload = [{"name":"Peter","children":[{"name":"Sarah","children":[{"name":"Sophie","children":[{"name":"Chris"}]}]}]}];
const result = mapNickname(payload);
console.log(result)