I followed the approach as the answer in this question suggested
The issue is that I want to construct an Array using setState and I dont; want to add duplicate data.
here is the sample data
const data = [
{
parentId: '1',
userChoice: 'NO',
child: []
},
{
parentId: '2',
userChoice: 'NO',
child: []
},
}
I'm catching the parentId from question.id from user input. If there is the same question then I don't want to concat into the array.
This is how I'm trying.
const [data, setData] = useState([]) // state
function arrayHandler(question) {
const hasParentId = data.find((parentId) => question.id === parentId)
// concat if array do not have any question with same id.
if (!hasParentId) {
setData((data) => data.concat({ parentId: question.id, userChoice: 'NO', child: [] }))
}
}
But this is not working, How can I fix this ?
You can check if any of the elements in the data have a matching ID using Array.some and if not then you can use concat or just the spread operator to add the new object to the existing data.
function arrayHandler(question) {
if (!data.some((d) => question.id === d.parentId)) {
setData((data) => [...data, { parentId: question.id, userChoice: "NO", child: [] }]);
}
}
I think that now it shold work
function arrayHandler(question) {
const hasParentId = data.find((object) => question.id === object.parentId)
// concat if array do not have any question with same id.
if (!hasParentId) {
setData((data) => data.concat({ parentId: question.id, userChoice: 'NO', child: [] }))
}
}
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.
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;
})
this is my code
const [state, setState] = useState(
[{id: 1, key:""}, {id: 2, key:""}, {id: 3, key:""}]
)
i want to to change "key" state
im confuse
now im using
setState(
[...state].map((data, index) => {
if (data.id === state[index].id) {
return {
...data,
key: result,
};
} else return data;
}),
);
}
result variable came from result when i fetching data.
result is a random string
If your data structure is always going to be in that order data.id === state[index].id doesn't really achieve much.
For example:
when data.id is 1 the index will be 0. And state[0].id is 1.
when data.id is 2 the index will be 2. And state[1].id is 2.
etc.
It just sounds like you want to iterate over all the objects in state and update each key value with that random string you mentioned in the comment section. There's no need to make a copy of state since map already returns a new array ready for setState to use.
function setState(mapped) {
console.log(mapped);
}
const state = [{ id: 1, key: '' }, { id: 2, key: '' }, { id: 3, key: '' }];
const result = 'random';
const mapped = state.map(data => {
return { ...data, key: result };
});
setState(mapped);
I have a function which gets the user input and update the array if the same key is available already. with the help of this article
This is how it looks;
const handleClickYes = (question) => {
// find if question is in the data array
const hasQuestion = data.find(({ parentId }) => question.id === parentId)
const indexOfQuestion = data.indexOf(hasQuestion)
if (hasQuestion) {
// update the value with specific selected index in the array.
data[indexOfQuestion] = { question: question.if_yes, parentId: question.id, userChoice: 'YES', child: [] }
} else {
setData((data) => data.concat({ question: question.if_yes, parentId: question.id, userChoice: 'YES', child: [] }))
}
localStorage.setItem('deviceReport', JSON.stringify(data))
}
I'm using localStorage to persist the state
const deviceReport = localStorage.getItem('deviceReport') ? JSON.parse(localStorage.getItem('deviceReport')) : []
const [data, setData] = useState(deviceReport)
Here the problem is if I use setData then it updates instantly but on replacing the array
at this part
data[indexOfQuestion] = { question: question.if_yes, parentId: question.id, userChoice: 'YES', child: [] }
it dosent update the mapped data on JSX portion. How can I configure it to update it happen in setState. ? Or any other better option to update the array.
You're not calling setState() within the first half of your if block. Also, never mutate state directly. Make a mutable copy, like so:
const handleClickYes = (question) => {
// find if question is in the data array
const hasQuestion = data.find(({ parentId }) => question.id === parentId);
const indexOfQuestion = data.indexOf(hasQuestion);
// copy data to mutable object
let newData = [...data];
if (hasQuestion) {
// update the value with specific selected index in the array.
newData[indexOfQuestion] = {
question: question.if_yes,
parentId: question.id,
userChoice: "YES",
child: [],
};
} else {
// concat existing data with a new question
newData = [
...newData,
{
question: question.if_yes,
parentId: question.id,
userChoice: "YES",
child: [],
},
];
}
localStorage.setItem("deviceReport", JSON.stringify(newData));
setData(newData);
};
My linter is bullying me.
I made a function to set tasks completed to !completed with ids as parameters
The data looks like this:
const lists = [
{
id: 'todo-3',
name: 'Example 3',
tasks: [{ name: 'task', completed: true, id: "Un" },{ name: 'task2', completed: true, id: "Deux" },{ name: 'task3', completed: true, id: "Trois" }]
}
{
id: 'todo-4',
name: 'Example 5',
tasks: [{ name: 'task', completed: true, id: "Un" },{ name: 'task2', completed: true, id: "Deux" },{ name: 'task3', completed: true, id: "Trois" }]
}
]
At first, I made a function like this and it works but the linter didn't like it.
const toggleTask = (listId: string, taskId: string) => {
const newListsToggled = lists.map((listItem) => {
if (listItem.id === listId) {
listItem.tasks.map((task) => {
if (task.id === taskId) {
task.completed = !task.completed;
}
return task;
});
}
return listItem;
});
};
"task.completed = !task.completed" this part gave me the No-param-reassign error so I tried another function:
const toggleTask = (listId: string, taskId: string) => {
const newListsToggled = lists.map((listItem) => {
if (listItem.id === listId) {
return listItem.tasks.map((task) => {
if (task.id === taskId) {
return {...task, completed: !task.completed}
}
return task;
}
);
}
return listItem;
});
console.log('testtoggle',newListsToggled)
};
toggleTask('todo-3','Deux')
This one doesn't return the whole array, the lists.name and lists.id parts are gone.
Without bypassing the linter is there any way to solve this function?
This one doesn't return the whole array, the lists.name and lists.id part are gone
To solve this problem, instead of returning return listItem.tasks.map(...), you can use the same object spread trick you have used with task:
if (listItem.id === listId) {
return {
...listItem,
tasks: listItem.tasks.map(...)
}
}
The warning is triggered by the fact that in your .map(task => ... ) you modify task (before then returning it). The way you use map has a side effect on the input collection. It could be intentional, or it could be irrelevant, but it still is happening.
The purpose of .map() is usually to generate a new result object/value for each input object/value. You should (almost) never change the input values, which is what you are doing, you should only generate output values.
If you actually want to modify the input values, it is better to use .forEach instead:
if (listItem.id === listId) {
listItem.tasks.forEach((task) => {
if (task.id === taskId)
task.completed = !task.completed;
}
}
return listItem;
This can be combined with .filter instead of the if:
if (listItem.id === listId) {
listItem.tasks
.filter(task => task.id === taskId)
.forEach(task => task.completed = !task.completed);
}
return listItem;
Or if you know there is never more than 1 matching task, then you can use .find:
if (listItem.id === listId) {
var task = listItem.tasks.find(task.id === taskId);
if (task) task.completed = !task.completed;
}
return listItem;