setState nested object in a map function - reactjs

My State Object is a array that holds objects.PS:(Feel free to change the structure) Here is the structure:
{
type: ListOfTypes[Math.floor(Math.random() * ListOfTypes.length)],
name: ListOfNames[Math.floor(Math.random() * ListOfNames.length)],
id: nanoid(),
channels: [
{
id: nanoid(),
Name: ListOfChannels[Math.floor(Math.random() * ListOfChannels.length)],
Files: [ { folder: "Folder", documents: [ { doc: "WordDoc1.doc", isChecked: false, id:nanoid() }, { doc: "WordDoc2.doc", isChecked: false, id:nanoid() }, { doc: "WordDoc3.doc", isChecked: false, id:nanoid() }, { doc: "WordDoc4.doc", isChecked: false, id:nanoid() }] }, ],
},
{
id: nanoid(),
Name: ListOfChannels[Math.floor(Math.random() * ListOfChannels.length)],
Files: [{ folder: "Folder", documents: [ { doc: "WordDoc1.doc", isChecked: false, id:nanoid() }, { doc: "WordDoc2.doc", isChecked: false, id:nanoid() }, { doc: "WordDoc3.doc", isChecked: false, id:nanoid() }, { doc: "WordDoc4.doc", isChecked: false, id:nanoid() }] }, ],
},
{
id: nanoid(),
Name: ListOfChannels[Math.floor(Math.random() * ListOfChannels.length)],
Files: [{ folder: "Folder", documents: [ { doc: "WordDoc1.doc", isChecked: false, id:nanoid() }, { doc: "WordDoc2.doc", isChecked: false, id:nanoid() }, { doc: "WordDoc3.doc", isChecked: false, id:nanoid() }, { doc: "WordDoc4.doc", isChecked: false, id:nanoid() }] }, ],
}
]
}
I want to change all the isChecked in every channel object, currently I'm using this function but it doesn't work as intended.
const handleChange = () => {
const ConnectionsArray = List.map( (connection) => connection.id == connectionId ?
{
...connection,
channels: connection.channels.map( (channel) => channel.Name == channelName ? {
...channel,
Files: channel.Files.map((file) => ({
...file,
documents: file.documents.map((doc) => ({ ...doc, isChecked: !doc.isChecked }) )
}))
} : channel)
} : connection)
setList(ConnectionsArray)
};

This should do it:
function toggleChecked (connections) {
return connections.map(connection => (connection.id === connectionId
? {
...connection,
channels: connection.channels.map(channel => (channel.Name === channelName
? {
...channel,
Files: channel.Files.map(file => ({
...file,
documents: file.documents.map(doc => ({
...doc,
isChecked: !doc.isChecked,
})),
})),
}
: channel)),
}
: connection));
}
Use like this:
setList(list => toggleChecked(list));
Here's another function to help with getting a random item from an array (I notice you're repeating a lot math expressions to do this in your code):
function getRandomElement (array) {
return array[Math.floor(Math.random() * array.length)];
}
Use like this:
// before
ListOfTypes[Math.floor(Math.random() * ListOfTypes.length)]
// after
getRandomElement(ListOfTypes)

Probably a good time to learn how to use the "immer" library. It's perfect for situations like these where you need to make changes too deeply nested objects. Without it, making changes to objects like the one you're dealing with gets really messy and hard to deal with.
Immer is really easy to pick up and learn in a day or two. If you used it, your code could be reduced to this:
import produce from 'immer';
const handleChange = ()=>{
const ConnectionsArray = produce(List, draft=>{
draft.forEach((object)=>{
object.channels.forEach((channel)=>{
channel.Files.forEach((file)=>{
file.documents.forEach((document)=>{
document.isChecked = !document.isChecked;
})
})
})
})
})
}
I didn't run this code so not 100% sure it works. Either way, something like this with immer will work and be a lot easier to deal with. Notice you don't have to deal with the spread syntax or any of that, and immer will actually be creating a new object so it avoids any of the headaches associated with mutable data.

Check this:
const handleChange = () => {
setList(prevState => {
let ConnectionsArray = [...prevState];
const itemIndex = ConnectionsArray.find(item => item.id === connectionId);
const connection = {...ConnectionsArray[itemIndex]};
ConnectionsArray[itemIndex] = {
...connection,
channels: connection.channels.map( (channel) => channel.Name == channelName ? {
...channel,
Files: channel.Files.map((file) => ({
...file,
documents: file.documents.map((doc) => ({ ...doc, isChecked: !doc.isChecked }) )
}))
} : channel)
};
return ConnectionsArray;
})
};

Related

Breaking down complex Prisma queries in Next.js app

I’ve got a Next.JS app. I want to render a todo feed on the homepage, but also the user page. I'm a bit stuck on how to break down my Prisma queries.
I fetch a big data object using getServerSideProps and pass this to the page component (and using react-query to hydrate and do re-fetching, but not relevant now)
- getRecentTodos (includes todos) for my homepage
- getUserDetailsByName (includes todos) for the user page
export type getRecentTodos = ReturnType<typeof getRecentTodos> extends Promise<
infer T
>
? T
: never
export const getRecentTodos = async (recentItemsAmount = 20) => {
return await prisma.todos.findMany({
where: { done: true },
select: {
id: true,
userId: true,
content: true,
done: true,
createdAt: true,
attachments: true,
todoReplies: {
select: {
id: true,
userId: true,
content: true,
todoReplyLikes: true,
todoId: true,
user: { select: { name: true, displayName: true, image: true } },
},
orderBy: { createdAt: 'asc' },
},
todoLikes: {
select: {
user: true,
},
},
user: {
select: {
name: true,
displayName: true,
image: true,
},
},
},
orderBy: { createdAt: 'desc' },
take: recentItemsAmount,
})
}
export const getUserDetailsByName = async (username: string) => {
return await prisma.user.findUnique({
where: {
name: username,
},
select: {
name: true,
displayName: true,
bio: true,
location: true,
twitter: true,
image: true,
createdAt: true,
todos: {
select: {
id: true,
content: true,
userId: true,
done: true,
updatedAt: true,
createdAt: true,
attachments: true,
user: true,
todoLikes: true,
todoReplies: {
take: 30,
orderBy: { createdAt: 'desc' },
select: {
id: true,
userId: true,
todoId: true,
createdAt: true,
content: true,
user: true,
},
},
},
take: 30,
orderBy: { createdAt: 'desc' },
},
projects: true,
},
})
}
Both queries return ‘todos,’ but they can return it in a slightly different way. The todo feed component expects certain properties to be available
- E.g. displayName on todoReplies
- But on getUserDetailsByName the displayName might not be part of the response or it’s nested one layer deeper or something
How to keep this from getting complex very fast?
You more or less want to select todos in your queries the same way (returning the same and omitting the same, apart of some things like order)
But manually keeping these things in sync over lot’s of queries qet’s complex quickly
Possible solutions?
Should I break the getServerSideProps into multiple fetches?
So instead of one ‘getUserDetailsByName’ which has todos as a relationfield included
fetch user details
fetch todos
This would mean I also have to write more react-query code for refetching etc… because you are dealing with multiple objects. But it does seperate concerns more.
Using Typescript to catch it in my codebase when a function tries to access a property which is not returned from that specific Prisma query? (I’m just now starting to see the possibilities of Typescript for stuff like this)
Should I just standardize the way the todos get created in a prisma query with a function and include that function inside of the Prisma queries? you can include like:
const todoSelect = {
id: true,
userId: true,
content: true,
{.......}
user: {
select: {
name: true,
displayName: true,
image: true,
},
},
}
export type getRecentTodos = ReturnType<typeof getRecentTodos> extends Promise<
infer T
>
? T
: never
export const getRecentTodos = async (recentItemsAmount = 20) => {
return await prisma.todos.findMany({
where: { done: true },
select: todoSelect,
orderBy: { createdAt: 'desc' },
take: recentItemsAmount,
})
}
const userSelect = {
name: true,
{......}
todos: {
select: todoSelect,
take: 30,
orderBy: { createdAt: 'desc' },
},
projects: true,
}
export const getUserDetailsByName = async (username: string) => {
return await prisma.user.findUnique({
where: {
name: username,
},
select: userSelect,
})
}

Select an array inside an array in React

I'm trying to select an arrayfield inside an array. Following code is inserted in my useStore:
const useStore = create(
persist(set => ({
projectszustand: [
{
id: nanoid(),
name: 'Projekt Final 1',
notes: 'Hier sind ein paar Notizen',
begin: '01/01/2001',
end: '02/01/2001',
icon: 'https://www.skopos.de/wp-content/uploads/2021/04/Element-5.svg',
color: 'blue',
edit: false,
selected: true,
feature: [
{
id: nanoid(),
name: 'Feature Final 1',
begin: '01/01/2001',
end: '02/01/2001',
isChecked: false,
edit: false,
selected: false,
todo: [
{...and so on
So, I'm trying to go with forEach and set all selected fields in the feature array to false:
selectFeature: index => {
set(
produce(draft => {
draft.projectszustand[index].feature.forEach(element => {
element.selected = false;
});
draft.projectszustand[index].feature[index].selected =
!draft.projectszustand[index].feature[index].selected;
})
);
},
This no works. Error message is: TypeError: can't access property "feature", draft.projectszustand[index] is undefined
Has somebody an easy solution for this?
Thanks very much for helping.

Why does forEach loop only set the last value if finds to state. ReactJS

const CategoriesData = [
{
name: "Category1",
isActive: true,
children: [
{
name: "Category1Child",
isActive: false,
}
]
},
{
name: "Category2",
isActive: false,
},
{
name: "Category3",
isActive: true,
children: [
{
name: "Category3Child",
isActive: false,
}
]
}
];
const [disabledCategories, setDisabledCategories] = useState([]);
function notActiveCategories(categories) {
// Loop logs out at least 7 isActive: false categories.
categories.forEach((category) => {
if (category.isActive) notActiveCategories(category.children);
if (!category.isActive) {
setDisabledCategories([...disabledCategories, category]);
console.log(category);
}
});
};
useEffect(() => {
notActiveCategories(CategoriesData);
console.log(disabledCategories); // Only 1 category is in the array.
}, []);
I feel like the function the loop is in calling itself is causing the disabledCategories state to revert to when it was empty and that is leading to only the last step of the foreach to be set.
So how would i get this to loop through the categories array and have the disabledCategories state to contain all of the category objects that have isActive: false.
Which in the example of CategoriesData above, it would mean that the disabledCategories state would contain:
[
{
name: "Category1Child",
isActive: false,
},
{
name: "Category2",
isActive: false,
},
{
name: "Category3Child",
isActive: false,
},
];
Try changing your setDisabledCategories to use the previous state param that comes from setState:
setDisabledCategories(prevState => [...prevState, category])
When multiple setState calls are batched together you need to be careful so they don't override each other. Using this method ensures that your setState calls are "chained" so you always get the updated state.
Way 1: Affect after recursive loop
function notActiveCategoriesRecusive(categories) {
let notActive = []
categories.forEach((category) => {
if (category.isActive) notActive = [...notActive, ...(notActiveCategories(category.children))];
if (!category.isActive) {
notActive.push(category)
}
});
return notActive
};
function notActiveCategories(categories) {
setDisabledCategories(notActiveCategoriesRecusive(categories)
}
Way 2: Get the last state because it doesn't has time to refresh
function notActiveCategories(categories) {
categories.forEach((category) => {
if (category.isActive) notActiveCategories(category.children);
if (!category.isActive) {
setDisabledCategories(oldState => ([...oldState, category]))
}
});
};
I'd only call setState once with the filtered array:
const findInactive = data =>
data.filter(e => !e.isActive)
.concat(...data.filter(e => e.children)
.map(e => findInactive(e.children)))
;
const categoriesData = [ { name: "Category1", isActive: true, children: [ { name: "Category1Child", isActive: false, } ] }, { name: "Category2", isActive: false, }, { name: "Category3", isActive: true, children: [ { name: "Category3Child", isActive: false, } ] } ];
const inactive = findInactive(categoriesData)
// the following is neeeded if it's possible for a
// node to have children and be inactive
// .map(({name, isActive}) => ({name, isActive}))
;
console.log(inactive);
//setDisabledCategories(inactive); // one time in React
This makes the code a lot easier to reason about and decouples React's API out from the filtering logic, which can be moved out to a generic function agnostic of React.
As others have mentioned, if you do want to call setState multiple times as a batch update, you can use the prevState callback to chain the updates: setDisabledCategories(prevState => [...prevState, category]);.

How to use spread operator to update array inside an object?

What the fetch returns is a list of items. I want to add those into state.
const [state, setState] = useState({
list: {
items: [],
}
});
fetch('http://example.com/list/')
// GET response: [{ name: 'foo' }, { name: 'bar' }, { name: 'baz' }]
.then((resList) => resList.json())
.then((list) => {
list.forEach(({ name }) => {
const itemUrl = `https://example.com/list/${name}`;
fetch(itemUrl)
// GET responses:
// { name: 'foo', desc: '123' }
// { name: 'bar', desc: '456' }
// { name: 'baz', desc: '789' }
.then((itemRes) => itemRes.json())
.then((item) => {
setState((prevState) => ({
...prevState,
list: {
items: [...state.list.items, item]
},
});
})
})
}
})
console.log(state);
// result: [{ name: 'baz', desc: '789' }]
// but wanted: [{ name: 'foo', desc: '123' }, { name: 'bar', desc: '456' }, { name: 'baz', desc: '789' }]
In your case no need to use prevState in setState.
I prepared an example for you. Just be careful at using hooks.
https://codesandbox.io/s/recursing-wood-4npu1?file=/src/App.js:0-567
import React, { useState } from "react"
import "./styles.css"
export default function App() {
const [state, setState] = useState({
list: {
items: [
{ name: "foo", desc: "123" },
{ name: "bar", desc: "456" },
],
},
})
const handleClick = () => {
setState(() => ({
list: {
items: [...state.list.items, { name: "baz", desc: "789" }],
},
}))
}
return (
<div className="App">
<button onClick={handleClick}>Click Me </button>
<hr />
{JSON.stringify(state)}
</div>
)
}
You can't directly access the callback for useState hooks. This is how you can update state after fetching the data:
setState({
...state,
list: {
items:[...state.list.items, item]
},
});

How to update multiple element inside List with ImmutableJS?

Hi I am using immutableJS and I would want to update multiple objects in my array if it has the same id from action.contacts
const initialState = fromJS({
list: [{
id: 1,
loading: false,
}, {
id: 2,
loading: false,
}, {
id: 3,
loading: false,
}]
});
action.contacts = [{
id: 1
}, {
id: 2
}]
I expected when I call state.get('list') it would equal to
list: [{
id: 1,
loading: true,
}, {
id: 2,
loading: true,
}, {
id: 3,
loading: false,
}]
what I have done so far is this:
case UNLOCK_CONTACTS:
const indexesOfRow = state.get('list').findIndex((listItem) => {
return action.contacts.map((contact)=> listItem.get('id') === contact.id)
})
return indexesOfRow.map((index)=> {
state.setIn(['list', index, 'loading'], true);
});
}));
but it's not working out for me, didn't update anything
I created a similar solution in a fiddle http://jsfiddle.net/xxryan1234/djj6u8xL/398/
You are missing the point of immutable.js. The objects are not mutable.
const initialState = Immutable.fromJS({
list: [{
id: 1,
loading: false
}, {
id: 2,
loading: false
}, {
id: 3,
loading: false
}],
});
const contacts = [{
id: 1
}, {
id: 3
}]
let newState = initialState.set( 'list', initialState.get('list').map( (row,index) => {
let contact = contacts.find((item)=>{
return item.id == row.get('id')
})
if (contact){
return row.set('loading', true)
}
return row;
}))
console.log(newState.toJS())
see in the updated fiddle http://jsfiddle.net/djj6u8xL/399/
const newState = initialState.update('list', (oldValue) =>
oldValue.map(item =>
action.contacts.find(act =>
act.get('id') === item.get('id')) !== undefined ?
item.update('loading',(oldVal)=> !oldVal) : item))
console.log(newState.toJS())
notice: you need to turn action.contacts into immutable list of immutable Maps.
case UNLOCK_CONTACTS:
return state.set('list', state.get('list').map((listItem) => {
const matched = _.find(action.contacts, (contact) => listItem.get('id') === contact.id);
if (matched) {
return fromJS({ ...listItem.toJS(), loading: true });
}
return listItem;
}));
So I manage to solve it by mapping the list and then finding if the listItem exists in action.contacts. If it matches I return the matched object with loading: true and if not I return the same object.
I am open to suggestions how can I refactor this solution though, I am quite new to immutable js and I feel there is much more simpler way to solve this but I don't know yet.

Resources