How to validate Form in React by pressing submit button - javascript

i am learning React and working on my pet-project. So, i have a form with custom input components and I want to validate them only when the user clicks on the button, but I don't know how. Full input check is whether the form is not empty and filled out correctly. I tried to create a model for subscribing components to the button click event, but when the form was filled correctly, the states were updated not after 1, but after 2 clicks, and I don't know why.
Screen of form in browser:
Code of Form component:
import React from 'react';
import { FormBox } from '../FormBox';
import styles from './checkoutpage.module.scss'
export function CheckoutPage() {
return (
<div className={styles.container}>
<div className={styles.formContainer}>
<h2>checkout</h2>
<legend>Contact info</legend>
<FormBox
type='email'
placeholder='Email address'
errorMessage='Enter a valid email address'
/>
<legend>Shipping info</legend>
<FormBox
type='name'
placeholder='Name'
/>
<FormBox
type='text'
placeholder='Streer address'
/>
<FormBox
type='text'
placeholder='Apt / Suite / Other (optional)'
/>
<div className={styles.divideBox}>
<FormBox
type='text'
placeholder='City'
/>
<FormBox
type='text'
placeholder='Province'
/>
</div>
<div className={styles.divideBox}>
<FormBox
type='text'
placeholder='Postal Code'
/>
<FormBox
type='text'
placeholder='Country'
/>
</div>
<FormBox
type='text'
placeholder='Phone Number'
/>
</div>
</div>
);
}
Code of Input component:
import React, { useEffect, useRef, useState } from "react";
import styles from "./formbox.module.scss";
interface IFormBox {
type: string;
placeholder: string;
errorMessage?: string;
}
export function FormBox({ type, placeholder, errorMessage }: IFormBox) {
const [valid, setValid] = useState(true);
const ref = useRef<HTMLInputElement>(null);
const checkValidityChange = () => {
if (ref.current) {
setValid(ref.current.checkValidity());
}
}
useEffect(() => {
if (ref.current) {
if (valid && ref.current.value !== "") {
ref.current.style.backgroundColor = "#e8f0fe";
}
else {
ref.current.style.backgroundColor = "white";
}
}
}, [valid])
return (
<div className={styles.container}>
<input
ref={ref}
onChange={checkValidityChange}
name="formInput" type={type}
placeholder={placeholder}
onBlur={() => {if (ref.current && ref.current.value === "") ref.current.style.backgroundColor = "white";}}
/>
{!valid && errorMessage && (
<span>{'* '.concat(errorMessage)}</span>
)}
</div>
);
}
Method i tried to use:
Form component:
import React, { useEffect, useRef, useState } from 'react';
import { FormBox } from '../FormBox';
import styles from './checkoutpage.module.scss'
export function CheckoutPage() {
const [formValidity, setFormValidity] = useState(false);
const [emailValidity, setEmailValidity] = useState(false);
const [nameValidity, setNameValidity] = useState(false);
const [addressValidity, setAddressValidity] = useState(false);
const [aptValidity, setAptValidity] = useState(false);
const [cityCalidity, setCityValidity] = useState(false);
const [provinceValidity, setProvinceValidity] = useState(false);
const [codeValidity, setCodeValidity] = useState(false);
const [countryValidity, setCountryValidity] = useState(false);
const [phoneValidity, setPhoneValidity] = useState(false);
const refButton = useRef<HTMLButtonElement>(null);
function subscrition (event: MouseEvent, setValidity: (v: boolean) => void, setValidityValue: boolean) {
if (event.target === refButton.current) {
setValidity(setValidityValue);
}
}
const getValidity = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
const result = emailValidity && nameValidity && addressValidity && aptValidity && cityCalidity && provinceValidity && codeValidity && countryValidity && phoneValidity
setFormValidity(result)
console.log(formValidity)
}
return (
<div className={styles.container}>
<form className={styles.formContainer}>
<h2>checkout</h2>
<legend>Contact info</legend>
<FormBox
subscrition={subscrition}
type='email'
placeholder='Email address'
errorMessage='Enter a valid email address'
setFullValidity={setEmailValidity}
/>
<legend>Shipping info</legend>
<FormBox
subscrition={subscrition}
type='name'
placeholder='Name'
setFullValidity={setNameValidity}
/>
<FormBox
subscrition={subscrition}
type='text'
placeholder='Streer address'
setFullValidity={setAddressValidity}
/>
<FormBox
subscrition={subscrition}
type='text'
placeholder='Apt / Suite / Other (optional)'
setFullValidity={setAptValidity}
/>
<div className={styles.divideBox}>
<FormBox
subscrition={subscrition}
type='text'
placeholder='City'
setFullValidity={setCityValidity}
/>
<FormBox
subscrition={subscrition}
type='text'
placeholder='Province'
setFullValidity={setProvinceValidity}
/>
</div>
<div className={styles.divideBox}>
<FormBox
subscrition={subscrition}
type='text'
placeholder='Postal Code'
setFullValidity={setCodeValidity}
/>
<FormBox
subscrition={subscrition}
type='text'
placeholder='Country'
setFullValidity={setCountryValidity}
/>
</div>
<FormBox
subscrition={subscrition}
type='text'
placeholder='Phone Number'
setFullValidity={setPhoneValidity}
/>
<button ref={refButton} onClick={getValidity} type="submit" className={styles.btn}>Place Your Order</button>
{formValidity && (
<span style={{backgroundColor: "green"}}>Form is valid</span>
)}
{!formValidity && (
<span style={{backgroundColor: "red"}}>Form is invalid</span>
)}
</form>
</div>
);
}
Input component:
import React, { useEffect, useRef, useState } from "react";
import styles from "./formbox.module.scss";
interface IFormBox {
type: string;
placeholder: string;
errorMessage?: string;
setFullValidity: (v: boolean) => void;
subscrition: (event: MouseEvent, setValidity: (v: boolean) => void, setValidityValue: boolean) => void;
}
export function FormBox({ type, placeholder, errorMessage, setFullValidity, subscrition }: IFormBox) {
const [valid, setValid] = useState(true);
const ref = useRef<HTMLInputElement>(null);
const checkValidityChange = () => {
if (ref.current) {
setValid(ref.current.checkValidity());
}
}
useEffect(() => {
if (ref.current) {
// #ts-ignore
document.addEventListener('click', (event: MouseEvent) => {subscrition(event, setFullValidity, valid && (ref.current.value !== ""))})
}
})
useEffect(() => {
if (ref.current) {
if (valid && ref.current.value !== "") {
ref.current.style.backgroundColor = "#e8f0fe";
} else {
ref.current.style.backgroundColor = "white";
}
}
}, [valid])
return (
<div className={styles.container}>
<input
ref={ref}
onChange={checkValidityChange}
name="formInput" type={type}
placeholder={placeholder}
onBlur={() => {
if (ref.current && ref.current.value === "") {
ref.current.style.backgroundColor = "white";
}
}}
/>
{!valid && errorMessage && (
<span>{'* '.concat(errorMessage)}</span>
)}
</div>
);
}
Any advice on how to improve the code will be very warmly welcomed. Thanks for reading, and sorry if i waste your time.

You can follow this example to validate input fields. Here input reference is used like ref to validate data.
WORKING DEMO
import React from "react";
import ReactDOM from "react-dom";
import "./style.css";
function validate(name, email, password) {
// we are going to store errors for all fields
// in a signle array
const errors = [];
if (name.length === 0) {
errors.push("Name can't be empty");
}
if (email.length < 5) {
errors.push("Email should be at least 5 charcters long");
}
if (email.split("").filter(x => x === "#").length !== 1) {
errors.push("Email should contain a #");
}
if (email.indexOf(".") === -1) {
errors.push("Email should contain at least one dot");
}
if (password.length < 6) {
errors.push("Password should be at least 6 characters long");
}
return errors;
}
class SignUpForm extends React.Component {
constructor() {
super();
this.state = {
errors: []
};
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSubmit(e) {
e.preventDefault();
const name = ReactDOM.findDOMNode(this._nameInput).value;
const email = ReactDOM.findDOMNode(this._emailInput).value;
const password = ReactDOM.findDOMNode(this._passwordInput).value;
const errors = validate(name, email, password);
if (errors.length > 0) {
this.setState({ errors });
return;
}
// submit the data...
}
render() {
const { errors } = this.state;
return (
<form onSubmit={this.handleSubmit}>
{errors.map(error => (
<p key={error}>Error: {error}</p>
))}
<input
ref={nameInput => (this._nameInput = nameInput)}
type="text"
placeholder="Name"
/>
<input
ref={emailInput => (this._emailInput = emailInput)}
type="text"
placeholder="Email"
/>
<input
ref={passwordInput => (this._passwordInput = passwordInput)}
type="password"
placeholder="Password"
/>
<button type="submit">Submit</button>
</form>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<SignUpForm />, rootElement);
More Details

Related

How to use react-router to update an edit form in react.js

I want to create an edit form so that I can be able to edit a contact, and update it using react-router. I passed in the state through the router link, then I receive it using the useLocation. But the problem I am facing right now is that I do not know how to use the received data to initialize my form when I click on any contact to edit. I meant, if I should click on edit, it should load the information on that contact I click on, then I will update it.
This is where I pass in the state to the edit button
import classes from './ContactItem.module.css'
import { Fragment, useState} from 'react'
import CustomButton from '../UI/CustomButton'
import {Link} from 'react-router-dom'
import {BiEdit} from 'react-icons/bi'
import {RiDeleteBinLine} from 'react-icons/ri'
import DeleteContact from '../UI/DeleteContact'
const ContactItem = (props) => {
const { id, firstName, lastName, phone, email} = props.contact;
const [show, setShow] = useState(false);
const handleShow = () => {
setShow(true);
}
return (
<Fragment >
<div className={classes.container}>
<div className={classes.name}>
<div>{firstName}</div>
<div>{lastName}</div>
</div>
<div className={classes.utilities}>
<div className={classes.contact}>
<div className={classes.email}>{email}</div>
<div className={classes.phone}>{phone}</div>
</div>
<div className={classes.utility}>
<Link to={`/edit/${id}`} state={props.contact }>
<div className={classes.edit}>
<BiEdit />
</div>
</Link>
<div className={classes.delete}>
<RiDeleteBinLine onClick={handleShow} />
</div>
</div>
</div>
<hr className={classes.hr}/>
</div>
<div className={classes.button}>
<Link to={`/${id}`}>
<CustomButton>view Details</CustomButton>
</Link>
</div>
<DeleteContact show={show} setShow={setShow} onDelete={id} />
</Fragment>
)
}
export default ContactItem
The below code is where I receive the data that is passed through the link. This is also the editForm components. I am using custom hook for my edit form component
import React from 'react'
import useInput from '../formHooks/hooks/use-input'
import './EditForm.css'
import LoadingSpinner from './LoadingSpinner'
import {useLocation} from 'react-router-dom'
const EditForm = (props) => {
const location = useLocation();
const data = location.state;
console.log(data)
const {
value: enteredFirstName,
isValid: enteredFirstNameIsValid,
hasError: firstNameInputHasError,
InputBlurHandler: firstNameBlurHandler,
valueChangeHandler: firstNameChangedHandler,
reset: resetFirstNameInput
} = useInput(value => value.trim() !== "")
const {
value: enteredLastName,
isValid: enteredLastNameIsValid,
hasError: lastNameInputHasError,
InputBlurHandler: lastNameBlurHandler,
valueChangeHandler: lastNameChangedHandler,
reset: resetLastNameInput
} = useInput(value => value.trim() !== "")
const {
value: enteredEmail,
isValid: enteredEmailIsValid,
hasError: emailInputHasError,
InputBlurHandler: emailBlurHandler,
valueChangeHandler: emailChangedHandler,
reset: resetEmailInput
} = useInput(value => value.includes('#'))
const {
value: enteredPhoneNumber,
isValid: enteredPhoneNumberIsValid,
hasError: phoneNumberInputHasError,
InputBlurHandler: phoneNumberBlurHandler,
valueChangeHandler: phoneNumberChangedHandler,
reset: resetPhoneNumberInput
} = useInput(value => value.trim() !== "")
const {
value: enteredAddress1,
isValid: enteredAddress1IsValid,
hasError: address1InputHasError,
InputBlurHandler: address1BlurHandler,
valueChangeHandler: address1ChangedHandler,
reset: resetAddress1Input
} = useInput(value => value.trim() !== "")
const {
value: enteredAddress2,
valueChangeHandler: address2ChangedHandler,
reset: resetAddress2Input
} = useInput(value => value.trim() == "")
const {
value: enteredState,
valueChangeHandler: stateChangedHandler,
reset: resetStateInput
} = useInput(value => value.trim() == "")
const {
value: enteredCountry,
valueChangeHandler: countryChangedHandler,
reset: resetCountryInput
} = useInput(value => value.trim() == "")
const {
value: enteredFile,
valueChangeHandler: fileChangedHandler,
reset: resetFileInput
} = useInput(value => value.trim() == "")
const {
value: enteredAbout,
isValid: enteredAboutIsValid,
hasError: aboutInputHasError,
InputBlurHandler: aboutBlurHandler,
valueChangeHandler: aboutChangedHandler,
reset: resetAboutInput
} = useInput(value => value.trim() !== "")
let formIsValid = false
if (enteredFirstNameIsValid
&& enteredEmailIsValid
&& enteredLastNameIsValid
&& enteredPhoneNumberIsValid
&& enteredAddress1IsValid
&& enteredAboutIsValid
) {
formIsValid = true
} else {
formIsValid = false
}
const formSubmitHandler = (e) => {
e.preventDefault();
if (!enteredFirstNameIsValid
&& !enteredLastNameIsValid
&& !enteredEmailIsValid
&& !enteredPhoneNumberIsValid
&& !enteredAddress1IsValid
&& !enteredAboutIsValid
) {
return;
}
resetFirstNameInput();
resetEmailInput();
resetLastNameInput();
resetPhoneNumberInput();
resetAddress1Input();
resetAddress2Input();
resetStateInput();
resetCountryInput();
resetFileInput();
resetAboutInput();
props.onSaveContact({
firstName: enteredFirstName,
lastName: enteredLastName,
email: enteredEmail,
phone: enteredPhoneNumber,
address1: enteredAddress1,
address2: enteredAddress2,
state: enteredState,
country: enteredCountry,
file: enteredFile,
about: enteredAbout
})
}
const firstNameInputClasses = firstNameInputHasError ? 'form-control invalid' : 'form-control'
const emailInputClasses = emailInputHasError ? 'form-control invalid' : 'form-control'
const lastNameInputClasses = lastNameInputHasError ? 'form-control invalid' : 'form-control'
const phoneNumberInputClasses = phoneNumberInputHasError ? 'form-control invalid' : 'form-control'
const address1InputClasses = address1InputHasError ? 'form-control invalid' : 'form-control'
const aboutInputClasses = aboutInputHasError ? 'form-control invalid' : 'form-control'
return (
<form onSubmit={formSubmitHandler}>
<div className='container'>
{props.isLoading && (
<div className='loading'>
<LoadingSpinner />
</div>
)}
<div className='control-group'>
<div className={firstNameInputClasses}>
<label htmlFor='firstName'>First Name</label>
<input
type='text'
id='firstName'
onChange={firstNameChangedHandler}
onBlur={firstNameBlurHandler}
value={enteredFirstName}
/>
{firstNameInputHasError &&
<p className="error-text">First Name must not be empty</p>
}
</div>
<div className={lastNameInputClasses}>
<label htmlFor="lastName">Last Name</label>
<input
type='text'
id="lastName"
onChange={lastNameChangedHandler}
onBlur={lastNameBlurHandler}
value={enteredLastName}
/>
{lastNameInputHasError &&
<p className="error-text">Enter a valid name</p>
}
</div>
</div>
<div className={emailInputClasses}>
<label htmlFor='email'>Email Address</label>
<input
type='email'
id='email'
onChange={emailChangedHandler}
onBlur={emailBlurHandler}
value={enteredEmail}
/>
{emailInputHasError &&
<p className="error-text">Enter a valid email</p>
}
</div>
<div className={phoneNumberInputClasses}>
<label htmlFor='phoneNumber'>Phone Number</label>
<input
type='text'
inputMode='numeric'
onChange={phoneNumberChangedHandler}
onBlur={phoneNumberBlurHandler}
value={enteredPhoneNumber}
/>
{phoneNumberInputHasError &&
<p className="error-text">Number must not be empty</p>
}
</div>
<div className={address1InputClasses}>
<label htmlFor='address1'>Address 1</label>
<input
type='text'
id='address1'
onChange={address1ChangedHandler}
onBlur={address1BlurHandler}
value={enteredAddress1}
/>
{address1InputHasError &&
<p className="error-text">Address must not be empty</p>
}
</div>
<div className='form-control'>
<label htmlFor='address2'>Address 2</label>
<input
type='text'
id='address2'
onChange={address2ChangedHandler}
value={enteredAddress2}
/>
</div>
<div className='country'>
<div className='form-control'>
<label htmlFor="country">Country</label>
<input
type='text'
id='country'
onChange={countryChangedHandler}
value={enteredCountry}
/>
</div>
<div className='form-control'>
<label htmlFor="state">State</label>
<input
type='text'
id='state'
onChange={stateChangedHandler}
value={enteredState}
/>
</div>
</div>
<div className='form-control'>
<label htmlFor="file">Select Image</label>
<input
type='file'
id="file"
onChange={fileChangedHandler}
value={enteredFile}
/>
</div>
<div className={aboutInputClasses}>
<label htmlFor='about'>About</label>
<textarea
type='text'
id='about'
rows='5'
cols='50'
onChange={aboutChangedHandler}
onBlur={aboutBlurHandler}
value={enteredAbout}
/>
{aboutInputHasError &&
<p className="error-text">Your input text must not below 100 characters</p>
}
</div>
<div className='form-actions'>
<button disabled={!formIsValid}>Update</button>
<button>Cancel</button>
</div>
</div>
</form>
)
}
export default EditForm
Below is the custom hook code I am using for the form
import { useState } from "react";
const useInput = (validateValue) => {
const [enteredValue, setEnteredValue] = useState('')
const [isTouched, setIsTouched] = useState(false)
const valueIsValid = validateValue(enteredValue);
const hasError = !valueIsValid && isTouched;
const valueChangeHandler = (e) => {
setEnteredValue(e.target.value)
// setEnteredNameIsValid(true)
}
const InputBlurHandler = (e) => {
setIsTouched(true)
}
const reset = () => {
setEnteredValue('');
setIsTouched(false)
}
return {
value: enteredValue,
isValid: valueIsValid,
hasError,
valueChangeHandler,
InputBlurHandler,
reset
}
};
export default useInput;
The basic gist is that you need to pass the passed route state value(s) to the form state. Ideally you'd want to initialize the field state to a value instead of an empty string. You could try the following refactor.
const useInput = ({
initialValue = '',
validateValue = () => true,
}) => {
const [enteredValue, setEnteredValue] = useState(initialValue || '');
const [isTouched, setIsTouched] = useState(false);
const valueIsValid = validateValue(enteredValue);
const hasError = !valueIsValid && isTouched;
const valueChangeHandler = (e) => {
setEnteredValue(e.target.value);
// setEnteredNameIsValid(true);
};
const InputBlurHandler = (e) => {
setIsTouched(true);
};
const reset = () => {
setEnteredValue(initialValue);
setIsTouched(false);
};
return {
value: enteredValue,
isValid: valueIsValid,
hasError,
valueChangeHandler,
InputBlurHandler,
reset
};
};
...
const location = useLocation();
const data = location.state;
const {
value: enteredFirstName,
isValid: enteredFirstNameIsValid,
hasError: firstNameInputHasError,
InputBlurHandler: firstNameBlurHandler,
valueChangeHandler: firstNameChangedHandler,
reset: resetFirstNameInput
} = useInput({
initialValue: data.firstName,
validateValue: value => value.trim() !== "",
});
...

How to pass multiple onChange form data from child to parent element in react

I am trying to print real-time user input from input tags by the user. I am even getting multiple user inputs from the child element to the parent element as a form of an object using useState. But whenever the user tries to fill the second input field, then the first input is re-render and it's replaced by the primary state which is an empty string.
code:-
Child Element
import React, { useState } from "react";
const Child = (props) => {
const [name, setName] = useState("");
const [age, setAge] = useState("");
let userData = {
name: "",
age: ""
};
const nameChangeHandler = (e) => {
setName(e.target.value);
userData.name = e.target.value;
};
const ageChangeHandler = (e) => {
setAge(e.target.value);
userData.age = e.target.value;
};
const formOnChageHandler = (e) => {
e.preventDefault();
props.getData(userData);
};
const fromOnSubmitHandler = (e) => {
e.preventDefault();
};
return (
<React.Fragment>
<form onChange={formOnChageHandler} onSubmit={fromOnSubmitHandler}>
<label htmlFor="name">Name:</label>
<input
id="name"
placeholder="Enter Name"
value={name}
onChange={nameChangeHandler}
/>
<br />
<label htmlFor="age">Age:</label>
<input
id="age"
placeholder="Enter Age"
value={age}
onChange={ageChangeHandler}
/>
</form>
</React.Fragment>
);
};
export default Child;
Parent Element
import React, { useState } from "react";
import Child from "./components/Child";
function App() {
const [name, setName] = useState("");
const [age, setAge] = useState("");
let userData = (data) => {
setName(data.name);
setAge(data.age);
};
return (
<React.Fragment>
<Child getData={userData} />
<h1>Your name is:{name}</h1>
<h1>Your age is:{age}</h1>
</React.Fragment>
);
}
export default App;
code sandbox Link- https://codesandbox.io/s/from-traversing-child-to-parent-to-another-child-ynwyqd?file=/src/App.js:0-441
How I can get both data being reflected by using onChange from child to parent element?
I suggest you accumulate the user data in one state.
Like this.
const [user, setUser] = useState({
name: "",
age: null
});
And put the state on the parent and pass as props, also just have one handleChange function to update both the name and age by the input id
Child.js
import React, { useState } from "react";
const Child = ({ user, setUser }) => {
const handleChange = (e) => {
setUser((prev) => ({
...prev,
[e.target.id]: e.target.value
}));
};
const formOnChageHandler = (e) => {
e.preventDefault();
};
const fromOnSubmitHandler = (e) => {
e.preventDefault();
};
return (
<React.Fragment>
<form onChange={formOnChageHandler} onSubmit={fromOnSubmitHandler}>
<label htmlFor="name">Name:</label>
<input
id="name"
placeholder="Enter Name"
value={user.name}
onChange={handleChange}
/>
<br />
<label htmlFor="age">Age:</label>
<input
id="age"
placeholder="Enter Age"
value={user.age}
onChange={handleChange}
/>
</form>
</React.Fragment>
);
};
export default Child;
App.js
import React, { useState } from "react";
import Child from "./components/Child";
function App() {
const [user, setUser] = useState({
name: "",
age: null
});
return (
<React.Fragment>
<Child user={user} setUser={setUser} />
<h1>Your name is:{user.name}</h1>
<h1>Your age is:{user.age}</h1>
</React.Fragment>
);
}
export default App;
CODESANDBOX
Try using the child component as below,
import React, { useState } from "react";
const Child = (props) => {
const [name, setName] = useState("");
const [age, setAge] = useState("");
let userData = {
name: name, // the value "name" comes for the local state will be listen to the onChange event every time
age: age // same applies here as well
};
const nameChangeHandler = (e) => {
setName(e.target.value);
};
const ageChangeHandler = (e) => {
setAge(e.target.value);
};
const formOnChageHandler = (e) => {
e.preventDefault();
props.getData(userData);
};
const fromOnSubmitHandler = (e) => {
e.preventDefault();
};
return (
<React.Fragment>
<form onSubmit={fromOnSubmitHandler}>
<label htmlFor="name">Name:</label>
<input
id="name"
placeholder="Enter Name"
value={name}
onChange={nameChangeHandler}
/>
<br />
<label htmlFor="age">Age:</label>
<input
id="age"
placeholder="Enter Age"
value={age}
onChange={ageChangeHandler}
/>
</form>
</React.Fragment>
);
};
export default Child;
just use simple one state to manage data. just take a look below example component created from your child component.
we simply use single object state.
use name prop as key to store value in state.
import React, { useState } from "react";
const Child = (props) => {
const [formData, setFormData] = useState({});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const formOnChageHandler = (e) => {
e.preventDefault();
props.getData(userData);
};
const fromOnSubmitHandler = (e) => {
e.preventDefault();
};
return (
<React.Fragment>
<form onChange={formOnChageHandler} onSubmit={fromOnSubmitHandler}>
<label htmlFor="name">Name:</label>
<input
id="name"
placeholder="Enter Name"
value={name}
onChange={handleChange}
name="name"
/>
<br />
<label htmlFor="age">Age:</label>
<input
id="age"
placeholder="Enter Age"
value={age}
onChange={handleChange}
name="age"
/>
</form>
</React.Fragment>
);
};
export default Child;
<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>
it happens because you dont watch to the state, try this:
Child.js
import React, { useState } from "react";
const Child = (props) => {
const [name, setName] = useState("");
const [age, setAge] = useState("");
let userData = {
name,
age
};
const nameChangeHandler = (e) => {
setName(e.target.value);
userData.name = e.target.value;
};
const ageChangeHandler = (e) => {
setAge(e.target.value);
userData.age = e.target.value;
};
const formOnChageHandler = (e) => {
e.preventDefault();
props.getData(userData);
};
const fromOnSubmitHandler = (e) => {
e.preventDefault();
};
return (
<React.Fragment>
<form onChange={formOnChageHandler} onSubmit={fromOnSubmitHandler}>
<label htmlFor="name">Name:</label>
<input
id="name"
placeholder="Enter Name"
value={name}
onChange={nameChangeHandler}
/>
<br />
<label htmlFor="age">Age:</label>
<input
id="age"
placeholder="Enter Age"
value={age}
onChange={ageChangeHandler}
/>
</form>
</React.Fragment>
);
};
export default Child;
Try this, i check in codesandbox and it works:
In App.js:
import React, { useState } from "react";
import Child from "./components/Child";
function App() {
const [name, setName] = useState("");
const [age, setAge] = useState("");
return (
<React.Fragment>
<Child name={name} age={age} setName={setName} setAge={setAge} />
<h1>Your name is:{name}</h1>
<h1>Your age is:{age}</h1>
</React.Fragment>
);
}
export default App;
In Child.js:
import React, { useState } from "react";
const Child = ({ name, age, setName, setAge }) => {
const nameChangeHandler = (e) => {
setName(e.target.value);
};
const ageChangeHandler = (e) => {
setAge(e.target.value);
};
const fromOnSubmitHandler = (e) => {
e.preventDefault();
};
return (
<React.Fragment>
<form onSubmit={fromOnSubmitHandler}>
<label htmlFor="name">Name:</label>
<input
id="name"
placeholder="Enter Name"
value={name}
onChange={nameChangeHandler}
/>
<br />
<label htmlFor="age">Age:</label>
<input
id="age"
placeholder="Enter Age"
value={age}
onChange={ageChangeHandler}
/>
</form>
</React.Fragment>
);
};
export default Child;
If you want to improve your code, you can research and use state management like: redux, zustand, react context,...
Hope it useful for you.

React: How to change form validation from onSubmit validation for all fields, to onBlur instant validation for each field in the form?

I have the form which is functioning great, but I need to make the validation instant (as soon as the user defocuses the field, I want the error message shown for that field only). At the moment I have a validation that shows error messages when the submit button is clicked:
Here is the code:
The useForm hook
import { useState, useEffect } from 'react';
const useForm = (callback, validate, post) => {
const [values, setValues] = useState(post || {});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
if (Object.keys(errors).length === 0 && isSubmitting) {
callback();
}
}, [errors]);
const handleSubmit = (event) => {
if (event) event.preventDefault();
console.log('values in useForm', values)
setErrors(validate(values))
setIsSubmitting(true);
};
const handleInputChange = (event) => {
event.persist();
setValues(values => ({ ...values, [event.target.name]: event.target.value }));
};
return {
handleInputChange,
handleSubmit,
values,
errors,
}
};
export default useForm;
The validate function
const validate = (values) => {
const errors = {};
if (!values.title) {
errors.title = 'Title is required'
} else if (values.title.length < 5) {
errors.title = 'Title must be at least 5 characters long'
}
if (!values.body) {
errors.body = "Blog body is required"
} else if (values.body.length < 2 || values.body.length > 20) {
errors.body = "Text has to be between 2 and 20 characters long"
}
if (!values.author) {
errors.author = "The author's name is required"
}
if (!values.number) {
errors.number = "A number is required"
}
if (!values.email) {
errors.email = 'Email is required';
} else if (
!/^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
) {
errors.email = 'Invalid email address';
}
return errors;
}
export default validate;
The form component
import { useHistory } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import useForm from '../hooks/useForm';
import { addPost } from '../actions';
import validate from '../helpers/validate';
const CreateForm = () => {
const dispatch = useDispatch();
const history = useHistory();
const { values, handleInputChange, handleSubmit, errors } = useForm(submit, validate)
const { title, body, author, number, email } = values;
function submit() {
console.log("No errors", values)
const post = values;
console.log('Submit form', post)
dispatch(addPost(post))
history.push('/');
}
return (
<div className="create">
<h2>Add a new blog</h2>
<form onSubmit={handleSubmit} noValidate>
<label>Blog title:</label>
<input
type="text"
required
name="title"
value={title || ""}
onChange={handleInputChange}
className={errors.title ? 'red-border' : ""}
/>
{errors.title && (<p className="danger">{errors.title}</p>)}
<label>Blog body:</label>
<textarea
required
name="body"
value={body || ""}
onChange={handleInputChange}
className={errors.body ? 'red-border' : ""}
/>
{errors.body && (
<p className="danger">{errors.body}</p>
)}
<label>Author:</label>
<input
type="text"
required
name="author"
value={author || ""}
onChange={handleInputChange}
className={errors.author ? 'red-border' : ""}
/>
{errors.author && (
<p className="danger">{errors.author}</p>
)}
<label>Number:</label>
<input
type="number"
required
name="number"
value={number || ""}
onChange={handleInputChange}
className={errors.number ? 'red-border' : ""}
/>
{errors.number && (
<p className="danger">{errors.number}</p>
)}
<label>Email:</label>
<input
type="text"
required
name="email"
value={email || ""}
onChange={handleInputChange}
className={errors.email ? 'red-border' : ""}
/>
{errors.email && (
<p className="danger">{errors.email}</p>
)}
<button>Save</button>
</form>
</div>
);
}
export default CreateForm;
As dicussed in the comments, you just need to call your setError when you want to update your error helpers. Here's a live example that flags an error if you type "error" in any of the fields: https://codesandbox.io/s/wispy-monad-3t8us?file=/src/App.js
const validateInputs = (e) => {
console.log("validating inputs");
if (e.target.value === "error")
setErrors({ ...errors, [e.target.name]: true });
else setErrors({ ...errors, [e.target.name]: false });
};
<input
type="text"
required
name="title"
value={values.title}
onChange={handleInputChange}
style={errors.title ? { border: "2px solid red" } : null}
onBlur={validateInputs}
/>
As you said in your question, you should call validate() in onBlur instead of in onSubmit.
So just add onBlur event in each of your input:
<input
type="number"
required
name="number"
onBlur={validate}
onChange={handleInputChange}
/>

React Too many re-renders

I am following the serverless-stack.com tutorial. But I am stuck after creating the login button.
I keep getting the error:
Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
I don't know what is causing the render so many times.
I combined my LoaderButton instead of importing to make it simpler.
import React, { useState } from "react";
import { Auth } from "aws-amplify";
import { useHistory } from "react-router-dom";
import { FormGroup, FormControl, ControlLabel } from "react-bootstrap";
import { useFormFields } from "../libs/hooksLib";
import { onError } from "../libs/errorLib";
import "../css/index.css";
const LoaderButton = (
isLoading,
className = "",
disabled = false,
...props ) => {
return(
<Button
className={`LoaderButton ${className}`}
disabled={disabled || isLoading}
{...props}
>
{isLoading && <Glyphicon glyph="refresh" className="spinning" />}
{props.children}
</Button>
)
};
export default function Login() {
let history = useHistory();
const [isLoading, setIsLoading] = useState(false);
const [fields, handleFieldChange] = useFormFields({
email: "",
password: ""
});
function validateForm() {
return fields.email.length > 0 && fields.password.length > 0;
}
async function handleSubmit(event) {
event.preventDefault();
setIsLoading(true);
try {
await Auth.signIn(fields.email, fields.password);
userHasAuthenticated(true);
console.log(history);
//history.push("/");
} catch (e) {
onError(e);
setIsLoading(false);
}
}
return (
<div className="Login">
<form onSubmit={ () => { handleSubmit() } }>
<FormGroup controlId="email" bsSize="large">
<ControlLabel>Email</ControlLabel>
<FormControl
autoFocus
type="email"
value={fields.email}
onChange={ () => { handleFieldChange() } }
/>
</FormGroup>
<FormGroup controlId="password" bsSize="large">
<ControlLabel>Password</ControlLabel>
<FormControl
type="password"
value={fields.password}
onChange={ () => { handleFieldChange() } }
/>
</FormGroup>
<LoaderButton
block
type="submit"
bsSize="large"
isLoading={ () => { isLoading() } }
disabled={() => { !validateForm() }}
>
Login
</LoaderButton>
</form>
</div>
);
}
hooksLib.js / useFormFields
import { useState } from 'react'
const useFormFields = (initalState) => {
const [fields, setValues] = useState(initalState)
return [
fields,
setValues({
...fields,
[event.target.id]: event.target.value
})
]
}
export { useFormFields }
Your custom hook should look like this if you want to accept the event value:
const useFormFields = (initalState) => {
const [fields, setValues] = useState(initalState)
return [
fields,
(event) => setValues({
...fields,
[event.target.id]: event.target.value
})
]
}
Since that parameter is actually a callback that should occur.
Also, your LoadingButton implementation needs to change to this:
<LoaderButton
block
type="submit"
bsSize="large"
isLoading={isLoading} // This is a boolean value, not a function
disabled={() => !validateForm()}
>...</LoaderButton>

React Redux-dynamically creating Redux Forms

I'm building a component which will allow users to invite their friends.
The spec for the component is that it will have several input forms for their friends' emails and companies, a button which will add more input forms, and a button which submits all the forms remotely. When forms are submitted, a spinner appears in each form until a response is received from the server, at which point, if the submit was successful, the form disappears, and if there was an error, the error is displayed.
I'm stuck on the following: in order to submit a form remotely with Redux Form, you need to pass its name to the submitting component. I want to create the forms programatically. The names will be auto-incremented integers, created by the form management component, and passed to the child forms as props. However, when I try to export the form, referencing the name as this.props.name, I get an error: 'Uncaught TypeError: Cannot read property 'props' of undefined'-"this" is undefined.
Questions:
Is my approach to solving this problem valid, or should I do something differently on a basic level?
How do I get around this? I assume it's a scoping error?
My components:
The management component (creates and deletes forms, submits them, etc.):
import React, { Component } from 'react';
import { connect } from 'react-redux'
import { submit } from 'redux-form'
import * as actions from '../../actions';
import InviteForm from './inviteForm';
class InvitationFormManager extends Component {
const buildForms = (length) =>{
for (let i = 0; i< length; i++)
{
this.setState({forms:[...this.state.forms, <InviteForm key={i} name={i}>]};
}
}
const addForm = () =>{
this.setState({forms:[...this.state.forms, <InviteForm key={(this.state.forms.length + 1)} name={(this.state.forms.length + 1)}>]});
}
const formSubmit = (form) =>
{
dispatch(submit(form.name))
.then(this.setState({
forms: this.state.forms.filter(f => f.name !== form.name)
}))
}
const submitForms = (){
for(let form of this.state.forms){formSubmit(form)}
}
constructor(props) {
super(props);
this.state = {forms:[]};
}
componentWillMount(){
buildForms(3)
}
render() {
return (<div>
<h5 className="display-6 text-center">Invite your team</h5>
{this.state.forms}
<br />
<button
type="button"
className="btn btn-primary"
onClick={submitForms}
>
Invite
</button>
<button
type="button"
className="btn btn-primary"
onClick={addForm}
>
+
</button>
</div>
);
}
}
export default connect(actions)(InvitationFormManager)
The form component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Field, reduxForm } from 'redux-form';
import * as actions from '../../actions';
import { Link } from 'react-router';
const renderField = ({
input,
label,
type,
meta: { touched, error, warning }
}) => (
<fieldset className="form-group">
<label htmlFor={input.name}>{label}</label>
<input className="form-control" {...input} type={type} />
{touched && error && <span className="text-danger">{error}</span>}
</fieldset>
);
class InviteForm extends Component {
constructor(props) {
super(props);
this.name = this.name.bind(this);
}
handleFormSubmit(props) {
this.props.sendInvitation(props);
}
render() {
if (this.props.submitting) {
return (
<div className="dashboard loading">
<Spinner name="chasing-dots" />
</div>
);
}
const { formName, handleSubmit } = this.props;
return (
<div className="form-container text-center">
<form
className="form-inline"
onSubmit={handleSubmit(this.handleFormSubmit.bind(this))}>
<div className="form-group">
<Field
name="email"
component={renderField}
type="email"
label="Email"
/>
<Field
name="company"
component={renderField}
type="text"
label="Company"
/>
</div>
</form>
<div>
{this.props.errorMessage &&
this.props.errorMessage.invited && (
<div className="error-container">
Oops! {this.props.errorMessage.invited}
</div>
)}
</div>
</div>
);
}
}
function validate(values) {
let errors = {};
if (values.password !== values.password_confirmation) {
errors.password = "Password and password confirmation don't match!";
}
return errors;
}
function mapStateToProps(state) {
return {
errorMessage: state.invite.error,
submitting: state.invite.submitting
};
}
InviteForm = reduxForm({
form: this.props.name,
validate
})(InviteForm);
export default connect(mapStateToProps, actions)(InviteForm);
The answer is, RTFM. Redux Form has this functionality as FieldArrays.
The example for Redux Form 7.0.4 is here: https://redux-form.com/7.0.4/examples/fieldarrays/
In case they move it later, here it is:
FieldArraysForm.js
import React from 'react'
import { Field, FieldArray, reduxForm } from 'redux-form'
import validate from './validate'
const renderField = ({ input, label, type, meta: { touched, error } }) =>
<div>
<label>
{label}
</label>
<div>
<input {...input} type={type} placeholder={label} />
{touched &&
error &&
<span>
{error}
</span>}
</div>
</div>
const renderHobbies = ({ fields, meta: { error } }) =>
<ul>
<li>
<button type="button" onClick={() => fields.push()}>
Add Hobby
</button>
</li>
{fields.map((hobby, index) =>
<li key={index}>
<button
type="button"
title="Remove Hobby"
onClick={() => fields.remove(index)}
/>
<Field
name={hobby}
type="text"
component={renderField}
label={`Hobby #${index + 1}`}
/>
</li>
)}
{error &&
<li className="error">
{error}
</li>}
</ul>
const renderMembers = ({ fields, meta: { error, submitFailed } }) =>
<ul>
<li>
<button type="button" onClick={() => fields.push({})}>
Add Member
</button>
{submitFailed &&
error &&
<span>
{error}
</span>}
</li>
{fields.map((member, index) =>
<li key={index}>
<button
type="button"
title="Remove Member"
onClick={() => fields.remove(index)}
/>
<h4>
Member #{index + 1}
</h4>
<Field
name={`${member}.firstName`}
type="text"
component={renderField}
label="First Name"
/>
<Field
name={`${member}.lastName`}
type="text"
component={renderField}
label="Last Name"
/>
<FieldArray name={`${member}.hobbies`} component={renderHobbies} />
</li>
)}
</ul>
const FieldArraysForm = props => {
const { handleSubmit, pristine, reset, submitting } = props
return (
<form onSubmit={handleSubmit}>
<Field
name="clubName"
type="text"
component={renderField}
label="Club Name"
/>
<FieldArray name="members" component={renderMembers} />
<div>
<button type="submit" disabled={submitting}>
Submit
</button>
<button type="button" disabled={pristine || submitting} onClick={reset}>
Clear Values
</button>
</div>
</form>
)
}
export default reduxForm({
form: 'fieldArrays', // a unique identifier for this form
validate
})(FieldArraysForm)
validate.js
const validate = values => {
const errors = {}
if (!values.clubName) {
errors.clubName = 'Required'
}
if (!values.members || !values.members.length) {
errors.members = { _error: 'At least one member must be entered' }
} else {
const membersArrayErrors = []
values.members.forEach((member, memberIndex) => {
const memberErrors = {}
if (!member || !member.firstName) {
memberErrors.firstName = 'Required'
membersArrayErrors[memberIndex] = memberErrors
}
if (!member || !member.lastName) {
memberErrors.lastName = 'Required'
membersArrayErrors[memberIndex] = memberErrors
}
if (member && member.hobbies && member.hobbies.length) {
const hobbyArrayErrors = []
member.hobbies.forEach((hobby, hobbyIndex) => {
if (!hobby || !hobby.length) {
hobbyArrayErrors[hobbyIndex] = 'Required'
}
})
if (hobbyArrayErrors.length) {
memberErrors.hobbies = hobbyArrayErrors
membersArrayErrors[memberIndex] = memberErrors
}
if (member.hobbies.length > 5) {
if (!memberErrors.hobbies) {
memberErrors.hobbies = []
}
memberErrors.hobbies._error = 'No more than five hobbies allowed'
membersArrayErrors[memberIndex] = memberErrors
}
}
})
if (membersArrayErrors.length) {
errors.members = membersArrayErrors
}
}
return errors
}
export default validate

Categories

Resources