ReactJS Add to Object that is a State - javascript

hoping this is a simple answer but it's racking my brain. Let's say I have the following React pseudocode:
const [cats, setCats] = useState({});
console.log(cats);
cats = {
}
I am trying to add uh cats to the cats object via a button click. I'm not sure what the exact code should be, as any guess I have so far doesn't work. The closest i've gotten is the following pseudo code:
heres the object i'm sending in:
{
Bruto:{
id : 2,
name: 'Bruto',
}
}
const addCat (e, catName) => {
setCats(...cats, event.target.value);
}
<input onKeyDown={e=>addCat(e,catDetails)}></input>
Which all it does is just add the word typed into the input as cats, but doesn't add it structurally. Ideally what i'd want is the following:
1. Check to see if catDetails exists in cats
2. If not, add the catDetails.id to the object
3. Add catDetails.name to cats[catDetails.id] such as the object should be:
cats{
0: {
name: "Heathcliff"
}
2: {
name: "Bruto"
}
}
Hoping someone can help. Below is the code I have so far
const addTag = (event, cats) => {
if (event.key === "Enter"){
setCats(...cats, event.target.value)
}
console.log(cats);
}
Thank you!

You'll probably find an array easier to use.
Have one state (an array) for all your cat objects, and have another (string) for the cat you're adding in the input, and then update the cats array with the current cat if it's not been found.
const { useEffect, useState } = React;
function Example() {
// Initialise cat states
const [ cats, setCats ] = useState([]);
const [currentCat, setCurrentCat] = useState('');
// Updates the currentCat state when the input changes
function handleChange(e) {
setCurrentCat(e.target.value);
}
// First find out if the cat name we're entering exists
// in the cats array. If it doesn't, add it
function addCat(e) {
const found = cats.find(cat => cat.name === currentCat);
if (!found) {
setCats([
...cats,
{ id: cats.length + 1, name: currentCat }
]);
}
}
useEffect(() => console.log(cats), [cats]);
return (
<div>
<input
type="text"
onChange={handleChange}
/>
<button
type="button"
onClick={addCat}
>Add cat
</button>
</div>
);
};
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

You can do:
setCats( prevCats => {...prevCats, {id:2, name:"Bruto"});
Or if you want the IDs as keys:
setCats( prevCats => {...prevCats, [id]: name});
Instead of passing the new state to setCats you pass a function which will automatically receive the previous state, and whatever is returned will be used as the new state.
Or even better: you can use useReducer instead of useState

Related

How can I update state as an array of objects without overriding previous state

I want to update setTopic without overriding previous state. But I am getting topic is not iterable error.
What I tried?
I tried looking a different examples on stack overflow, But still couldn't figure out how to append updated state without losing previous state.
Also, which is a better way to save multiple topics : an array of objects, simply objects or simply array?
const AddTopic = (props) => {
const { subjectName } = props;
const [topic, setTopic] = useState([
{
topics: [
{
id: Math.random().toString(36).substr(2, 7),
topicName: "topic name",
subject: subjectName,
},
],
},
]);
const addTopicHandler = () => {
setTopic(
[...topic].map((item) => {
return {
...item,
id: Math.random().toString(36).substr(2, 7),
topicName: "another topic name",
subject: subjectName,
};
})
);
};
console.log(topic);
Instead of the child component using state lift the state to a parent component, and then just create dumb Topic components from the state.
Rename your state. Call it topics, and the update function setTopics. Initialise it as an array, not an array containing one object containing an array.
You can't immediately log an updated state. You need to use useEffect to watch for changes in state, and then log something.
const { useEffect, useState } = React;
// Topic component - just gets handed the
// subject (in this example) in the props
function Topic({ subject, category }) {
return <div>{subject}: {category}</div>;
}
function Example() {
// Initialise `topics` as an array
const [ topics, setTopics ] = useState([]);
// When `topics` is updated, log the updated state
useEffect(() => console.log(JSON.stringify(topics)), [topics]);
// Helper function that maps over the state and
// produces an array of topics
function getTopics() {
return topics.map(topic => {
const { subject, category } = topic;
return (
<Topic
subject={subject}
category={category}
/>
);
});
}
// Helper function to add a new topic object
// to the topics state
function addTopic() {
const obj = { subject: 'Math', category: 'Fish' };
setTopics([...topics, obj ]);
}
return (
<div>
<div>{getTopics()}</div>
<button onClick={addTopic}>Add topic</button>
</div>
);
};
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Try to replace [...topic].map with [...topic.topics].map.
edit
Sorry, I didn't see that the topic itself is an arr of objs, so it should be: [...topic[0].topics].map.

Why I can't delete a user object this way?

Hello Everyone !
I'm new to ReactJS, and I'm trying to do some simple projects to get the basics.
I started a little project consisting at adding and deleting user of a list with React Hooks (manipulating the state).
I can properly add a new User to my Userlist and display it, but when it comes to delete a user, nothing happened.
I found the solution, but I can't explain it, that's why I'm asking for your help !
Here is my App.js file with the DeleteUser function that works
import style from './App.module.css';
import React, { useState } from 'react';
import UserList from './components/UserList';
import UserForm from './components/UserForm'
let USERS = [
{
id: 1,
name: 'John',
age: 27
},
{
id: 2,
name: 'Mark',
age: 24
}
]
const App = () => {
const [userss, SetUsers] = useState(USERS);
const AddNewUser = (user) => {
SetUsers((prevList) => {
let updatedList = [...prevList];
updatedList.unshift(user);
return updatedList;
});
};
const DeleteUser = user => {
SetUsers((prevList) => {
let updated = prevList.filter(el => el.id !== user.id);
return updated;
});
};
return (
<div className={style.root}>
<UserForm liftUpNewUser={AddNewUser} />
<UserList users={userss} liftUpUserToDelete={DeleteUser} />
</div>
);
}
export default App;
My Question is:
Why does the DeleteUser function writtten this way (below) doesn't work ? Knowing that it is the same logic as the AddNewUser function.
const DeleteUser = user => {
SetUsers((prevList) => {
let updated = [...prevList];
updated.filter(el => el.id !== user.id);
return updated;
});
};
Sorry in advance for my english!
Hope someone can help me =)
This line in your code...
updated.filter(el => el.id !== user.id);
... is a no-op, as value of updated array never gets changed. filter returns a new array instead, and this new array gets assigned to a variable in the first snippet.
The side effect of this is that React won't have to compare those arrays by value: their references will be different. It wouldn't have been the case if filter worked the way you expected it to work, making the changes in-place, similar to Array.splice.
Because Array.filter method does not modify the original array but returns a new one.

How do I create a new JSON object inside a react hook?

I have two issues first how do I add/update the JSON items within a hook?
The other being that React won't let me use the name stored from a previous JSON file.
I am open to other solutions, basically, as my input field are dynamically generated from a JSON file I'm unsure of the best way to store or access the data that's input into them I think storing them in a react hook as JSON and then passing them though as props to another component is probably best.
What I want to happen is onChange I would like the quantity value to be stored as a JSON object in a Hook here's my code:
React:
import React, { useState, useEffect } from 'react';
import Data from '../shoppingData/Ingredients';
import Quantities from '../shoppingData/Quantities';
const ShoppingPageOne = (props) => {
//element displays
const [pageone_show, setPageone_show] = useState('pageOne');
//where I want to store the JSON data
const [Quantities, setQuantities] = useState({});
useEffect(() => {
//sets info text using Json
if (props.showOne) {
setPageone_show('pageOne');
} else {
setPageone_show('pageOne hide');
}
}, [props.showOne]);
return (
<div className={'Shopping_Content ' + pageone_show}>
//generates input fields from JSON data
{Data.map((Ingredients) => {
const handleChange = (event) => {
// this is where I'd like the Hook to be updated to contain instances of the ingredients name and quantity of each
setQuantities(
(Ingredients.Name: { ['quantities']: event.target.value })
);
console.log(Quantities);
};
return (
<div className="Shopping_input" key={Ingredients.Name}>
<p>
{Ingredients.Name} £{Ingredients.Price}
</p>
<input
onChange={handleChange.bind(this)}
min="0"
type="number"
></input>
</div>
);
})}
<div className="Shopping_Buttons">
<p onClick={props.next_ClickHandler}>Buy Now!</p>
</div>
</div>
);
};
export default ShoppingPageOne;
JSON file:
//Json data for the shopping ingredients
export default [
{
Name: 'Bread',
Price: "1.10",
},
{
Name: 'Milk',
Price: "0.50",
},
{
Name: 'Cheese',
Price: "0.90",
},
{
Name: 'Soup',
Price: "0.60",
},
{
Name: 'Butter',
Price: "1.20",
}
]
Assuming your Quantities object is meant to look like:
{
<Ingredient Name>: { quantities: <value> }
}
you need to change your handleChange to look like this
const handleChange = (event) => {
setQuantities({
...Quantities,
[Ingredients.Name]: {
...(Quantities[Ingredients.Name] ?? {}),
quantities: event.target.value
}
});
};
Explanation
When updating state in React, it is important to replace objects rather than mutating existing ones, as this is what tells React to rerender components. This is commonly done using the spread operator, and with array functions such as map and filter. For example:
const myObject = { test: 1 };
myObject.test = 2; // Mutates existing object, wrong!
const myNewObject = { ...myObject, test: 2 }; // Creates new object, good!
Note the spread operator doesn't operate below the first level, what I mean by that is, objects within the object will be copied by reference, for example:
const myObject = { test : { nested: 1 } };
const myObject2 = { ...myObject };
myObject2.test.nested = 2;
console.log(myObject.test.nested); // outputs 2
Also in my answer, I have used the nullish coalescing operator (??), this will return it's right operand if the left operand is null or undefined, for example:
null ?? 'hello'; // resolves to "hello"
undefined ?? 'world'; // resolves to "world"
"foo" ?? "bar"; // resolves to "foo"
In my answer I used it to fallback to an empty object if Quantities[Ingredients.Name] is undefined.
Finally, I used square brackets when using a variable as an object key as this causes the expression to be evaluated before being used as a key:
const myKey = 'hello';
const myObject = {
[myKey]: 'world';
};
console.log(myObject); // { hello: 'world' }

use object in useEffect 2nd param without having to stringify it to JSON

In JS two objects are not equals.
const a = {}, b = {};
console.log(a === b);
So I can't use an object in useEffect (React hooks) as a second parameter since it will always be considered as false (so it will re-render):
function MyComponent() {
// ...
useEffect(() => {
// do something
}, [myObject]) // <- this is the object that can change.
}
Doing this (code above), results in running effect everytime the component re-render, because object is considered not equal each time.
I can "hack" this by passing the object as a JSON stringified value, but it's a bit dirty IMO:
function MyComponent() {
// ...
useEffect(() => {
// do something
}, [JSON.stringify(myObject)]) // <- yuck
Is there a better way to do this and avoid unwanted calls of the effect?
Side note: the object has nested properties. The effects has to run on every change inside this object.
You could create a custom hook that keeps track of the previous dependency array in a ref and compares the objects with e.g. Lodash isEqual and only runs the provided function if they are not equal.
Example
const { useState, useEffect, useRef } = React;
const { isEqual } = _;
function useDeepEffect(fn, deps) {
const isFirst = useRef(true);
const prevDeps = useRef(deps);
useEffect(() => {
const isFirstEffect = isFirst.current;
const isSame = prevDeps.current.every((obj, index) =>
isEqual(obj, deps[index])
);
isFirst.current = false;
prevDeps.current = deps;
if (isFirstEffect || !isSame) {
return fn();
}
}, deps);
}
function App() {
const [state, setState] = useState({ foo: "foo" });
useEffect(() => {
setTimeout(() => setState({ foo: "foo" }), 1000);
setTimeout(() => setState({ foo: "bar" }), 2000);
}, []);
useDeepEffect(() => {
console.log("State changed!");
}, [state]);
return <div>{JSON.stringify(state)}</div>;
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
The above answer by #Tholle is absolutely correct. I wrote a post regarding the same on dev.to
In React, side effects can be handled in functional components using useEffect hook. In this post, I'm going to talk about the dependency array which holds our props/state and specifically what happens in case there's an object in the dependency array.
The useEffect hook runs even if one element in the dependency array changes. React does this for optimisation purposes. On the other hand, if you pass an empty array then it never re-runs.
However, things become complicated if an object is present in this array. Then even if the object is modified, the hook won't re-run because it doesn't do deep object comparison between these dependency changes for that object. There are couple of ways to solve this problem.
Use lodash's isEqual method and usePrevious hook. This hook internally uses a ref object that holds a mutable current property that can hold values.
It’s possible that in the future React will provide a usePrevious Hook out of the box since it is a relatively common use case.
const prevDeeplyNestedObject = usePrevious(deeplyNestedObject)
useEffect(()=>{
if (
!_.isEqual(
prevDeeplyNestedObject,
deeplyNestedObject,
)
) {
// ...execute your code
}
},[deeplyNestedObject, prevDeeplyNestedObject])
Use useDeepCompareEffect hook as a drop-in replacement for useEffect hook for objects
import useDeepCompareEffect from 'use-deep-compare-effect'
...
useDeepCompareEffect(()=>{
// ...execute your code
}, [deeplyNestedObject])
Use useCustomCompareEffect hook which is similar to solution #2
I prepared a CodeSandbox example related to this post. Fork it and check it yourself.
Your best bet is to use useDeepCompareEffect from react-use. It's a drop-in replacement for useEffect.
const {useDeepCompareEffect} from "react-use";
const App = () => {
useDeepCompareEffect(() => {
// ...
}, [someObject]);
return (<>...</>);
};
export default App;
Plain (not nested) object in dependency array
I just want to challenge these two answers and to ask what happen if object in dependency array is not nested. If that is plain object without properties deeper then one level.
In my opinion in that case, useEffect functionality works without any additional checks.
I just want to write this, to learn and to explain better to myself if I'm wrong. Any suggestions, explanation is very welcome.
Here is maybe easier to check and play with example: https://codesandbox.io/s/usehooks-bt9j5?file=/src/App.js
const {useState, useEffect} = React;
function ChildApp({ person }) {
useEffect(() => {
console.log("useEffect ");
}, [person]);
console.log("Child");
return (
<div>
<hr />
<h2>Inside child</h2>
<div>{person.name}</div>
<div>{person.age}</div>
</div>
);
}
function App() {
const [person, setPerson] = useState({ name: "Bobi", age: 29 });
const [car, setCar] = useState("Volvo");
function handleChange(e) {
const variable = e.target.name;
setPerson({ ...person, [variable]: e.target.value });
}
function handleCarChange(e) {
setCar(e.target.value);
}
return (
<div className="App">
Name:
<input
name="name"
onChange={(e) => handleChange(e)}
value={person.name}
/>
<br />
Age:
<input name="age" onChange={(e) => handleChange(e)} value={person.age} />
<br />
Car: <input name="car" onChange={(e) => handleCarChange(e)} value={car} />
<ChildApp person={person} />
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-
dom.development.js"></script>
<div id="root"></div>
You can just expand the properties in the useEffect array:
var obj = {a: 1, b: 2};
useEffect(
() => {
//do something when any property inside "a" changes
},
Object.entries(obj).flat()
);
Object.entries(obj) returns an array of pairs ([["a", 1], ["b", 2]]) and .flat() flattens the array into:
["a", 1, "b", 2]
Note that the number of properties in the object must remain constant because the length of the array cannot change or else useEffect will throw an error.

Push method in React Hooks (useState)?

How to push element inside useState array React hook?
Is that as an old method in react state? Or something new?
E.g. setState push example ?
When you use useState, you can get an update method for the state item:
const [theArray, setTheArray] = useState(initialArray);
then, when you want to add a new element, you use that function and pass in the new array or a function that will create the new array. Normally the latter, since state updates are asynchronous and sometimes batched:
setTheArray(oldArray => [...oldArray, newElement]);
Sometimes you can get away without using that callback form, if you only update the array in handlers for certain specific user events like click (but not like mousemove):
setTheArray([...theArray, newElement]);
The events for which React ensures that rendering is flushed are the "discrete events" listed here.
Live Example (passing a callback into setTheArray):
const {useState, useCallback} = React;
function Example() {
const [theArray, setTheArray] = useState([]);
const addEntryClick = () => {
setTheArray(oldArray => [...oldArray, `Entry ${oldArray.length}`]);
};
return [
<input type="button" onClick={addEntryClick} value="Add" />,
<div>{theArray.map(entry =>
<div>{entry}</div>
)}
</div>
];
}
ReactDOM.render(
<Example />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
Because the only update to theArray in there is the one in a click event (one of the "discrete" events), I could get away with a direct update in addEntry:
const {useState, useCallback} = React;
function Example() {
const [theArray, setTheArray] = useState([]);
const addEntryClick = () => {
setTheArray([...theArray, `Entry ${theArray.length}`]);
};
return [
<input type="button" onClick={addEntryClick} value="Add" />,
<div>{theArray.map(entry =>
<div>{entry}</div>
)}
</div>
];
}
ReactDOM.render(
<Example />,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
To expand a little further,
here are some common examples. Starting with:
const [theArray, setTheArray] = useState(initialArray);
const [theObject, setTheObject] = useState(initialObject);
Push element at end of array
setTheArray(prevArray => [...prevArray, newValue])
Push/update element at end of object
setTheObject(prevState => ({ ...prevState, currentOrNewKey: newValue}));
Push/update element at end of array of objects
setTheArray(prevState => [...prevState, {currentOrNewKey: newValue}]);
Push element at end of object of arrays
let specificArrayInObject = theObject.array.slice();
specificArrayInObject.push(newValue);
const newObj = { ...theObject, [event.target.name]: specificArrayInObject };
theObject(newObj);
Here are some working examples too.
https://codesandbox.io/s/reacthooks-push-r991u
You can append array of Data at the end of custom state:
const [vehicleData, setVehicleData] = React.useState<any[]>([]);
setVehicleData(old => [...old, ...newArrayData]);
For example, In below, you appear an example of axios:
useEffect(() => {
const fetchData = async () => {
const result = await axios(
{
url: `http://localhost:4000/api/vehicle?page=${page + 1}&pageSize=10`,
method: 'get',
}
);
setVehicleData(old => [...old, ...result.data.data]);
};
fetchData();
}, [page]);
Most recommended method is using wrapper function and spread operator together. For example, if you have initialized a state called name like this,
const [names, setNames] = useState([])
You can push to this array like this,
setNames(names => [...names, newName])
Hope that helps.
// Save search term state to React Hooks with spread operator and wrapper function
// Using .concat(), no wrapper function (not recommended)
setSearches(searches.concat(query))
// Using .concat(), wrapper function (recommended)
setSearches(searches => searches.concat(query))
// Spread operator, no wrapper function (not recommended)
setSearches([...searches, query])
// Spread operator, wrapper function (recommended)
setSearches(searches => [...searches, query])
https://medium.com/javascript-in-plain-english/how-to-add-to-an-array-in-react-state-3d08ddb2e1dc
setTheArray([...theArray, newElement]); is the simplest answer but be careful for the mutation of items in theArray. Use deep cloning of array items.
I tried the above methods for pushing an object into an array of objects in useState but had the following error when using TypeScript:
Type 'TxBacklog[] | undefined' must have a 'Symbol.iterator' method that returns an iterator.ts(2488)
The setup for the tsconfig.json was apparently right:
{
"compilerOptions": {
"target": "es6",
"lib": [
"dom",
"dom.iterable",
"esnext",
"es6",
],
This workaround solved the problem (my sample code):
Interface:
interface TxBacklog {
status: string,
txHash: string,
}
State variable:
const [txBacklog, setTxBacklog] = React.useState<TxBacklog[]>();
Push new object into array:
// Define new object to be added
const newTx = {
txHash: '0x368eb7269eb88ba86..',
status: 'pending'
};
// Push new object into array
(txBacklog)
? setTxBacklog(prevState => [ ...prevState!, newTx ])
: setTxBacklog([newTx]);
if you want to push after specific index you can do as below:
const handleAddAfterIndex = index => {
setTheArray(oldItems => {
const copyItems = [...oldItems];
const finalItems = [];
for (let i = 0; i < copyItems.length; i += 1) {
if (i === index) {
finalItems.push(copyItems[i]);
finalItems.push(newItem);
} else {
finalItems.push(copyItems[i]);
}
}
return finalItems;
});
};

Categories

Resources