Using setState inside a arrow function resets state - reactjs

I am trying to modify a state when a users input fields on my dashboard is changed. This is how the handler is intended to work:
If the state is empty. Create a user with the standard values and change its values to the changed inputs
If the user exists in the state, change the changed field in the state to the new value.
If the user does not exist. Add the user to the state and change the changed field to the new value.
I am doing this by calling this function on a change of any inputs:
const handleInputChange = (event, person) => {
let new_form_val = {
objectId: person._id,
name: person.name,
role: person.role,
privilege: person.privilege,
group: person.group
};
console.log("handle change function called")
if (formValues.length == 0)
{
console.log("formValues is empty")
new_form_val[event.target.name] = event.target.value
console.log("adding", new_form_val)
setFormValues([...formValues, new_form_val])
}
// console.log(event.target.value)
console.log("Change target id", event.target.id)
console.log("current formvalue before change", formValues)
let form_val = formValues.find((item) => item.objectId == event.target.id)
if (form_val) {
console.log("person found in formValues", form_val)
let index = formValues.indexOf(form_val)
formValues[index][event.target.name] = event.target.value
console.log("Changed already existing formvalue", formValues)
setFormValues(formValues)
}
else {
new_form_val[event.target.name] = event.target.value
console.log("new person in form value", new_form_val)
setFormValues([...formValues, new_form_val])
}
}
Later on I am using that function as an onChange event handler
useEffect(() => {
// GARL: https: //bobbyhadz.com/blog/react-push-to-state-array
setPeople([])
console.log("get users effetct ran")
axios.get('/api/getusers').then((response) => {
response.data.forEach((item, index) => {
setPeople(oldStatusArray => {
return [...oldStatusArray, <Person
key={index}
id={index+1}
_id={item._id}
name={item.name}
role={item.role}
privilege_id={item.privilege}
group_id={item.group}
onChange={(event) => handleInputChange(event, item)}
/>]
})
});
})
}, []);
The problem I am facing though is whenever the onChange function is called. The whole formValues sate is reset and replaced with the new changed state. For exmpale: I change user A to a new name and role and the change is logged to the console. I also Change User B and then C to a new group. Finally the state only has the changes made from C.
Here is the full code:
import Link from 'next/link';
import axios from 'axios';
import React, { useState, useEffect } from "react";
import Person from '../components/person' // Not actually a import
const Dashboard = () => {
const [people, setPeople] = useState([]);
const [formValues, setFormValues] = useState([]);
const handleInputChange = (event, person) => {
let new_form_val = {
objectId: person._id,
name: person.name,
role: person.role,
privilege: person.privilege,
group: person.group
};
console.log("handle change function called")
if (formValues.length == 0)
{
console.log("formValues is empty")
new_form_val[event.target.name] = event.target.value
console.log("adding", new_form_val)
setFormValues([...formValues, new_form_val])
}
// console.log(event.target.value)
console.log("Change target id", event.target.id)
console.log("current formvalue before change", formValues)
let form_val = formValues.find((item) => item.objectId == event.target.id)
if (form_val) {
console.log("person found in formValues", form_val)
let index = formValues.indexOf(form_val)
formValues[index][event.target.name] = event.target.value
console.log("Changed already existing formvalue", formValues)
setFormValues(formValues)
}
else {
new_form_val[event.target.name] = event.target.value
console.log("new person in form value", new_form_val)
setFormValues([...formValues, new_form_val])
}
}
useEffect(() => {
setPeople([])
console.log("get users effetct ran")
axios.get('/api/getusers').then((response) => {
response.data.forEach((item, index) => {
setPeople(oldStatusArray => {
return [...oldStatusArray, <Person
key={index}
_id={item._id}
name={item.name}
role={item.role}
privilege_id={item.privilege}
group_id={item.group}
onChange={(event) => handleInputChange(event, item)}
/>]
})
});
})
}, []);
const submit = (values) => {
// Submits state to backend for handling
}
return (
<div id="main">
<h1>Administration</h1>
{(people.length == 0) ?
<h1>Laddar innehållet..</h1> : people }
</div>
);
}
export default Dashboard;
Here is the output after changing the input fields a couple of times:
>> handle change function called
>> formValues is empty
>> adding - Object { objectId: "634ea9b368bd856cebfdddc0", name: "RADICATED", role: "...", privilege: "634ff6d42c7b67c5708e901b", group: "634ff7322c7b67c5708e901d" }
>> change target id 634ea9b368bd856cebfdddc0
>> current formvalue before change - Array []
>> new person in form value - Object { objectId: "634ea9b368bd856cebfdddc0", name: "RADICATED", role: "....", privilege: "634ff6d42c7b67c5708e901b", group: "634ff7322c7b67c5708e901d" }
>> CURRENT formvalues - Array [ {…} ] (len: 1)
I have also tried to adding formValues as a dependency to useEffect however, this results in a rerender of the users if I change any of the inputs as the setPeople is called in the useEffect.
How can I achieve a handleInputChange function that works as intended without updating the renderer or reseting the state?

I noticed the step 1 and 3 are actually the same so I put those together. The itemExists check if the person is already in the state. If the state is empty itemExists is false and if the person does not exists itemExists is also false.
When false we just update the field and return the previous and the new new_form_val.
When true we loop over all the current values until we find the one we want to edit, and then update the field we want to update.
const handleInputChange = (event, person) => {
const new_form_val = {
objectId: person._id,
name: person.name,
role: person.role,
privilege: person.privilege,
group: person.group,
};
// check if the item already exists
const itemExists =
formValues.find((item) => item.objectId == event.target.id) !== undefined;
if (itemExists) {
setFormValues((prevFormValues) => {
// map current values
const newValues = prevFormValues.map((item) => {
// if its not the item we're editing just return the item
if (item.objectId !== event.target.id) return item;
// if it is, update the item
const updatedItem = {
...item,
[event.target.name]: event.target.value,
};
return updatedItem;
});
return newValues;
});
} else {
// update the field with the new value
new_form_val[event.target.name] = event.target.value;
// add to the values
setFormValues((prevFormValues) => [...prevFormValues, new_form_val]);
}
};
I also updated the way the people were set. Now we first loop over all the data received from the api and create an array of Person components and set that array to the state, instead of setting the state for every result in the api data.
useEffect(() => {
// no need to set the people to an empty array since the default state is already an empty array
// setPeople([]);
console.log("get users effetct ran");
axios.get("/api/getusers").then((response) => {
const peopleFromApi = response.data.map((item, index) => (
<Person
key={index}
_id={item._id}
name={item.name}
role={item.role}
privilege_id={item.privilege}
group_id={item.group}
onChange={(event) => handleInputChange(event, item)}
/>
));
setPeople(peopleFromApi);
});
}, []);
I hope this helps you continue your project!

Related

How to set value for the checkbox in ReactJS

I try to show my value using checkbox. Value always comes for the console log. But it didn't set for the checkbox. Here is the code and image for my problem:
var NotePage = createClass({
addTags(e) {
console.log("id****************", e.target.id);
let id = e.target.id;
let selectedTags = this.state.selectedTags;
if (selectedTags.includes(id)) {
var index = selectedTags.indexOf(id)
selectedTags.splice(index, 1);
} else {
selectedTags.push(id);
}
console.log("id****************selectedTags", selectedTags);
this.setState({
selectedTags: selectedTags
})
},
render: function () {
assignStates: function (note, token, tagCategories) {
let fields = [];
fields["title"] = note.title_en;
fields["body"] = note.body_en;
let selectedFileName = null
if (note.file_url_en != "") {
console.log("note.file_url_en ", note.file_url_en);
selectedFileName = note.file_url_en
}
let selectedTags = [];
let n = 0;
(note.note_tag).forEach(tag => {
selectedTags.push(tag.id.toString());
n++;
});
console.log("id****************first", selectedTags);
let initial_values = {
note: note,
id: note.id,
api: new Api(token),
message: "",
title: note.title_en,
body: note.body_en,
fields: fields,
isEdit: false,
selectedTags: selectedTags,
tagCategories: tagCategories,
selectedFileName: selectedFileName,
}
return initial_values;
},
const { selectedTags } = this.state;
{(tagCategory.tags).map((tag) => (
<div className="col-3">
<div>
<input
type="checkbox"
value={selectedTags.includes(tag.id)}
id={tag.id}
onChange={this.addTags} />
<label style={{ marginLeft: "10px", fontSize: "15px" }}>
{tag.name_en}
</label>
</div>
</div>
))
}
})
Image related for the problem
You've an issue with state mutation. You save a reference to the current state, mutate it, and then save it back into state. This breaks React's use of shallow reference equality checks during reconciliation to determine what needs to be flushed to the DOM.
addTags(e) {
let id = e.target.id;
let selectedTags = this.state.selectedTags; // reference to state
if (selectedTags.includes(id)) {
var index = selectedTags.indexOf(id)
selectedTags.splice(index, 1); // mutation!!
} else {
selectedTags.push(id); // mutation!!
}
this.setState({
selectedTags: selectedTags // same reference as previous state
});
},
To remedy you necessarily return a new array object reference.
addTags(e) {
const { id } = e.target;
this.setState(prevState => {
if (prevState.selectedTags.includes(id)) {
return {
selectedTags: prevState.selectedTags.filter(el => el !== id),
};
} else {
return {
selectedTags: prevState.selectedTags.concat(id),
};
}
});
},
Use the "checked" attribute.
<input
type="checkbox"
value={tag.id}
checked={selectedTags.includes(tag.id)}
id={tag.id}
onChange={this.addTags} />
also, about the value attribute in checkboxes:
A DOMString representing the value of the checkbox. This is not displayed on the client-side, but on the server this is the value given to the data submitted with the checkbox's name.
Note: If a checkbox is unchecked when its form is submitted, there is
no value submitted to the server to represent its unchecked state
(e.g. value=unchecked); the value is not submitted to the server at
all. If you wanted to submit a default value for the checkbox when it
is unchecked, you could include an inside the
form with the same name and value, generated by JavaScript perhaps.
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#value
I think you should use checked property instead of value.
For reference check react js docs here
You are mutating state variable directly with selectedTags.splice(index, 1); and selectedTags.push(id);
What you need to do is make a copy of the state variable and change that:
addTags(e) {
let id = e.target.id;
if (this.state.selectedTags.includes(id)) {
this.setState(state => (
{...state, selectedTags: state.selectedTags.filter(tag => tag !== id)}
))
} else {
this.setState(state => (
{...state, selectedTags: [...state.selectedTags, id]}
))
}
}

Why when the state is updated the changes in the rendering are not updated?

I have created a hook to access the collections of the database and its methods.
import { remote } from 'electron'
import { useState, useEffect } from "react"
function useCollections(collections = []) {
let [dbInstances, setDbInstances] = useState(null)
let [data, setData] = useState(null)
// DB METHODS
// Create
let create = async (doc, dbName) => {
await dbInstances[dbName].create(doc)
let newData = await dbInstances[dbName].readAll()
setData({ ...data, [dbName]: newData })
}
// Get details
let getDetails = async (id, dbName) => {
let doc = await dbInstances[dbName].read(id)
return doc
}
// Delete
let deleteOne = async (id, dbName) => {
await dbInstances[dbName].deleteOne(id)
let newData = await dbInstances[dbName].readAll()
setData({ ...data, [dbName]: newData })
}
// Update
let updateOne = async (id, updatedDoc, dbName) => {
await dbInstances[dbName].archive(id, updatedDoc)
let newData = await dbInstances[dbName].readAll()
setData({ ...data, [dbName]: newData })
}
// EFFECTS
useEffect(() => {
console.log('mounting component')
let newDBIs = {}
collections.forEach(col => newDBIs[col] = remote.getGlobal(col))
console.log('db instances settted', newDBIs)
setDbInstances(newDBIs)
}, [])
// When DBs are instantiated, request all docs and set data with response
useEffect(() => {
if (
dbInstances !== null &&
data === null &&
Object.keys(dbInstances).length === collections.length)
{
console.log('setting data')
let newData = {}
collections.forEach(async col => newData[col] = await dbInstances[col].readAll())
console.log('data setted => ', newData)
setData(newData)
}
}, [dbInstances])
return {
data,
create,
getDetails,
deleteOne,
updateOne
};
}
export default useCollections;
In the component where the data returned by the hook is consumed, even though the variable data contains the expected data, these are not rendered.
import WindowsLayout from "../../components/layout/WindowsLayout"
import { useState, useEffect } from "react"
import { remote } from "electron"
import useCollections from "../../hooks/useCollections"
const EditWorkWindow = ({ workId }) => {
let { data, deleteOne, updateOne } = useCollections([
'workDB',
'studioDB',
'rateDB'
])
useEffect(() => {
if (data !== null) console.log(data)
}, [data])
return (
<WindowsLayout title="Edit work window">
<div style={{ height: 243 }} className="window-content">
<div className="padded-more bg-gray-200">
<h2>{JSON.stringify(data)}</h2>
<button onClick={() => console.log(data)}>CLG</button>
</div>
</div>
</WindowsLayout >
)
}
export default EditWorkWindow
The effect hook shows the expected data by console.
<h2>{JSON.stringify(data)}</h2> = {}
When the button is clicked, the expected data is displayed on the console.
I can't understand why if data contains properties, they are not shown in {JSON.stringify(data)}
This is what is shown by the console after clicking on the button
console.log(data) image
And this is example data and their its properties
{
"workDB":[
{
"product":"Work name 1",
"amounts_rates":[
{
"rate":"EflcQflqu2oWWVk2",
"amount":6
},
{
"rate":"FeMIX00pwpmZwoVW",
"amount":1
}
],
"date":"2020-08-31",
"studio":"BCvPeWzMiS8fZsmS",
"_id":"2ZvHMWFODBHYWEBo",
"createdAt":"2020-08-31T09:39:21.077Z",
"updatedAt":"2020-08-31T09:39:21.077Z"
},
{
"product":"Work name 2",
"amounts_rates":[
],
"date":"2020-09-02",
"director":"",
"_id":"PRpp1OQcJnkFKeR0",
"createdAt":"2020-09-01T19:56:33.201Z",
"updatedAt":"2020-09-01T19:56:33.201Z"
}
],
"studioDB":[
{
"name":"Studio name 1",
"_id":"0J1AVXtgDjwBjRS9",
"createdAt":"2020-08-25T10:18:40.004Z",
"updatedAt":"2020-08-25T10:18:40.004Z"
},
{
"name":"Studio name 2",
"_id":"8sFH7gncaM6V7lHh",
"createdAt":"2020-08-25T10:19:45.232Z",
"updatedAt":"2020-08-25T10:19:45.232Z"
}
],
"rateDB":[
{
"name":"Rate name 1",
"value":4.1,
"_id":"EflcQflqu2oWWVk2",
"createdAt":"2020-08-25T10:24:17.357Z",
"updatedAt":"2020-08-25T10:24:17.357Z"
},
{
"name":"Rate name 1",
"value":34,
"_id":"FeMIX00pwpmZwoVW",
"createdAt":"2020-08-25T10:24:25.628Z",
"updatedAt":"2020-08-25T10:24:25.628Z"
}
]
}
Async problem it is.
// When DBs are instantiated, request all docs and set data with response
useEffect(() => {
if (
dbInstances !== null &&
data === null &&
Object.keys(dbInstances).length === collections.length)
{
console.log('setting data')
let newData = {}
collections.forEach(async col => newData[col] = await dbInstances[col].readAll())
console.log('data setted => ', newData)
setData(newData) // <-- 👋LOOK HERE
}
}, [dbInstances])
So you let newData = {} empty object, and send it off to trigger an update re-render by calling setData(), but newData is empty at the moment of calling.
In your rendering function JSON.stringify(data) pickup the data, but it's still empty at the moment of rendering!
It's only when async col => newData[col] = await someValue call is resolved, that your newData's properties will get assigned new values, the newData object stays the same. But by the time it's resolved, the rendering is done already.
Solution: wait till async function call is resolved, then you call setData()
useEffect(() => {
// ...
const promises = collections.map(async col => {
newData[col] = await dbInstances[col].readAll())
})
Promise.all(promises).then(() => { setData(newData) })
})
The reason why you see the updated value when inspecting in console, is because you didn't inspect "quick enough". By the time you mouse-click to expand the object in console, its properties are already assigned values. But if you change
console.log('data setted => ', newData)
// to
console.log('data setted => ', JSON.stringify(newData))
you'll see an empty object.

useState referring to stale value

I have a keeper app where I am adding notes and storing them in database. When I make a post request to the server, I am trying to fetch the _id from database, which will eventually help me to later delete the note ( if needed).
Here is my jsx file
function CreateMessage(props) {
const [currentGuest, setCurrentGuest] = useState({
guestName: '',
guestMessage: '',
id:''
});
function handleMessages(event) {
const {name, value} = event.target;
setCurrentGuest(prevGuest => {
return {
...prevGuest,
[name] : value
};
});
}
function submitMessage(event) {
//props.onAdd(currentGuest);
const params = {
guestName: currentGuest.guestName,
guestMessage: currentGuest.guestMessage,
}
axios
.post("http://localhost:8000/notes", params)
.then(res => {
console.log("The response is"+res.data._id);
console.log(res.status);
setCurrentGuest(prevGuest => {
console.log(res.data._id)
return {
...prevGuest,
id: res.data._id
};
});
console.log(currentGuest);
})
event.preventDefault();
}
return (
<div>
<form>
<input
name="guestName"
placeholder="Guest Name"
value={currentGuest.guestName}
onChange={handleMessages}
/>
<textarea
name="guestMessage"
placeholder="Write a Message"
rows="3"
value={currentGuest.guestMessage}
onChange={handleMessages}
/>
<button onClick={submitMessage}>Add</button>
</form>
</div>
);
}
The id is properly being fetched and displayed in ```console.log("The response is"+res.data._id"). But on first submit, the is always empty and stale id gets attached to the currentGuest object on next submit
function submitMessage(event) {
//props.onAdd(currentGuest);
const params = {
guestName: currentGuest.guestName,
guestMessage: currentGuest.guestMessage,
}
axios
.post("http://localhost:8000/notes", params)
.then(res => {
console.log("The response is"+res.data._id);
console.log(res.status);
setCurrentGuest(prevGuest => {
console.log(res.data._id)
return {
...prevGuest,
id: res.data._id
};
});
console.log(currentGuest);
})
event.preventDefault();
}
In the above snippet, after getting the response you're correctly changing the state but the problem is with where you're checking the changed state(console.log(currentGuest)). You're basically logging before the state is changed.
You can use useEffect hook and log the state inside it. This runs every time the currentGuest Changes.
useEffect(() => {
console.log(currentGuest)
}, [currentGuest])
Update
You can use the modified currentGuest inside the useEffect hook:
useEffect(() => {
console.log(currentGuest)
if(currentGuest.id) {
props.onAdd(currentGuest);
// You can also reset the state here as follows
setCurrentGuest({
guestName: '',
guestMessage: '',
id:''
});
}
}, [currentGuest]) // You might need to add the necessary dependencies to this array.

Duplication problem in TodoList application

I am creating a todo-list, the following function handleChange gets the id of the a todo component and changes its attribute of completed from true/false. This is then saved in state of allTodos
function handleChange(id) {
const updatedTodos = allTodos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
return todo
})
setTodos(updatedTodos)
}
const todoComponents = allTodos.map(item => <Todos key={item.id} item={item} handleChange={handleChange}/>)
the function updateDB takes that value from state and using it to update the database.
function updateDB(event) {
event.preventDefault()
const value = {
completed: false,
text: newTodo,
id: allTodos.length,
}
}
Here's where the problem arises: id: allTodos.length. If one of these are deleted, it will create a todo with a duplicate ID, crashing the whole thing. I don't know how to avoid this problem.
In updateDB, you are setting id to allTodos.length aka 1.

React js Change Field specifying the attribute name

I have attributes in the state, I would like to ensure that by specifying the function the attribute name changes the value contained in the state.
It seems to work, the problem that if I have an object of this type in the state:
companyInfo: {
name: "",
vatNumber: "",
legalRepresentative: ""
}
It does not work properly, as the code is now set in the state in this case a new attribute is created.
So I'd like to do something like this:
handleChangeField("companyInfo.name")
It is changed to the state atrribute name of the obj companyInfo that is in the state.
Can you give me some advice?
Link: codesandbox
Code:
import ReactDOM from "react-dom";
import React, { Component } from "react";
import ReactJson from "react-json-view";
class Todo extends Component {
constructor(props) {
super(props);
this.state = {
email: "email0",
role: "role0",
companyInfo: {
name: "",
vatNumber: "",
legalRepresentative: ""
}
};
}
returnStateElement = (...elements) => {
const copy = Object.assign({}, this.state);
return elements.reduce((obj, key) => ({ ...obj, [key]: copy[key] }), {});
};
handleChangeField = field => evt => {
let state = {};
state[field] = evt.target.value;
this.setState(state);
};
handleSubmit = () => {
let el = this.returnStateElement(
"name",
"email",
"vatNumber",
"legalRepresentative",
"role"
);
let { name, email, legalRepresentative, vatNumber, role } = el;
let dataSender = {};
dataSender.email = email;
dataSender.role = role;
dataSender.companyInfo = {};
dataSender.companyInfo.name = name;
dataSender.companyInfo.legalRepresentative = legalRepresentative;
dataSender.companyInfo.vatNumber = vatNumber;
console.log(this.state);
//console.log(dataSender)
};
render() {
return (
<div>
<input onChange={this.handleChangeField("email")} />
<br />
<br />
<input onChange={this.handleChangeField("companyInfo.name")} />
<br />
<br />
<button onClick={() => this.handleSubmit()}>send</button>
<br />
<br />
<ReactJson src={this.state} theme="solarized" />
</div>
);
}
}
ReactDOM.render(<Todo />, document.getElementById("root"));
Edit: I came up with a much better answer where one mutates the specific key of the oldState using a reduce. Less code, much more elegant and should work at any object depth.
Working example here
setNestedField(object, fields, newValue) {
fields.reduce((acc, field, index) => {
if (index === fields.length - 1) {
acc[field] = newValue;
}
return acc[field];
}, object);
return object;
}
handleChangeField = field => evt => {
const fields = field.split(".");
let oldState = this.state;
const newState = this.setNestedField(
{ ...oldState },
fields,
evt.target.value
);
this.setState(newState);
};
OLD ANSWER
handleChangeFields looks like this:
handleChangeField = field => evt => {
//first you split by '.' to get all the keys
const fields = field.split(".").reverse();
// you'll need the previous state
let oldState = this.state;
let newState = fields.reduce((acc, value, index) => {
if (index === 0) {
// you add the event value to the first key
acc[value] = evt.target.value;
return acc;
}
//copy acc to use it later
const tmp = { ...acc };
//delete previous key added to acc
delete acc[fields[index - 1]];
acc[value] = { ...oldState[value], ...tmp };
return acc;
}, {});
this.setState(newState);
};
What's going on step by step in the reduce function, if you do handleChangeField('company.name') with evt.target.value = "Big Corp":
1) you get the array ['name','company']
2) you go in the reduce function
when index = 0, acc = {}, key='name' => {name: 'Big Corp'}
when index=1, acc= {name: 'Big Corp'},key='company' => acc = { company: {name: 'Big Corp'}, name: 'BigCorp} so before returning we delete the previous key (name here) to return => { company: {name: 'Big Corp'}

Resources