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;
})
Related
I have an a state object in React that looks something like this (book/chapter/section/item):
const book = {
id: "123",
name: "book1",
chapters: [
{
id: "123",
name: "chapter1",
sections: [
{
id: "4r4",
name: "section1",
items: [
{
id: "443",
name: "some item"
}
]
}
]
},
{
id: "222",
name: "chapter2",
sections: []
}
]
}
I have code that adds or inserts a new chapter object that is working. I am using:
// for creating a new chapter:
setSelectedBook(old => {
return {
...old,
chapters: [
...old.chapters,
newChapter // insert new object
]
}
})
And for the chapter update, this is working:
setSelectedBook(old => {
return {
...old,
chapters: [
...old.chapters.map(ch => {
return ch.id === selectedChapterId
? {...ch, name: selectedChapter.name}
: ch
})
]
}
})
But for my update/create for the sections, I'm having trouble using the same approach. I'm getting syntax errors trying to access the sections from book.chapters. For example, with the add I need:
// for creating a new section:
setSelectedBook(old => {
return {
...old,
chapters: [
...old.chapters,
...old.chapters.sections?
newSection // how to copy chapters and the sections and insert a new one?
]
}
})
I know with React you're supposed to return all the previous state except for what you're changing. Would a reducer make a difference or not really?
I should note, I have 4 simple lists in my ui. A list of books/chapters/sections/items, and on any given operation I'm only adding/updating a particular level/object at a time and sending that object to the backend api on each save. So it's books for list 1 and selectedBook.chapters for list 2, and selectedChapter.sections for list 3 and selectedSection.items for list 4.
But I need to display the new state when done saving. I thought I could do that with one bookState object and a selectedThing state for whatever you're working on.
Hopefully that makes sense. I haven't had to do this before. Thanks for any guidance.
for adding new Section
setSelectedBook( book =>{
let selectedChapter = book.chapters.find(ch => ch.id === selectedChapterId )
selectedChapter.sections=[...selectedChapter.sections, newSection ]
return {...book}
})
For updating a section's name
setSelectedBook(book=>{
let selectedChapter = book.chapters.find(ch => ch.id === selectedChapterId )
let selectedSection = selectedChapter.sections.find(sec => sec.id === selectedSectionId )
selectedSection.name = newName
return {...book}
})
For updating item's name
setSelectedBook(book =>{
let selectedChapter = book.chapters.find(ch => ch.id === selectedChapterId )
let selectedSection = selectedChapter.sections.find(sec => sec.id === selectedSectionId )
let selectedItem = selectedSection.items.find(itm => itm.id === selectedItemId)
selectedItem.name = newItemName
return {...book}
})
I hope you can see the pattern.
I think the map should work for this use case, like in your example.
setSelectedBook(old => {
return {
...old,
chapters: [
...old.chapters.map(ch => {
return { ...ch, sections: [...ch.sections, newSection] }
})
]
}
})
In your last code block you are trying to put chapters, sections and the new section into the same array at the same level, not inside each other.
Updating deep nested state objects in React is always difficult. Without knowing all the details of your implementation, it's hard to say how to optimize, but you should think hard about different ways you can store that state in a flatter way. Sometimes it is not possible, and in those cases, there are libraries like Immer that can help that you can look in to.
Using the state object you provided in the question, perhaps you can make all of those arrays into objects with id for keys:
const book = {
id: "123",
name: "book1",
chapters: {
"123": {
id: "123",
name: "chapter1",
sections: {
"4r4": {
id: "4r4",
name: "section1",
items: {
"443": {
id: "443",
name: "some item"
}
}
}
}
},
"222": {
id: "222",
name: "chapter2",
sections: {},
}
]
}
With this, you no longer need to use map or find when setting state.
// for creating a new chapter:
setSelectedBook(old => {
return {
...old,
chapters: {
...old.chapters,
[newChapter.id]: newChapter
}
}
})
// for updating a chapter:
setSelectedBook(old => {
return {
...old,
chapters: {
...old.chapters,
[selectedChapter.id]: selectedChapter,
}
}
})
// for updating a section:
setSelectedBook(old => {
return {
...old,
chapters: {
...old.chapters,
[selectedChapter.id]: {
...selectedChapter,
sections: {
[selectedSectionId]: selectedSection
}
},
}
}
})
Please let me know if I misunderstood your problem.
The idea was to query a dataset with querystring params. I only want the "records" to match only what was queried.
Dataset
{
1111:
{
Category: "Education"
Role: "Analyst"
}
2222:
{
Category: "Communications and Media"
Role: "Analyst"
}
3333:
{
Category: "Public Sector"
Role: "Something else"
}
4444:
{
Category: "Public Sector"
Role: "Something else"
}
...
}
[[Prototype]]: Object
I'm sending in qString
Category: (2) ['Communications and Media', 'Education']
Role: ['Analyst']
length: 0
[[Prototype]]: Array(0)
I'd like to loop over that and filter/reduce so I only have records that match. Sort of an and instead of an or.
dataSet is an Object of objects. Thoughts? Thanks in advance.
export const Filtered = (qStrings, dataSet) => {
const filtered = [];
Object.entries(qStrings).forEach(([field]) => {
qStrings[field].forEach((value) => {
filtered.push(
..._.filter(dataSet, (sess) => {
if (sess[field] && sess[field].toString() === value.toString()) {
return sess;
}
})
);
});
});
return _.uniq(filtered);
};
geez, I figured it out with a colleague who's way smarter than me wink Jess!
export const Filtered = (qStrings, dataSet) => {
let filtered = [];
Object.entries(qStrings).forEach(([field], idx) => {
let source = filtered;
if (idx === 0) {
source = dataSet;
}
filtered = _.filter(source, (sess) => {
return sess[field] && sess[field].includes(qStrings[field]);
});
});
return _.uniq(filtered);
};
Now to clean this up.
Not sure if this solves your problem exactly, but you can apply this logic without mutation for a much cleaner function.
export const matches = (qStrings, dataSet) =>
Object.entries(dataSet).reduce((acc, [key, value]) =>
Object.entries(value).every(([rKey, rValue]) => qStrings[rKey]?.includes(rValue))
? { ...acc, [key]: value }
: acc,
{});
This will return records 1111 and 2222 because they match one of the categories and the role in qStrings.
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'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)
service.$post() my observable from a service of return type <Observable<any[]>>, emits this collection
[{ name: 'nike' }, { name: 'nika' }, { name: 'niko' }, { name: 'niky' }]
this.posts = <Observable<any[]>>this.service.$post();
// result
// [{ name: 'nike' }, { name: 'nika' }, { name: 'niko' }, { name: 'niky' }]
My question is how can I control or filter the number of objects being returned from emitted collection, in the example below I want to take 2 objects from <Observable<any[]>>this.service.$post(). Please help
this.posts = this.service.$post()
.pipe(
take(2)
);
// should be
// [{ name: 'nike' }, { name: 'nika' }]
You can get the first element like this:
this.posts = this.service.$post()
.pipe(
map(arr => arr[0])
);
take(1) operator would return only the first value emitted by your observable. In this case it would be the entire array. And since the post request only emits once, it wouldn't really be useful.
If you want to keep x number of items, you can do it like this:
const x = 2;
this.posts = this.service.$post()
.pipe(
map(arr => arr.slice(0, x))
);
const x = 2;
return this.service.$post()
.map(arr => arr.slice(0, x));
I think this will do