I have the following state:
const [state, setState] = React.useState({
title: "",
exchangeTypes: [],
errors: {
title: "",
exchangeTypes: "",
}
})
I am using a form validation in order to populate the state.errors object if a condition is not respected.
function formValidation(e){
const { name, value } = e.target;
let errors = state.errors;
switch (true) {
case (name==='title' && value.length < 4):
setState(prevState => ({
errors: { // object that we want to update
...prevState.errors, // keep all other key-value pairs
[name]: 'Title cannot be empty' // update the value of specific key
}
}))
break;
default:
break;
}
}
When I do so, the object DOES update BUT it deletes the value that I have not updated.
Before I call formValidation
My console.log(state) is:
{
"title": "",
"exchangeTypes: [],
"errors": {
title: "",
exchangeTypes: "",
}
}
After I call formValidation
My console.log(state) is:
{
"errors": {
title: "Title cannot be empty",
exchangeTypes: ""
}
}
SO my other state values have disappeared. There is only the errors object.
I followed this guide: How to update nested state properties in React
What I want:
{
"title": "",
"exchangeTypes: [],
"errors": {
title: "Title cannot be empty",
exchangeTypes: "",
}
}
What I get:
{
"errors": {
title: "Title cannot be empty",
exchangeTypes: "",
}
}
unlike the setState in class component, setState via useState doesn't automatically merge when you use an object as the value. You have to do it manually
setState((prevState) => ({
...prevState, // <- here
errors: {
// object that we want to update
...prevState.errors, // keep all other key-value pairs
[name]: 'Title cannot be empty', // update the value of specific key
},
}));
Though you can certainly use the useState hook the way you're doing, the more common convention is to track the individual parts of your component's state separately. This is because useState replaces state rather than merging it, as you've discovered.
From the React docs:
You don’t have to use many state variables. State variables can hold objects and arrays just fine, so you can still group related data together. However, unlike this.setState in a class, updating a state variable always replaces it instead of merging it.
So in practice, your code might look like the following:
const MyComponent = () => {
const [title, setTitle] = useState('');
const [exchangeTypes, setExchangeTypes] = useState([]);
const [errors, setErrors] = useState({
title: "",
exchangeTypes: "",
});
function formValidation(e) {
const { name, value } = e.target;
switch (true) {
case (name === 'title' && value.length < 4):
setErrors({
...errors,
[name]: 'Title cannot be empty'
});
break;
default:
break;
}
}
return (
...
);
};
Related
I want to save my data in localstorage to evade the loss of it when reloading the page but i also need it in my gloable state to show a preview of it once it's added and never be lost when reloading the page,This is my slice format:
import { createSlice } from "#reduxjs/toolkit";
export const resumeSlicer = createSlice({
name: "resume",
initialState: {
Education: [
{
key: NaN,
Title: "",
Date: "",
Establishment: "",
Place: "",
},
],
},
reducers: {
SaveEducation: (state, action) => {
let Education = JSON.parse(localStorage.getItem("Education"));
if (!Education) {
Education.push(action.payload);
localStorage.setItem("Education", JSON.stringify(Education));
state.Education = Education;
} else {
Education.push(action.payload);
let i = 0;
Education.map((e) => {
e.key = i;
i++;
return e.key;
});
localStorage.setItem("Education", JSON.stringify(Education));
state.Education = Education;
}
},
getEducation: (state, action) => {
const items = JSON.parse(localStorage.getItem("Education"));
const empty_array = [
{
key: NaN,
Title: "",
Date: "",
Establishment: "",
Place: "",
},
];
state.Education.splice(0, state.Education.length);
state.Education = items;
},
},
});
And this is how i fetched:
const EdList = useSelector((state) => state.Education);
When i console.log it the result is "undefined"
Image Preview
https://i.stack.imgur.com/hD8bx.png
I'm hazarding a guess that the issue is a missing reference into the state. The chunk of state will typically nest under the name you give the slice, "resume" in this case. This occurs when you combine the slice reducers when creating the state object for the store.
Try:
const EdList = useSelector((state) => state.resume.Education);
If it turns out this isn't the case then we'll need to see how you create/configure the store and how you combine your reducers.
My component's state is as below:
const [state, setState] = useState({
teamMembersOptions: [],
selectedTeamMember: {},
});
teamMembersOptions are being mapped from the redux state teamMembersList as below:
const teamMembersList = useSelector(state => state.get_all_team_members.team)
useEffect(() => {
if (teamMembersList)
mapTeamMembers();
}, [teamMembersList])
const mapTeamMembers = () => {
const teamMembers = [];
teamMembersList.map(member => {
const memberObject = {
'value': member.id,
'label': member.first_name.charAt(0).toUpperCase() + member.first_name.slice(1) + ' ' + member.last_name.charAt(0).toUpperCase() + member.last_name.slice(1)
}
if (member.is_leader == 1) {
memberObject.label = memberObject.label + ' (owner)'
setState({
...state,
selectedTeamMember: memberObject
})
}
teamMembers.push(memberObject)
})
setState({
...state,
teamMembersOptions: teamMembers
})
}
The state variables of selectedTeamMember and teamMemberOptions are not updating, it keeps consoling empty state. Whenever I console the local array of teamMembers inside mapTeamMembers function, it logs all the values successfully teamMembersList from Redux
also logs successfully that means teamMembersList and teamMembers are not empty. But the state is not updating. Why the setState statement inside mapTeamMembers function is not updating the state?
There are a number of things going on here and lot of them cause renders to trigger more renders which is why you are getting unexpected output.
I have add useMemo() and useCallback() around the data and calculation method respectively, and added their return values to the dependency array for useEffect(). This is to avoid the useEffect dependencies change on every render.
Calling setState() within the .map() function doesn't feel like the right choice either as each time it is called a render might occur, even though you are halfway through the mapping operation. Instead I suggest, and opted for, using .reduce() on the array and returning that result which can then be used to update the state within the useEffect hook.
Have a look at the working code below and a sample output given the defined input from teamMembersList. Note: this doesn't use Redux in the example given that it more setup to prove the concept.
import { useCallback, useEffect, useMemo, useState } from "react";
export default function App() {
const [state, setState] = useState({
teamMembersOptions: [],
selectedTeamMember: {}
});
const teamMembersList = useMemo(
() => [
{ id: 1, first_name: "John", last_name: "Smith", is_leader: 0 },
{ id: 2, first_name: "Maggie", last_name: "Simpson", is_leader: 1 }
],
[]
);
const mapTeamMembers = useCallback(
() =>
teamMembersList.reduce(
(acc, member) => {
const memberObject = {
value: member.id,
label:
member.first_name.charAt(0).toUpperCase() +
member.first_name.slice(1) +
" " +
member.last_name.charAt(0).toUpperCase() +
member.last_name.slice(1)
};
if (member.is_leader === 1) {
memberObject.label = memberObject.label + " (owner)";
acc.leader = memberObject;
}
acc.teamMembers.push(memberObject);
return acc;
},
{
teamMembers: [],
leader: ""
}
),
[teamMembersList]
);
useEffect(() => {
if (teamMembersList) {
const members = mapTeamMembers();
setState({
selectedTeamMember: members.leader,
teamMembersOptions: members.teamMembers
});
}
}, [teamMembersList, mapTeamMembers, setState]);
return (
<div>
<pre>
<code>{JSON.stringify(state, null, 4)}</code>
</pre>
</div>
);
}
The above will render out:
{
"selectedTeamMember": {
"value": 2,
"label": "Maggie Simpson (owner)"
},
"teamMembersOptions": [
{
"value": 1,
"label": "John Smith"
},
{
"value": 2,
"label": "Maggie Simpson (owner)"
}
]
}
I'd consider splitting the state object into individual state items but that's really up to you and how you want to handle the data.
I am trying to create some custom error validation in React
I have a values obj in state and an error obj in state that share the same keys
const [values, setValues] = useState({
name: "",
age: "",
city: ""
});
const [err, setErr] = useState({
name: "",
age: "",
city: ""
});
i have a very simple handle change and an onSubmit which i want to run my custom validator function inside
const handleChange = (e) => {
setValues({
...values,
[e.target.name]: e.target.value
});
};
const handleSubmit = (e) => {
e.preventDefault();
validateForms();
};
in my validateForms function my theory is since both my pieces of state share the same keys I am trying to see if any of those values === '' if yes match is the same key in the err obj and set that respective value to the error and then do other stuff in JSX
const validateForms = () => {
for (const value in values) {
if (values[value] === "") {
setErr({
...err,
value: `${value} is a required field`
});
}
}
};
I definitely feel like I'm not using setErr properly here. Any help would be lovely.
link to sandbox: https://codesandbox.io/s/trusting-bartik-6cbdb?file=/src/App.js:467-680
You have two issues. First, your error object key needs to be [value] rather than the string value. Second, you're going to want to use a callback function in your state setter so that you're not spreading an old version of the error object:
const validateForms = () => {
for (const value in values) {
if (values[value] === "") {
setErr(err => ({
...err,
[value]: `${value} is a required field`
}));
}
}
};
A more intuitive way to set errors might be to accumulate them all and just set the error state once:
const validateForms = () => {
const errors = {};
for (const value in values) {
errors[value] = values[value] === "" ? `${value} is a required field` : "";
}
setErr(errors);
};
Question, I have this state coming from the backend It's a array of messages that will be store in state using useState. This state will be pass on in the child component. The problem is I want to change value of a specific key before storing it into the state.
Sample
Messages Array sample data
const messages = [
{
value: 'sample value one',
status: false,
},
{
value: 'sample value two',
status: false,
},
];
UseSelector
const messageGetById = useSelector((state) => state.messageGetById);
const { message } = messageGetById;
UseEffect
useEffect(() => {
if (message) {
setCurrentMessage(message);
}
}, [message]);
The output that I want is before passing the message into setCurrentMessage, all the value of status will be change to true.
Thanks!
You can use map method to map thought the array and change the status to true.
useEffect(() => {
if (message) {
const newMessages = messages?.map((mess) => {
return {...mess, status: true}})
setCurrentMessage(newMessages);
}}, [message]);
Set the mapped state with useEffect
useEffect(() => {
const data = [...message];
if (data.length > 0) {
data.map((ele) => ({
value: "YOUR CHANGED VALUE",
status: ele.status,
}));
setCurrentMessage(data);
}
}, [message]);
Hello guys I'm trying to update the state of a nested object in react, I'm currently doing this:
handleChange({target: {id, value}}, type) {
this.setState(
state => ({
dwelling: (Object.assign(state.dwelling, {[id]: {[type]: value}}))
})
);
}
it comes from a formgroup:
<FormGroup controlId="spaces">
<ControlLabel>Dormitorios</ControlLabel>
<FormControl
componentClass="select"
value={dwelling.spaces.dorms}
placeholder="Seleccione"
onChange={e => this.handleChange(e, 'dorms')}
>
The problem is when I update the state of the sub object dwelling.spaces.dorms is created but when I try to place another property it replaces the old one instead of getting added:
Before Dwelling:
{
address: "",
currency: "",
price: 0,
publicationType: "",
spaces: {
closets: "",
dorms: "",
pools: ""
},
subtype: "",
type: ""
}
After onChange for dwelling.spaces.dorms
{
address: "",
currency: "",
price: 0,
publicationType: "",
spaces: {
dorms: "3",
},
subtype: "",
type: ""
}
After onChange for dwelling.spaces.closets
{
address: "",
currency: "",
price: 0,
publicationType: "",
spaces: {
closets: "3",
},
subtype: "",
type: ""
}
This example uses ES6 spread operator to keep your old properties which is the equivalent of Object.assign.
So what was happening is you're not keeping your nested value.
this.setState({
dwelling: {
...this.state.dwelling,
[id]: {
...this.state.dwelling[id],
[type]: value
}
}
});
In your example you overwrote your value with a new object. Notice the bolded text below.
dwelling: (Object.assign(state.dwelling, {[id]: {[type]: value}}))
In the bolded text it specifically set a new object into state.dwelling without keeping the old values. So what we did is that we used the ES6 spread operator to help merge your old values with the new value
{
...this.state.dwelling[id],
[type]: value
}
I keep my form state in a complex object and to simplify my code I use this helper function in my "handleChange" function referenced by TextField, Select, etc..
export function updateObject(obj, keys, value) {
let key = keys.shift();
if (keys.length > 0) {
let tmp = updateObject(obj[key], keys, value);
return {...obj, [key]: tmp};
} else {
return {...obj, [key]: value};
}
}
React-redux/Material-UI example
let [formState, changeFormState] = useState({});
function handleChange(event) {
changeFormState(updateObject(formState, event.target.name.split('.'),
event.target.value));
}
<TextField className={classes.textfield} name='foo.bar' value=
formstate.foo.bar || ''} onChange={handleChange} />