React nested items rerender not working as expected - reactjs

im trying to develop component that will sort new items, depending if item parent is or is not present on the list already. Components can be nested in each other to unlimited depth. Parent have list of children, children have parentId. Now, it works as expected at the first render, but when new item appear on the list (its added by user, using form, up in the structure), it does in fact make its way to components list, but is not shown on the screen until page reload. I can see temporary list that is used to make all calculations have the item as expected in the nested structure. Then i set state list to value of temp, but its not working, and i dont know why. Im quite new to react stuff. In act of desperation i even tried to destructure root parent of the item, hoping it will force rerender, but that didnt worked too. Anybody could help with this?
http://jsfiddle.net/zkfj03um/13/
import React, { useState } from 'react';
function Component(props) {
const [component, setComponent] = useState(props.component);
return (
<div>
{component.id};
{component.name};
<ul>
{component.subcomps && component.subcomps.map((comp) =>
<li key={comp.id} style={{ textAlign: 'left' }}>
<Component component={comp}
id={comp.id}
name={comp.name}
parentId={comp.parentId}
subcomps={comp.subcomps}
/>
</li>)}
</ul>
</div>
);
}
function ComponentsList(props) {
const newComponents = props.newComponents;
const [filteredComponents, setFilteredComponents] = useState();
function deepSearch(collection, key, value, path=[]) {
for (const o of collection) {
for (const [k, v] of Object.entries(o)) {
if (k === key && v === value) {
return {path: path.concat(o), object: o};
}
if (Array.isArray(v)) {
const _o = deepSearch(v, key, value, path.concat(o));
if (_o) {
return _o;
}
}
}
}
}
async function filter() {
let temp = [];
await newComponents.forEach((comp) => {
//parent may be, or may not be on the list. Its not necesary
const parentTuple = deepSearch(filteredComponents, 'id', comp.parentId);
if (!parentTuple) {
//create parent substitute logic
} else {
const parent = parentTuple.object;
const root = parentTuple.path[0];
const mutReplies = [comm, ...parent.replies];
parent.replies = mutReplies;
temp = [{...root}, ...temp]
}
})
setFilteredComponents([...temp])
}
useEffect(() => {
setLoading(false);
}, [filteredComponents]);
useEffect(() => {
setLoading(true);
filter();
}, [newComponents]);
return (<>
{!loading && filteredComponents.map((component, index) =>
<li key={index}>
<Component component={component} />
</li>
)}
</>);
}
const items = [
{ id: 1, name: 'sample1', subcomps: [{ id: 5, name: 'subcomp1', parentId: 1, subcomps: [] }] },
{
id: 2, name: 'sample2', subcomps: [
{ id: 6, name: 'subcomp2', subcomps: [], parentId: 2 },
{ id: 7, name: 'subcomp3', subcomps: [], parentId: 2 }
]
},
]
ReactDOM.render(<ComponentsList newComponents={items} />, document.querySelector("#app"))

Related

How to update object in array of objects

I have following part of React code:
This is handler for adding new object item to the array.
So I am checking if object exist in whole array of objects. If yes I want to increase quantity of this object by 1. Else I want to add object and set quantity to 1.
I am getting error when I want to add 2nd same product (same id)
Uncaught TypeError: Cannot assign to read only property 'quantity' of object '#<Object>'
export const Component = ({ book }: { book: Book }) => {
const [basket, setBasket] = useRecoilState(Basket);
const handleAddBookToBasket = () => {
const find = basket.findIndex((product: any) => product.id === book.id)
if (find !== -1) {
setBasket((basket: any) => [...basket, basket[find].quantity = basket[find].quantity + 1])
} else {
setBasket((basket: any) => [...basket, { ...book, quantity: 1 }])
}
}
EDIT:
if (find !== -1) {
setBasket((basket: any) =>
basket.map((product: any) => ({
...product,
quantity: product.quantity + 1,
}))
);
} else {
setBasket((basket: any) => [...basket, { ...book, quantity: 1 }]);
}
data structures
I'd say the root of your "problem" is that you chose the wrong data structure for your basket. Using Array<Item> means each time you need to update a specific item, you have to perform O(n) linear search.
If you change the basket to { [ItemId]: Item } you can perform instant O(1) updates. See this complete example below. Click on some products to add them to your basket. Check the updated basket quantity in the output.
function App({ products = [] }) {
const [basket, setBasket] = React.useState({})
function addToBasket(product) {
return event => setBasket({
...basket,
[product.id]: {
...product,
quantity: basket[product.id] == null
? 1
: basket[product.id].quantity + 1
}
})
}
return <div>
{products.map((p, key) =>
<button key={key} onClick={addToBasket(p)} children={p.name} />
)}
<pre>{JSON.stringify(basket, null, 2)}</pre>
</div>
}
const products = [
{ id: 1, name: "ginger" },
{ id: 2, name: "garlic" },
{ id: 3, name: "turmeric" }
]
ReactDOM.render(<App products={products}/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
object update
As a good practice, you can make a function for immutable object updates.
// Obj.js
const update = (o, key, func) =>
({ ...o, [key]: func(o[key]) })
export { update }
Then import it where this behavior is needed. Notice it's possible to use on nested object updates as well.
// App.js
import * as Obj from "./Obj"
function App({ products = [] }) {
// ...
function addToBasket(product) {
return event => setBasket(
Obj.update(basket, product.id, (p = product) => // ✅
Obj.update(p, "quantity", (n = 0) => n + 1) // ✅
)
)
}
// ...
}
object remove
You can use a similar technique to remove an item from the basket. Instead of coupling the behavior directly in the component that needs removal, add it to the Obj module.
// Obj.js
const update = (o, key, func) =>
({ ...o, [key]: func(o[key]) })
const remove: (o, key) => { // ✅
const r = { ...o }
delete r[key]
return r
}
export { update, remove } // ✅
Now you can import the remove behaviour in any component that needs it.
function App() {
const [basket, setBasket] = React.useState({
1: { id: 1, name: "ginger", quantity: 5 },
2: { id: 2, name: "garlic", quantity: 6 },
3: { id: 3, name: "tumeric", quantity: 7 },
})
function removeFromBasket(product) {
return event => {
setBasket(
Obj.remove(basket, product.id) // ✅
)
}
}
return <div>
{Object.values(basket).map((p, key) =>
<div key={key}>
{p.name} ({p.quantity})
<button onClick={removeFromBasket(p)} children="❌" />
</div>
)}
<pre>{JSON.stringify(basket, null, 2)}</pre>
</div>
}
// inline module for demo
const Obj = {
remove: (o, key) => {
const r = {...o}
delete r[key]
return r
}
}
ReactDOM.render(<App />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
In your setBasket your should create a new object instead of updating current one
your code should look like these one :
{...basket, ...{
quantity : basket.quantity + 1
}}

Replacing value in nested state with react functional components

I am trying to use update state in a react function component but it is not working. I tried following a tutorial on pluralsite and apply it to my own project. Ideally this code should be finding the product based on the ID number and replacing the total with a new value.
Unfortunately I am getting an error saying that 'productData.find' is not a function and I'm not sure where the code being used for that is. Are there any suggestions on how to solve this issue?
This is what the data looks like. In this example I am saving the first element of the array.
export let data = [
{
name: "Name",
description:
"",
products: [
{
id: 1,
name: "Name 1",
material: 1.05,
time: 25,
total: 0,
},
{
id: 2,
name: "Name 2",
material: 3,
time: 252,
total: 0,
},
],
},
...
];
function CompareCard({}) {
const index = 0;
const [productData, setProductData] = useState(data[index]);
function setTotalUpdate(id) {
const productPrevious = productData.find(function (rec) {
return rec.id === id;
});
const productUpdated = {
...productPrevious,
total: 1,
};
const productNew = productData.map(function (rec) {
return rec.id === id ? productUpdated : rec;
});
setProductData(productNew);
}
setTotalUpdate(1)
}
It's because productData is not an array so .find would not work. You want iterate over the products property in your data, so do productData.products.find(...)
When you do
const [productData, setProductData] = useState(data[index])
you don't pass an Array on your state but an Object (the first element of your data so an Object) and Object don't have find method.
Try
const [productData, setProductData] = useState([data[index]])
with [] on our useState to put your Object on array
///////////////////////////////// Edit /////////////
Ok, I try your code, and I propose you this.
import React, { useState } from "react";
const data = [
{
name: "Name",
description: "",
products: [
{
id: 1,
name: "Name 1",
material: 1.05,
time: 25,
total: 0,
},
{
id: 2,
name: "Name 2",
material: 3,
time: 252,
total: 0,
},
],
},
];
const CompareCard = () => {
// setState with the subArray products from data[0], I use '...' (spread operator) inside a Array or an Object to make a shalow copy
const [productsData, setProductsData] = useState([...data[0].products]);
const setTotalUpdate = (id) => {
// find the product to change inside products collection, that's ok
const productPrevious = productsData.find((rec) => {
return rec.id === id;
});
// create a new product to change one property's value, you can do something like 'productPrevious.total = 1', same same
const productUpdated = {
...productPrevious,
total: 1,
};
// create a new products collection to update state
const productNew = productsData.map((rec) => {
return rec.id === id ? productUpdated : rec;
});
setProductsData([...productNew]);
};
const setTotalUpdateSecond = (id) => {
// create a newState
const newState = productsData.map((product) => {
// condition if id === productId and do something
if (id === product.id) {
product.total = 1;
}
// both case, I return the product (change or not)
return product;
});
setProductsData([...newState]);
};
return (
<>
<button onClick={() => setTotalUpdate(1)}>Test old method on product 1</button>
<button onClick={() => setTotalUpdateSecond(2)}>Test second method on product 2</button>
{productsData.map((product) => {
return (
<>
<p>Product Id : {product.id} / Product Total : {product.total}</p>
</>
);
})}
</>
);
};
export default CompareCard;
Can you copy / past this, try and say me if it's what you want, if yes, I explain you where the confusion was. If not, explain me, what's the problem here and I modificate.

Setting value to an state of array of interface doesn't work, instead it's following the amount of { } in the [ ] in React Typescript

So, i want to fetch data from API then putting the data on my DataTable. The thing i did is using a state as a variable so it can be dynamically changed any time. The problem is when i put empty array in the use state and calling the set function, it won't work, here is the example of the code :
const [moduleRows, setModuleRows] = React.useState([] as ModuleRow[]);
useEffect(() => {
function getModule():Promise<Module[]> {
return fetch('http://localhost:7071/api/module/').then(res => res.json()).then(res => {
return res as Module[]
})
}
getModule().then(module => {
let tempModuleRows:ModuleRow[] = []
for (let index = 0; index < module.length; index++) {
tempModuleRows.push({
no: index+1,
module_name: module[index].moduleName,
category: "test",
actions: <div style={{display: "flex"}}>
<DeleteActionButton buttonLabel="Delete" onClick={() => changeModuleId("delete", 1)}/>
<UpdateActionButton buttonLabel="Update" onClick={() => changeModuleId("update", 1)} />
</div>
})
}
setModuleRows([...tempModuleRows])
})
}, [])
when i try to access one of the element in the moduleRows, it's undefined
but when i try to change the declaration of the state to :
const [moduleRows, setModuleRows] = React.useState([{}] as ModuleRow[]);
i can access index 0 of moduleRows, but i can't access index 1 and so on, then i try adding another { } in the array bracket, so my initialization looks like :
const [moduleRows, setModuleRows] = React.useState([{}, {}] as ModuleRow[]);
now i can access index 0 and 1, but i can't access 2 and so on, but when i checking the length of moduleRows, it's exactly the same as the data fetched from the API
so i want to know why the empty array in the useState doesn't work and why using { } in the [ ] makes the program to read only as much as the { }
I can't reproduce the bug you are talking about. Here is a working codesandbox.io demo.
import { ReactNode, useEffect, useState } from "react";
import { sleep } from "sleepjs";
type ModuleRow = {
no: number;
module_name: string;
category: string;
actions: ReactNode;
};
type Module = {
moduleName: string;
};
const getModule: () => Promise<Module[]> = () => {
const array: Module[] = [];
for (let i = 0; i < 10; ++i) {
array.push({
moduleName: `Module ${i + 1}`
});
}
return new Promise(async (res) => {
await sleep(1000);
return res(array);
});
};
export default function App() {
const [moduleRows, setModuleRows] = useState<ModuleRow[]>([]);
useEffect(() => {
getModule().then((module: Module[]) => {
let tempModuleRows: ModuleRow[] = [];
for (let index = 0; index < module.length; index++) {
tempModuleRows.push({
no: index + 1,
module_name: module[index].moduleName,
category: "test",
actions: (
<div key={index} style={{ display: "flex" }}>
{module[index].moduleName}
<button>Update</button>
<button>Delete</button>
</div>
)
});
}
setModuleRows([...tempModuleRows]);
});
}, []);
if (!moduleRows.length) return <div>Loading...</div>;
return <>{moduleRows.map((m) => m.actions)}</>;
}
problem solved, the problem is not how i initialize my state, but how i do access the state, i can access the state by using the map but when im call the state using index ([0], [1]) it does give an error

Array data from Graphql is not populating a component in React

I am trying to render multiple checkboxes that have two functions:
Show two states: checked and unchecked
Update a checked array with the checked checkboxes
Currently, I am successfully able to accomplish these two goals with dummy data:
const dummyPlayers = [
{ id: 1, name: 'Puppa' },
{ id: 2, name: 'Korvo' },
{ id: 3, name: 'Jesse' },
{ id: 4, name: 'Terry' },
{ id: 5, name: 'Gobblins' }
]
This is the shape of the array I want to populate the checkboxes with:
[
{
"id": "936d6050-00df-4bd4-bc54-6ce58ad0210c",
"name": "Travis",
"owner": "moralesfam",
"type": "Member",
"createdAt": "2021-09-24T20:08:02.292Z",
"updatedAt": "2021-09-24T20:08:02.292Z"
}...
]
However, when I start pulling data in from a database with Graphql, while I am able to render the checkboxes to the DOM, they are not interactive (don't show checked state) and don't log the checked checkboxes.
I bring in the data, an array of objects through a custom React hook, called useMembers and the data is stored in a members array. Console logging members prints out the array, but as soon as I swap the dummyPlayers for the members array, the two goals I stated earlier are unsuccessful.
// RecordGame.js
import React, { useState } from 'react'
import useLoadMembers from '../hooks/useLoadMembers'
import useUser from '../hooks/useUser'
function RecordGame() {
const dummyPlayers = [
{ id: 1, name: 'Puppa' },
{ id: 2, name: 'Korvo' },
{ id: 3, name: 'Jesse' },
{ id: 4, name: 'Terry' },
{ id: 5, name: 'Gobblins' },
]
const { members } = useLoadMembers(updateLoading)
const { user } = useUser()
const [checkedState, setCheckedState] = useState(
new Array(members.length).fill(false)
)
let playingPlayers = []
for (var index in checkedState) {
if (checkedState[index] === true) {
playingPlayers.push(dummyPlayers[index])
}
}
console.log(playingPlayers)
const handleOnChange = (position) => {
const updatedCheckedState = checkedState.map((player, index) =>
index === position ? !player : player
)
setCheckedState(updatedCheckedState)
}
// Rendered elements
const playerCheckboxes = dummyPlayers.map((player, index) => {
return (
<div key={index}>
<label htmlFor={player.name}>
<input
type="checkbox"
id={player.name}
name={player.name}
checked={checkedState[index]}
onChange={() => handleOnChange(index)}
/>
<span> {player.name}</span>
</label>
</div>
)
})
return (
<div>
<form>
{/* Game Players */}
<div>
<label htmlFor="players">
Who Played?
</label>
<div>{playerCheckboxes}</div>
</div>
</form>
</div>
)}
</Dashboard>
)
}
export default RecordGame
//useLoadMember.js
import { useState, useEffect } from 'react'
import { API, Auth } from 'aws-amplify'
import { listMembers } from '../graphql/queries'
const useLoadMembers = (updateLoading) => {
const [members, updateMembers] = useState([])
useEffect(() => {
fetchMembers()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const fetchMembers = async () => {
try {
let memberData = await API.graphql({
query: listMembers,
variables: { limit: 100 },
})
updateLoading(false)
let allMembers = memberData.data.listMembers.items
setFilteredMembers(allMembers)
} catch (err) {
console.error(err)
}
}
const setFilteredMembers = async (allMembers) => {
const { username } = await Auth.currentAuthenticatedUser()
const myMemberData = allMembers.filter((p) => p.owner === username)
updateMembers(myMemberData)
}
return { members }
}
export default useLoadMembers
In this first, picture, I used the dummyPlayers array and got the results I wanted.
However, in this second screenshot, I replaced the dummyData with the members array and did not get any results I wanted.
I'm just confused on why I am getting different results with the same array shape.
wherever you use members you will need to check that members is not undefined before it gets used. If the api calls are not complete, it will get set initially as undefined.
eg: members && members.map(() => ....)

Update item in state onClick ReactJS

So, I have class Comopnent :
state = {
tokens: [
{
name: "first",
value: 3
},
{
name: "second",
value: 2
},
{
name: "third",
value: 4
}
]
}
handleClick = (name, id) => {
const newState = this.state.tokens.map((token => {
console.log(token.name)
}))
}
render() {
const token = this.state.tokens;
const tokenList = token.map(t => {
return (
<div value={t.name} onClick={() => this.handleClick(t.name, t.value)}>
<img src=""/>
</div>
)
})
What i need to do - after click - to subtract 1 from value clicked token.
So - ex. after click on "First" token i want his value equal 2.
So far I've done just as much as the above.
I do not know how to go about it, i am new in ReactJS, so thanks for help in advance!
You'll have to find in your state in tokens array the object which has the same name as the argument passed in the onclick handler. Then you will have to change it's value - decrement it (value--) but you have to be aware that you can't mutate the state.
handleClick = name => () => {
const { tokens } = this.state;
const clickedToken = tokens.find(token => token.name === name);
clickedToken.value--;
const clickedTokenIndex = tokens.indexOf(clickedToken);
const newTokens = [
...tokens.slice(0, clickedTokenIndex),
clickedToken,
...tokens.slice(clickedTokenIndex + 1)
];
this.setState({ tokens: newTokens });
};
Codesandbox link: https://codesandbox.io/s/92yz34x97w
First, some things are wrong with your code.
1- You have an array of tokens, then you're mapping the list, but you don't have a key to index, this will cause weird behaviors, I improve your tokens list with keys now.
2.- You can handle the click and change the state of the tokens list, this will trigger a reload of the component.
state = {
tokens: [
{
name: "first",
value: 3,
id: 1
},
{
name: "second",
value: 2,
id: 2
},
{
name: "third",
value: 4,
id: 3
}
]
}
handleClick = (name, id) => {
const { tokens} = this.state;
const newState = tokens.map((token => {
if(token.id === id) {
token.value--;
}
return token;
}))
}
render() {
const token = this.state.tokens;
const tokenList = token.map(t => {
return (
<div key={t.key} value={t.name} onClick={() => this.handleClick(t.name, t.value, t.key)}>
<img src=""/>
</div>
)
})

Resources