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
}}
Related
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"))
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
I'm doing a facebook clone, and everytime i press like's button, i want to see the change immediately, that's something that swr provides, but, it only updates after 4-8 seconds :/
What i tried to do is the following: when i click like's button, i first mutate the cache that swr provides, then i make the call to the API, then revalidate data to see if everything is right with the data, actually i console log the cache and it updates immediately, but it the UI doesn't and i don't know why
Let me give sou some context with my code
This is how my publication looks like ( inside pub it's the likes property )
export type theLikes = {
identifier: string;
};
export type theComment = {
_id?: string;
body: string;
name: string;
perfil?: string;
identifier: string;
createdAt: string;
likesComments?: theLikes[];
};
export interface Ipublication {
_id?: string;
body: string;
photo: string;
creator: {
name: string;
perfil?: string;
identifier: string;
};
likes?: theLikes[];
comments?: theComment[];
createdAt: string;
}
export type thePublication = {
data: Ipublication[];
};
This is where i'm asking for all publications with getStaticProps
const PublicationsHome = ({ data: allPubs }) => {
// All pubs
const { data: Publications }: thePublication = useSWR(
`${process.env.URL}/api/publication`,
{
initialData: allPubs,
revalidateOnFocus: false
}
);
return (
<>
{Publications ? (
<>
<PublicationsHomeHero>
{/* Create pub */}
<CreatePubs />
{/* Show pub */}
{Publications.map(publication => {
return <Pubs key={publication._id} publication={publication} />;
})}
</PublicationsHomeHero>
</>
) : (
<div></div>
)}
</>
);
};
export const getStaticProps: GetStaticProps = async () => {
const { data } = await axios.get(`${process.env.URL}/api/publication`);
return {
props: data
};
};
export default PublicationsHome;
This is where the like button is ( focus on LikePub, there is where the logic is )
The conditional is simple, if user already liked a pub, cut the like, otherwise, like the pub
interface IlikesCommentsProps {
publication: Ipublication;
}
const LikesComments: React.FC<IlikesCommentsProps> = ({ publication }) => {
const LikePub = async (): Promise<void> => {
try {
if (publication.likes.find(f => f.identifier === userAuth.user.id)) {
mutate(
`${process.env.URL}/api/publication`,
(allPublications: Ipublication[]) => {
const currentPub = allPublications.find(f => f === publication);
const deleteLike = currentPub.likes.findIndex(
f => f.identifier === userAuth.user.id
);
currentPub.likes.splice(deleteLike, 1);
const updatePub = allPublications.map(pub =>
pub._id === currentPub._id ? currentPub : pub
);
return updatePub;
},
false
);
} else {
mutate(
`${process.env.URL}/api/publication`,
(allPublications: Ipublication[]) => {
const currentPub = allPublications.find(f => f === publication);
currentPub.likes.push({ identifier: userAuth.user.id });
const updatePub = allPublications.map(pub =>
pub._id === currentPub._id ? currentPub : pub
);
return updatePub;
},
false
);
}
console.log(publication.likes);
await like({ identifier: userAuth.user.id }, publication._id);
mutate(`${process.env.URL}/api/publication`);
} catch (err) {
if (err) {
mutate(`${process.env.URL}/api/publication`);
}
}
};
return (
<motion.div
onClick={LikePub}
variants={Actions}
whileHover="whileHover"
whileTap="whileTap"
>
<motion.span>
<LikeIcon colorIcon="rgb(32, 120, 244)" />
</motion.span>
<LikeCommentText
separation="0.2rem"
colorText="rgb(32, 120, 244)"
>
Me gusta
</LikeCommentText>
</motion.div>
)
}
As you can see as well, i console.log publication's likes, and this is what happened
The identifier is added to the cache, meaning that user liked the pub, but the UI doesn't update, it takes 4 - 7 seconds to update, probably even more, the same thing happened with removing the like, look at this
Cut the like, but, the UI doesn't update
I'm desperate, i've tried everything, i've been trying to fix this for almost a week, but found nothing, what am i doing wrong, is this a bug ?
I believe the problem is you're directly mutating (in the javascript not swr sense) swr's data that is completely invisible to swr. And only when response is returned from the API your state is updated and that finally triggers swr's observers.
Here you may notice that currentPub.likes is an array (reference) inside currentPub object. You're directly mutating it (with splice) and then insert the same reference back into allPublications object. From swr's perspective the likes array didn't change. It still holds the same reference as before the mutation:
(allPublications: Ipublication[]) => {
const currentPub = allPublications.find(f => f === publication);
const deleteLike = currentPub.likes.findIndex(
f => f.identifier === userAuth.user.id
);
currentPub.likes.splice(deleteLike, 1);
const updatePub = allPublications.map(pub =>
pub._id === currentPub._id ? currentPub : pub
);
return updatePub;
}
Snippet to illustrate the behavior:
const allPublications = [{ attr: 'attr', likes: [1, 2, 3] }]
const currentPub = allPublications[0]
currentPub.likes.splice(1, 1)
const updatePub = allPublications.map((pub, idx) => idx === 0 ? currentPub : pub)
console.log(updatePub[0] === allPublications[0]) // true
console.log(updatePub[0].likes === allPublications[0].likes) // true. the reference stays the same
console.log(updatePub[0]) // while the object has changed
You should rewrite it to exlude direct mutation and always return changed references for changed objects/arrays. Something like:
(allPublications: Ipublication[]) => {
const currentPub = allPublications.find(f => f === publication);
const likes = currentPub.likes.filter( // filter creates new array (reference)
f => f.identifier !== userAuth.user.id
);
const updatePub = allPublications.map(pub => // map creates new array
pub._id === currentPub._id ? { ...currentPub, likes } : pub // {} creates new object (reference)
);
return updatePub;
}
const allPublications = [{ attr: 'attr', likes: [1, 2, 3] }]
const currentPub = allPublications[0]
const likes = currentPub.likes.filter((el) => el !== 2)
const updatePub = allPublications.map((pub, idx) =>
idx === 0 ? { ...currentPub, likes } : pub
)
console.log(updatePub[0] === allPublications[0]) // false
console.log(updatePub[0].likes === allPublications[0].likes) // false
console.log(updatePub[0])
And do the same for else branch mutation function.
suppose i am having an array of like
[
{ id: 1, name: "John" , selected: true}
{ id: 2, name: "Jim" , selected: false }
{ id: 3, name: "James" , selected: false }
]
I am having function which get id and on the basis of that id I want to change the selected property of object to true and other selected property object to false
here is my function what i have tried but its throwing error
const handleOnClick = (id) => {
const temps = state.findIndex((sub) => sub.id === id)
setState(prevState => ({
...prevState,
[prevState[temps].selected]: !prevState[temps].selected
}))
Use a .map() to iterate everything. You shouldn't change values within the array without recreating the tree down to the changed object. See below example.
Below you will:
map over every element
Spread their current values
Overwrite the selected depending on if the id is the newly "selected" id
I also supplied an additional function to update using the index of the item in the array instead of by id since your title is "how to update object using index in react state?", although your question also mentions changing by id. I would say go by id if you have the choice, array indices can change.
const App = () => {
const [state, setState] = React.useState([
{ id: 1, name: "John" , selected: true},
{ id: 2, name: "Jim" , selected: false },
{ id: 3, name: "James" , selected: false }
]);
// This takes the ID of the item
const handleOnClick = (id) => {
const newState = state.map(item => {
return {
...item,
selected: item.id === id
};
});
setState(newState);
};
// This takes the index of the item in the array
const handleOnClickByIndex = (indexToSelect) => {
const newState = state.map((item, idx) => {
return {
...item,
selected: indexToSelect === idx
};
});
setState(newState);
};
return (
<div>
{state.map((item, mapIndex) => (
<div key={item.id}>
<div>
<span>{item.name} is selected: {item.selected ? "yes" : "no"}</span>
{/* This will update by ID*/}
<button onClick={() => handleOnClick(item.id)}>Select using ID</button>
{/* This will update by the index of the array (supplied by map) */}
<button onClick={() => handleOnClickByIndex(mapIndex)}>Select using index</button>
</div>
</div>
))}
</div>
);
};
ReactDOM.render(
<App/>,
document.getElementById("app")
);
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="app"></div>
I have tree structure. What I needed is to uncheck the lover level of the tree when a level higher to it is checked.
But it is not working as I said above. What I have now is shown in the picture below
I clicked TPL
Clicked Accident Report(39)
3 During this time uncheck TPL
Code:
onCheck(checkedKeys) {
const {
FilterTaskList
} = this.props;
console.log(checkedKeys);
var checked2 = [];
if (checkedKeys.checked.length != 0) {
var addedkey = checkedKeys.checked[checkedKeys.checked.length - 1];
checked2 = _.filter(checkedKeys.checked, (o) => o.substring(0, addedkey.length + 1) != addedkey + "-");
}
checkedKeys.checked = checked2;
this.setState({
checkedKeys,
ischecked: true
});
let selectedLevel = 0;
let leveldata = [];
var checked = checkedKeys.checked;
const data = [];
const dataremove = [];
const AllLevel = [];
checked && checked.forEach((value) => {
var keys = value.split("-");
var task;
if (keys.length == 1) {
task = FilterTaskList[keys[0]];
data.push({
'TID': task.TID,
'TeamID': this.props.TeamId,
'RefID': task.REfID,
'FinClass': '',
'TLID': task.TLID,
'SelectionLevel': 2,
'SubTeamID': task.STID,
'Type': task.Type
});
AllLevel.push(2);
}
if (keys.length == 2) {
task = FilterTaskList[keys[0]].Chidrens[keys[1]];
data.push({
'TID': task.TID,
'TeamID': this.props.TeamId,
'RefID': task.REfID,
'FinClass': task.FinClass,
'TLID': task.TLID,
'SelectionLevel': 3,
'SubTeamID': task.STID,
'Type': task.Type
});
AllLevel.push(3);
}
if (keys.length == 3) {
task = FilterTaskList[keys[0]].Chidrens[keys[1]].Chidrens[keys[2]];
data.push({
'TID': task.TID,
'TeamID': this.props.TeamId,
'RefID': task.REfID,
'FinClass': task.FinClass,
'TLID': task.TLID,
'SelectionLevel': 4,
'SubTeamID': task.STID,
'Type': task.Type
});
AllLevel.push(4);
}
if (keys.length == 4) {
task = FilterTaskList[keys[0]].Chidrens[keys[1]].Chidrens[keys[2]].Chidrens[keys[3]];
data.push({
'TID': task.TID,
'TeamID': this.props.TeamId,
'RefID': task.REfID,
'FinClass': task.FinClass,
'TLID': task.TLID,
'SelectionLevel': 5,
'SubTeamID': task.STID,
'Type': task.Type
});
AllLevel.push(5);
}
The Id's of the tree as follows(backend)(Eg:):
2
2-0
2-0-0
To handle children's state from parent is basically react anti-pattern. You can solve it by registering children refs in parent and touch its checked status via refs. But at the and of the day, I always end up using redux in these cases, it is much cleaner.
Store the whole tree in global state and on firing uncheck action on parent the reducer handle the children's checked state based on parent id.
Meanwhile all the lines are connected components watching their own state based on their ids.
const reducer = (state, action) => {
let isUnchecking;
switch (action.type) {
case 'TOGGLE': return state.map(line => {
if (line.id === action.payload.id) {
isUnchecking = line.checked;
return { ...line, checked: !line.checked }
}
if (line.parent == action.payload.id && isUnchecking) {
return { ...line, checked: false }
}
return line;
});
default: return state;
}
}
const initialState = [
{ id: 1, checked: true, parent: null },
{ id: 2, checked: true, parent: 1 },
{ id: 3, checked: false, parent: 1 },
{ id: 4, checked: false, parent: null }
]
const store = Redux.createStore(reducer, initialState);
const Line = ({ checked, onClick, label, children }) => (
<li onClick={onClick}>
<span>{checked ? '[x] ' : '[_] '}</span>
{label}
<ul>{children}</ul>
</li>
)
const mapStateToProps = state => ({ lines: state })
const LineContainer = ReactRedux.connect(mapStateToProps)(props => {
const { id } = props;
return (
<Line
{...props}
onClick={e => {props.dispatch({ type: 'TOGGLE', payload: { id } }); e.stopPropagation();}}
checked={props.lines.find(line => line.id === id).checked}
/>
)
}
)
class App extends React.Component {
render(){
return (
<ReactRedux.Provider store={store}>
<LineContainer id={1} label="Accident Report">
<LineContainer id={2} label="TPL" />
<LineContainer id={3} label="WC" />
</LineContainer>
<LineContainer id={4} label="Administrative Supervisor" />
</ReactRedux.Provider>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.1/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/6.0.1/react-redux.js"></script>
<div id="root"></div>