Understanding state update in React - reactjs

New to React. I have a NewItem component that should be pretty simple (and I have managed to do similar stuff before without problems): it has a form with two input text fields and it should update a stateful object (I think that's what it's called) when the form is submitted. Yet the state is updated only the second time the user submit the form. Why is that and how can I fix this? (Again, I am aware it's probably a very silly thing to ask, but I have done similar stuff with arrays and thought I got it).
Thank you for your patience and help.
Here's the component:
import { useState } from "react";
const NewItem = () => {
const [newName, setNewName] = useState("");
const [newType, setNewType] = useState("");
const [newItem, setNewItem] = useState({ name: "", type: "" });
const getNewName = (event) => {
const changedName = event.target.value;
setNewName(changedName);
};
const getNewType = (event) => {
const changedType = event.target.value;
setNewType(changedType);
};
const handleSubmit = (event) => {
event.preventDefault();
const toAdd = { ...newItem, name: newName, type: newType };
setNewItem(toAdd);
console.log(toAdd);
};
return (
<form onSubmit={handleSubmit}>
<label>
Name
<input type="text" value={newName} onChange={getNewName} />
</label>
<label>
Type
<input type="text" value={newType} onChange={getNewType} />
</label>
<button type="submit">Add</button>
</form>
);
};
export default NewItem;

Related

ReactJS form need to submit 2 times to work

const [name, setName] = useState("");
const [age, setAge] = useState("");
const initialValues = {
name: "",
age: "",
};
const [formValues, setFormValues] = useState(initialValues);
const [formErrors, setFormErrors] = useState({});
const [isSubmit, setIsSubmit] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({ ...formValues, [name]: value });
};
const handleSubmit = (e) => {
setFormErrors(validate(formValues));
setIsSubmit(true);
};
const validate = (values) => {
const errors = {};
if (!values.name) {
errors.name = "Name is required";
}
if (!values.age) {
errors.age= "Age is required";
}
return errors;
};
const userCreate = async () => {
await api.post("/createuser", {
name,
age,
});
};
return (
<div class="container">
<Form
onSubmit={
Object.keys(formErrors).length === 0 && isSubmit
? userCreate
: handleSubmit
}
>
<Form.Field>
<label>Name</label>
<input
name="name"
onChange={(e) => {
setName(e.target.value);
handleChange(e);
}}
values={formValues.name}
/>
<span className="error-message">{formErrors.name}</span>
</Form.Field>
<Form.Field>
<label>Age</label>
<input
name="age"
onChange={(e) => {
setAge(e.target.value);
handleChange(e);
}}
values={formValues.age}
/>
<p className="error-message">{formErrors.age}</p>
</Form.Field>
<Button type="submit">Submit</Button>
</Form>
</div>
);
I'm trying to use axios to do POST method for creating user.
I got everything works fine but there's one small problem but I don't know how to fix.
The problem is that I always need to submit the form 2 times to make the POST request. There's nothing happen in the first submit, but it will work in the second submit.
Does anyone know what's wrong with my code?
Edited
According to #DBS solution.
I'm trying to follow the steps but now the form can't submit anymore. Can someone let me know if I missed something?
const [name, setName] = useState("");
const [age, setAge] = useState("");
const initialValues = {
name: "",
age: "",
};
const [formValues, setFormValues] = useState(initialValues);
const [formErrors, setFormErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormValues({ ...formValues, [name]: value });
};
const handleSubmit = (e) => {
if (!Object.keys(formErrors).length && !isSubmitting) {
setFormErrors(validate(formValues));
} else {
userCreate();
setisSubmitting(true);
}
};
const validate = (values) => {
const errors = {};
if (!values.name) {
errors.name = "Name is required";
}
if (!values.age) {
errors.age= "Age is required";
}
return errors;
};
const userCreate = async () => {
await api.post("/createuser", {
name,
age,
});
};
return (
<div class="container">
<Form
onSubmit={handleSubmit}
>
<Form.Field>
<label>Name</label>
<input
name="name"
onChange={(e) => {
setName(e.target.value);
handleChange(e);
}}
values={formValues.name}
/>
<span className="error-message">{formErrors.name}</span>
</Form.Field>
<Form.Field>
<label>Age</label>
<input
name="age"
onChange={(e) => {
setAge(e.target.value);
handleChange(e);
}}
values={formValues.age}
/>
<p className="error-message">{formErrors.age}</p>
<
The issue here is your isSubmit, it is required to be true for userCreate to be called:
onSubmit={
Object.keys(formErrors).length === 0 && isSubmit
? userCreate
: handleSubmit
}
But it starts as false:
const [isSubmit, setIsSubmit] = useState(false);
And is only ever updated when handleSubmit is called (which, confusingly, is only called when the validation fails)
So your current code does this:
isSubmit is false
Submit is clicked, handleSubmit is called and isSubmit is set to true
Submit is clicked again, now isSubmit is true it will call userCreate
To solve this, there are a few different approaches, but I would:
Move all the submit handler logic into onSubmit={handleSubmit} (To keep things clear)
Inside there, do your error length check (0 error) and isSubmit (Which I would probably rename to isSubmitting for clarity, and make sure it's false) (E.g. !Object.keys(formErrors).length && !isSubmitting)
If there are errors, show the appropriate message (Leaving isSubmitting as false)
If not, call userCreate (And set isSubmitting to true)
Lastly, if this can be submitted multiple times, add an effect/callback/then to reset isSubmitting once the call is complete.
Are you using the isSubmitting flag for something? if not below might be work for you.
If there is no error, calling the create method
const handleSubmit = (e) => {
setFormErrors(validate(formValues));
if(Object.keys(formErrors).length === 0) {
userCreate();
}
};
if isSubmitting is used to check the submit or create in progress
const handleSubmit = (e) => {
setFormErrors(validate(formValues));
if(Object.keys(formErrors).length === 0) {
setIsSubmitting(true);
userCreate();
}
};
The flag isSubmitting should be set as false on API is success or failed
setIsSubmitting(false)

Is there a way to assign multiple inputs to a state array in react?

I'm trying to have the user input multiple "themes" via a form input bar so that I can add it to the database. The schema model I have created for the object has the "theme" as an array so that part's done. I'm wondering if there's a way to add multiple input values to the same state variable theme in the code down below.
Here is what my code looks like:
import { useState } from "react";
const ProjectAdminForm = () => {
const [sdg, setSDG] = useState('')
const [goal, setGoal] = useState('')
const [orginization, setOrginization] = useState('')
const [source, setSource] = useState('')
const [location, setLocation] = useState('')
const [published, setPublished] = useState('')
const [website_url, setWebsiteURL] = useState('')
const [assignment_type, setAssignmentType] = useState('')
const [theme, setTheme] = useState('')
const [sharepoint_link, setSharepointLink] = useState('')
const [statement, setStatement] = useState('')
const [error, setError] = useState(null)
const handleSubmit = async (e) => {
e.preventDefault() // Prevents refresh of page from happening
console.log('button clicked')
const project = {sdg, goal, orginization, source, location, published, website_url, assignment_type, theme, sharepoint_link, statement}
console.log(project)
// Sending form response to backend
const response = await fetch('/api/projects', {
method: 'POST',
body: JSON.stringify(project),
headers: {
'Content-Type': 'application/json'
}
})
const json = await response.json
// Checking for error
if (!response.ok) {
setError(json.error)
}
if (response.ok) {
// Reset form inputs back to empty string
setSDG('')
setGoal('')
setOrginization('')
setSource('')
setLocation('')
setPublished('')
setWebsiteURL('')
setAssignmentType('')
setTheme('')
setSharepointLink('')
setStatement('')
setError(null)
console.log('new project added', json)
}
}
return (
<form className="create" onSubmit={handleSubmit}>
<h3>Add a New Project</h3>
<label>SDG (Num + Name):</label>
<input
type="text"
placeholder="e.g. SDG 2: Zero Hunger"
onChange={(e) => setSDG(e.target.value)}
value={sdg}
/>
<label>Goal:</label>
<input
type="text"
onChange={(e) => setGoal(e.target.value)}
value={goal}
/>
<label>Orginization:</label>
<input
type="text"
onChange={(e) => setOrginization(e.target.value)}
value={orginization}
/>
<label>Source:</label>
<input
type="text"
onChange={(e) => setSource(e.target.value)}
value={source}
/>
<label>Location:</label>
<input
type="text"
onChange={(e) => setLocation(e.target.value)}
value={location}
/>
<label>Published:</label>
<input
type="text"
onChange={(e) => setPublished(e.target.value)}
value={published}
/>
<label>Website URL:</label>
<input
type="text"
onChange={(e) => setWebsiteURL(e.target.value)}
value={website_url}
/>
<label>Assignment Type:</label>
<input
type="text"
onChange={(e) => setAssignmentType(e.target.value)}
value={assignment_type}
/>
<label>Theme:</label>
<input
type="text"
onChange={(e) => setTheme(e.target.value)}
value={theme}
/>
<label>Sharepoint Link:</label>
<input
type="text"
onChange={(e) => setSharepointLink(e.target.value)}
value={sharepoint_link}
/>
<label>Statement:</label>
<input
type="text"
onChange={(e) => setStatement(e.target.value)}
value={statement}
/>
<button>Add Project</button>
{error && <div className="error">{error}</div>}
</form>
)
}
export default ProjectAdminForm
Specifically for the theme input:
<label>Theme:</label>
<input
type="text"
onChange={(e) => setTheme(e.target.value)}
value={theme}
/>
, how can I have multiple inputs that link to the same theme state which holds them all as an array and then gets passed on to the backend via handleSubmit? Say a user wants to enter "magic", "joy", and "fun" as the themes, how could I make it so that all 3 of those get stored in the theme state variable via the input form?
As array:
//initialise an empty array as the default state for adding themes
const [themes, setThemes] = useState([]);
//create an array to hold your theme names
const available_themes = ['magic', 'joy', 'fun', 'more'];
//create a function for adding themes to your state array
const addTheme = (name) =>{
//if the theme to be added does not exists in the array
if(!themes.includes(name)){
//make a copy (...) of the current themes, and add your new theme name to a new array
setThemes([...themes, name])
}
}
//create a function for removing items from the array
const removeTheme = (name) =>{
//get the index (position) of the theme name in the current state array
const index = themes.indexOf(name);
//if the item exists in the themse array(has an index, is not -1)
if(index !== -1) {
//copy the current array to a tempory data store
let temp_themes = [...themes];
//remove the item from the array using its index
temp_themes.splice(index, 1);
//now save the new array to your state variable of themes
setThemes(temp_themes);
}
}
//for each available theme name create a check box
const themeselector = available_themes.map((name)=>{
//we check if the check box needs to be set to checked, i.e. selected
let checked = false;
//if the current theme is in the theme array it should be selected
if(themes.includes(name)){
checked = true;
}
// we add the checked=checked property {checked: 'checked'} to the check box if the list item should be checked using {...(checked ? {checked: 'checked'}: {})} where (boolean ? on true : else on false )
//we get the ev.target.checked variable to see if the check box has been selected or deselected. We either addTheme, or removeTheme based upon the value of teh variable.
return <div><input type="checkbox" {...(checked ? {checked: 'checked'}: {})} value={name} onClick={(ev)=>{ if(ev.target.checked) { addTheme(name) } else { removeTheme(name)} } } /> {name}</div>;
});
As object:
const [themes, setThemes] = useState({});
const available_themes = ['magic', 'joy', 'fun', 'more'];
const addTheme = (name) =>{
if(typeof themes[name] == 'undefined') {
let temp_themes = {...themes};
temp_themes[name] = name;
setThemes(temp_themes);
}
}
const removeTheme = (name) =>{
if(typeof themes[name] !== 'undefined') {
let temp_themes = {...themes};
delete temp_themes[name];
setThemes(temp_themes);
}
}
const themeselector = available_themes.map((name)=>{
let checked = false;
if(typeof themes[name] !== 'undefined'){
checked = true;
}
return <div><input type="checkbox" {...(checked ? {checked: 'checked'}: {})} value={name} onClick={(ev)=>{ if(ev.target.checked) { addTheme(name) } else { removeTheme(name)} } } /> {name}</div>;
});
<label>Select Themes</label>
{themeselector}
You could do something like this, I guess:
const [theme, setTheme] = useState([''])
And then define a function addTheme that would call setTheme and add a string if it's not already present in the array.
function addTheme(newTheme){
if(!theme.includes(newTheme){
setTheme([...theme, newTheme])
}
}
If you want to use a single text input, one could just create a new array with the values:
const [themes, setThemes] = useState([]);
const [themeText, setThemeText] = useState("");
onSubmit(e) {
e.preventDefault();
setThemes(themeText.split(" "));
}
<label>Theme:</label>
<input
type="text"
onChange={(e) => setThemeText(e.target.value)}
value={themeText}
/>

State not updating after setting state

I'm creating a contact form, and attempting to setState for each field, then pass that value to an email form.
When I console.log, I'm not getting any output. What am I doing wrong here?
Should I be using useEffect? From what I understand, useEffect is called whenever I set state, so I shouldn't need to. Is there something else I'm missing or doing wrong?
import style from "../styles/Contact.module.css";
import React, { useState } from 'react'
export default function Contact() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [subject, setSubject] = useState('')
const [message, setMessage] = useState('')
const [errors, setErrors] = useState({})
const [buttonText, setButtonText] = useState('Submit')
const [showSuccessMessage, setShowSuccessMessage] = useState(false)
const [showFailureMessage, setShowFailureMessage] = useState(false)
const handleValidation = () => {
let tempErrors = {}
let isValid = true
setName(document.getElementById('name').value)
setEmail(document.getElementById('email').value)
setSubject(document.getElementById('subject').value)
setMessage(document.getElementById('message').value)
console.log(name, email, subject, message)
if (name.length <= 0) {
tempErrors['name'] = true
isValid = false
}
if (email.length) {
tempErrors['email'] = true
isValid = false
}
if (subject.length) {
tempErrors['subject'] = true
isValid = false
}
if (message.length <= 0) {
tempErrors['message'] = true
isValid = false
}
setErrors({ ...tempErrors })
console.log('errors', errors)
return isValid
}
const handleSubmit = async (e) => {
e.preventDefault()
let isValidForm = handleValidation()
if (isValidForm) {
setButtonText('Sending')
const res = await fetch('/api/sendgrid', {
body: JSON.stringify({
email: email,
name: name,
subject: subject,
message: message
}),
headers: {
'Content-Type': 'application/json'
},
method: 'POST',
})
const { error } = await res.json()
if (error) {
console.log(error)
setShowSuccessMessage(false)
setShowFailureMessage('Error, please complete all sections')
setButtonText('Submit')
console.log('success is ', showSuccessMessage)
console.log('failure is ', showFailureMessage)
return
}
setShowSuccessMessage('Contact form submitted')
setShowFailureMessage(false)
console.log('success is ', showSuccessMessage)
console.log('failure is ', showFailureMessage)
setButtonText('Send')
}
console.log(name, email, subject, message)
}
return (
<div className={style.container}>
<h1 className={style.title}>Get in Touch</h1>
<form className={style.form}>
<input className={style.inputS} type="text" placeholder="Name" id='name' />
<input className={style.inputS} type="text" placeholder="Phone" id='phone' />
<input className={style.inputL} type="text" placeholder="Email" id='email' />
<input className={style.inputL} type="text" placeholder="Subject" id='subject' />
<textarea
className={style.textArea}
type="text"
rows={6}
placeholder="Message"
id='message'
/>
<button className={style.button} onClick={handleSubmit}
>{buttonText}
</button>
<p className='error' >
{showSuccessMessage}
{showFailureMessage}
'test'
</p>
</form>
</div>
);
}
Set state is an asynchronous operation, so log the state after setting it, will log the old value.
If you want to sure that the value has been setten correctly, you need to use setState.
setShowSuccessMessage('Contact form submitted')
setShowFailureMessage(false)
setShowSuccessMessage(prev => {
console.log(prev)
return prev
})// log the 'Contact form submitted'
setShowFailureMessage(prev => {
console.log(prev)
return prev
})// log false
And setting state, in this case, will not make the component re-render as you return the same value.
The reason is that setXxx methods are async. If you click submit 10 times fast enough, you will see different results.
You have two options:
not preferred: If you don't use 'controlled' input (you are not using it now - in input elements, you are not using things like 'value={name}'). then you don't need the state at all.
Use controlled input. You can check document on reactjs org.
Simply put,
useState
on the input tag, add value={name} onChange={changeHandler},
in changHandler, setState
in formSubmit, use the value in state
There is a section on forms on this excellent tutorial:
Bob Ziroll free React Course
You are using react so you need to set data and use those data after that i have updated your code it should work for you. i.e. you need to set value in onChange function as below:-
import style from "../styles/Contact.module.css";
import React, { useState } from 'react'
const style = {};
export default function Contact() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [subject, setSubject] = useState('')
const [message, setMessage] = useState('')
const [errors, setErrors] = useState({})
const [buttonText, setButtonText] = useState('Submit')
const [showSuccessMessage, setShowSuccessMessage] = useState(false)
const [showFailureMessage, setShowFailureMessage] = useState(false)
const handleValidation = () => {
let tempErrors = {}
let isValid = true
console.log(name, email, subject, message)
if (name.length <= 0) {
tempErrors['name'] = true
isValid = false
}
if (email.length) {
tempErrors['email'] = true
isValid = false
}
if (subject.length) {
tempErrors['subject'] = true
isValid = false
}
if (message.length <= 0) {
tempErrors['message'] = true
isValid = false
}
setErrors({ ...tempErrors })
console.log('errors', errors)
return isValid
}
const handleSubmit = async (e) => {
e.preventDefault()
let isValidForm = handleValidation()
if (isValidForm) {
setButtonText('Sending')
const res = await fetch('/api/sendgrid', {
body: JSON.stringify({
email: email,
name: name,
subject: subject,
message: message
}),
headers: {
'Content-Type': 'application/json'
},
method: 'POST',
})
const { error } = await res.json()
if (error) {
console.log(error)
setShowSuccessMessage(false)
setShowFailureMessage('Error, please complete all sections')
setButtonText('Submit')
console.log('success is ', showSuccessMessage)
console.log('failure is ', showFailureMessage)
return
}
setShowSuccessMessage('Contact form submitted')
setShowFailureMessage(false)
console.log('success is ', showSuccessMessage)
console.log('failure is ', showFailureMessage)
setButtonText('Send')
}
console.log(name, email, subject, message)
}
return (
<div className={style.container}>
<h1 className={style.title}>Get in Touch</h1>
<form className={style.form}>
<input className={style.inputS} type="text" placeholder="Name" id='name' onChange={(e) => setName(e.target.value)} />
<input className={style.inputS} type="text" placeholder="Phone" id='phone' />
<input className={style.inputL} type="text" placeholder="Email" id='email' onChange={(e) => setEmail(e.target.value)} />
<input className={style.inputL} type="text" placeholder="Subject" id='subject' onChange={(e) => setSubject(e.target.value)} />
<textarea
className={style.textArea}
type="text"
rows={6}
placeholder="Message"
id='message'
onChange={(e) => setMessage(e.target.value)}
/>
<button className={style.button} onClick={handleSubmit}
>{buttonText}
</button>
<p className='error' >
{showSuccessMessage}
{showFailureMessage}
'test'
</p>
</form>
</div>
);
}
And if you want same for phone you can also set data for the same and use after that.

How to Sort Form Input Automatically With React Hooks

I have a form where user can enter a name that will then be displayed on a list. Upon entering a new name the list should automatically be sorted in alphabetical order. Current attempt with useEffect does work but is buggy(list will only be sorted after user start deleting previous input text).
A few notable things to highlight with current setup:
Submission component is used for rendering list of names
Form component is used to store state of app and input fields
handleSortName() will execute sorting
useEffect() executes handleSortName() when there is a change to submissions value
import React, { useEffect, useState } from "react";
const Submission = ({ submission }) => {
return <div>name: {submission.name}</div>;
};
const Form = () => {
const [values, setValues] = useState({
name: ""
});
const [submissions, setSubmission] = useState([
{ name: "John" }
]);
const addSubmission = (values) => {
const newSubmissions = [...submissions, values];
setSubmission(newSubmissions);
};
const handleChange = (event) => {
const value = event.target.value;
setValues({ ...values, [event.target.name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
addSubmission(values);
handleSortName(submissions);
};
const handleSortName = (submissions) => {
return submissions.sort((a, b) => a.name.localeCompare(b.name));
};
useEffect(() => {
handleSortName(submissions);
}, [submissions]);
return (
<>
<form onSubmit={handleSubmit}>
<h1>Student Enrollment</h1>
<div>
<label>name: </label>
<input
required
type="text"
name="name"
value={values.name}
onChange={handleChange}
/>
<input type="submit" value="Submit" />
</div>
</form>
<h1>Submitted Student</h1>
{submissions.map((submission, index) => (
<Submission key={index} submission={submission} />
))}
</>
);
};
export default Form;
Working Sample: https://codesandbox.io/s/usestate-form-oj61v9?file=/src/Form.js
I am aware that useState is asynchronous and will not update value right away.
Any suggestion on other implementations such as functional updates, a custom hook or current UseEffect approach? Thanks in Advance!
UPDATE:
because React re-renders the component when the props or state changes. that means inside your handleSortName() function you have to call setSubmissions with the new sorted array, then React will know that the state was changed.
const handleSortName = (submissions) => {
// create a new copy of the array with the three dots operator:
let copyOfSubmissions = [...submissions];
// set the state to the new sorted array:
setSubmissions(
copyOfSubmissions.sort((a, b) => a.name.localeCompare(b.name))
);
};
or you can do both steps in 1 line:
const handleSortName = (submissions) => {
// set the state to the newly created sorted array with the three dots operator:
setSubmissions(
[...submissions].sort((a, b) => a.name.localeCompare(b.name))
);
};
sandbox link here

How to read the value of updated array while using useState hook in react?

I am trying to build my 1st ToDo-list-app in React & I can't seem to successfully read the data from my "list" array which I've initiated using useState([]).
The issue I'm facing is - if my 1st entered task is "1-Eat Breakfast" & I click on the add button, I'm getting an empty array in console.log,
When I enter my 2nd task lets say - "2-Hit the gym"; that's when my previous task is getting console logged. So, apparently I am unable to read the latest state of my array - "list".
Can you please point out what I am doing wrong in the code given below?
Thanks a lot.
import { useState } from "react";
const ToDo = () => {
const [task, setTask] = useState("");
const [list, setList] = useState([]);
const readData = (event) => {
event.preventDefault();
setTask(event.target.value);
};
const showList = (event) => {
event.preventDefault();
setList([...list, task]);
console.log(list);
};
return (
<div>
<div>
<form onSubmit={showList}>
<input type="text" value={task} onChange={readData} />
<button>Add to List</button>
</form>
</div>
</div>
);
};
export default ToDo;
can you change
const showList = (event) => {
event.preventDefault();
setList([...list, task]);
console.log(list);
};
to
const showList = (event) => {
event.preventDefault();
list.push(task);
setList(list);
console.log(list);
};

Resources