How to validate form using react hooks ?
I used class components and it worked great, but now decided to use functional components with hooks and don't know the best way to validate form.
My code:
const PhoneConfirmation = ({ onSmsCodeSubmit }) => {
const [smsCode, setSmsCode] = useState('');
const onFormSubmit = (e) => {
e.preventDefault();
if (smsCode.length !== 4) return;
onSmsCodeSubmit(smsCode);
}
const onValueChange = (e) => {
const smsCode = e.target.value;
if (smsCode.length > 4) return;
setSmsCode(smsCode);
};
return (
<form onSubmit={onFormSubmit}>
<input type="text" value={smsCode} onChange={onValueChange} />
<button type="submit">Submit</button>
</form>
)
};
It works but I don't think it's good idea to use handler function inside functional component, because it will be defined every time when the component is called.
Your code is fine, but you can slightly improve it because you don't need a controlled component.
Moreover, you can memorize the component so it won't make unnecessary render on onSmsCodeSubmit change due to its parent render.
const FORM_DATA = {
SMS: 'SMS'
}
const PhoneConfirmation = ({ onSmsCodeSubmit, ...props }) => {
const onSubmit = e => {
e.preventDefault();
const data = new FormData(e.target);
const currSmsCode = data.get(FORM_DATA.SMS);
onSmsCodeSubmit(currSmsCode);
};
return (
<form onSubmit={onSubmit} {...props}>
<input type="text" name={FORM_DATA.SMS} />
<button type="submit">Submit</button>
</form>
);
};
// Shallow comparison by default
export default React.memo(PhoneConfirmation)
Related
I am trying to trigger the form submit function from custom component.
In general my goal is to trigger a boolean state value which is a part of form, and even though the custom component is imported inside the form, somehow it doesn't work.
Problem is about sendEmail function which comes from Custom Component
Here is the codesandbox link and code example below.
Custom Component
import React from "react";
const CustomComponent = (props) => {
return (
<div>
<button onClick={props.sendEmail} type="submit">
send email
</button>
</div>
);
};
export default CustomComponent;
App.js
import { useState } from "react";
import CustomComp from "./CustomComp";
export default function App() {
const [isDone, setIsDone] = useState(false);
const [inputText, setInputText] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
setIsDone(true);
console.log("inputText", inputText);
};
console.log(isDone);
const sendEmail = () => { // this doesn't work
handleSubmit();
console.log("isDone", isDone);
};
const onChangeHandler = (e) => {
setInputText(e.target.value);
};
return (
<form>
<h1>Hello CodeSandbox</h1>
<input type="text" onChange={onChangeHandler} value={inputText} />
<CustomComp sendEmail={sendEmail} />
<button onClick={handleSubmit} type="submit">
submit
</button>
</form>
);
}
Any help will be appreciated
The first problem you have is you're calling handleSubmit in the sendEmail without passing through 'e'
I'd change to this:
const sendEmail = (e) => {
handleSubmit(e);
console.log("isDone", isDone);
};
After doing this, you'll notice your isDone isn't showing what you'd expect. This is because when you're changing the state via handleSubmit, it won't change within this call. You shouldn't worry about logging it as you have. It'll only be an issue if you need to do something with the isDone value
There is no event passed when you trigger it from the custom component so event.preventDefault() will result in an error.
Add the event to sendMail, and pass it to handleSubmit. The onClick will automatically pass the event as first param:
const handleSubmit = (event) => {
event.preventDefault();
setIsDone(true);
console.log("inputText", inputText);
};
const sendEmail = (event) => {
handleSubmit(event);
console.log("isDone", isDone);
};
Updated CodeSandbox
I was trying to access Parent state when a function is called from Child component, for that created a function in Parent component and passed it to Child, issue is I am not able to access the state completely.
for example on button click I add a new input field and a delete button, suppose I added 10 input fields, and added all of them in state array, but when i click delete button of second input field, the count I get from state is 1, similar if I click 5th delete button i get count as 4 and it only show me 4 items in state, but it has 10 items
Here is an example link https://codesandbox.io/s/add-react-component-onclick-forked-t2i0ll?file=/src/index.js:0-869
import React, { useState } from "react";
import ReactDOM from "react-dom";
const Input = ({ deleteRow, position }) => {
const handleDelete = () => deleteRow(position);
return (
<>
<input placeholder="Your input here" />
<button onClick={handleDelete}>Delete</button>
</>
);
};
const Form = () => {
const [inputList, setInputList] = useState([]);
const onDeleteRow = (position) => {
console.log("inputCount", inputList);
};
const onAddBtnClick = (event) => {
setInputList(
inputList.concat(
<Input
key={inputList.length}
position={inputList.length}
deleteRow={onDeleteRow}
/>
)
);
};
return (
<div>
<button onClick={onAddBtnClick}>Add input</button>
{inputList}
</div>
);
};
ReactDOM.render(<Form />, document.getElementById("form"));
It's called a stale closure.
You can avoid that by not storing react elements inside the state.
Example:
const Form = () => {
const [inputList, setInputList] = useState([]);
const onDeleteRow = (position) => {
console.log("inputCount", inputList);
};
const onAddBtnClick = (event) => {
setInputList([...inputList, inputList.length])
);
};
return (
<div>
<button onClick={onAddBtnClick}>Add input</button>
{inputList.map(item => (
<Input
key={item}
position={item}
deleteRow={onDeleteRow}
/>
)}
</div>
);
};
I am trying to build a website to convert recipes to a different number of servings. I am using React. I have setup a component to take the number of original servings, new number of servings, and number of ingredients. It will not update in the App.js file when I change the value in the PreInputs component.
App.js:
const App = () => {
//define variables using states
const [numIngredients, setNumIngredients] = useState(5);
const [originalServings, setOriginalServings] = useState(8);
const [newServings, setNewServings] = useState(4);
//create handling functions for the variables
const handleNumIngredients = (num) => {
setNumIngredients(num);
}
const handleOriginalServings = (num) => {
setOriginalServings(num)
}
const handleNewServings = (num) => {
setNewServings(num);
}
return (
<div>
<Header title="test"/>
<PreInputs changeNumIngredients={handleNumIngredients} changeOriginalServings={handleOriginalServings} changeNewServings={handleNewServings}/>
<h1>{originalServings}</h1>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'))
PreInputs.js:
import React, { useCallback, useState } from 'react';
export const PreInputs = (props) => {
const numOriginalServings = useState(0)
const handleOriginalServingsChange = (e) => {
const originalServings = e.target.value;
props.changeOriginalServings(originalServings)
}
return (
<div className='pre-inputs'>
<form>
<label>Original Servings: </label>
<input value={numOriginalServings} onChange={handleOriginalServingsChange} type='number' />
<button></button>
</form>
<h1>Placeholder</h1>
</div>
);
}
It is anti-pattern in React to store passed props into local state, so you don't really need the numOriginalServings state in PreInputs. Pass originalServings to PreInputs to be used as the default value of the input and pass the updated input value back to the parent component.
PreInputs
const PreInputs = (props) => {
const handleOriginalServingsChange = (e) => {
const originalServings = e.target.value;
props.changeOriginalServings(originalServings);
};
return (
<div className="pre-inputs">
<form>
<label>Original Servings: </label>
<input
defaultValue={props.originalServings} // <-- initialize input
onChange={handleOriginalServingsChange}
type="number"
/>
</form>
<h1>Placeholder</h1>
</div>
);
};
Parent
function App() {
//define variables using states
const [numIngredients, setNumIngredients] = useState(5);
const [originalServings, setOriginalServings] = useState(8);
const [newServings, setNewServings] = useState(4);
//create handling functions for the variables
const handleNumIngredients = (num) => {
setNumIngredients(num);
};
const handleOriginalServings = (num) => {
setOriginalServings(num);
};
const handleNewServings = (num) => {
setNewServings(num);
};
return (
<div>
<Header title="test"/>
<h1>{originalServings}</h1>
<PreInputs
originalServings={originalServings} // <-- pass state
changeOriginalServings={handleOriginalServings} // <-- pass callback
/>
</div>
);
}
if you want to send data to the parent component
you have 3 ways for it
1- use redux
2- use context
3- make function in the parent component and get data in this function and sent function to the child component
According to good programming practice, you shouldn't transfer state this way.
For small Apps you should use context.
It's really simple and superfast, especially for data reads.
EDIT: See the comment of O.o for the explanation of the answer and the variant in case you are using classes.
I've come across to something and I can't find the solution.
I have 4 components in my web app:
Parent
child_1
child_2
child_3
I have a button on the Parent, and different forms (with inputs, checkboxes and radiobuttons) at the children.
Each child has his own button that executes several functions, some calculations, and updates the corresponding states. (No states are passed through parent and child).
I need to replace the three buttons of the children with the parent button.
Is there a way that I can execute the functions at the three children from the parent button and retrieve the results? (the results are one state:value per child.)
function Child1(props) {
const [value, setValue] = useState("");
useEffect(() => {
calculate();
}, [props.flag]);
calculate() {
//blah blah
}
onChange(e) {
setValue(e.target.value);
props.onChange(e.target.value); // update the state in the parent component
}
return (
<input value={value} onChange={(e) => onChange(e)} />
);
}
function Parent(props) {
const [flag, setFlag] = useState(false);
const [child1Value, setChild1Value] = useState("");
return (
<div>
<Child1 flag={flag} onChange={(value) => setChild1Value(value)}/>
<button onClick={() => setFlag(!flag)} />
</div>
);
}
I didn't test this but hope this helps you. And lemme know if there is an issue.
Try the following:
create refs using useRef for child form components.
for functional components, in order for the parent to access the child's methods, you need to use forwardRef
using the ref, call child component functions on click of parent submit button (using ref.current.methodName)
See the example code. I have tested it on my local, it is working ok.
Parent
import React, { Fragment, useState, useRef } from "react";
import ChildForm1 from "./ChildForm1";
const Parent = props => {
const [form1Data, setFormData] = useState({});//use your own data structure..
const child1Ref = useRef();
// const child2Ref = useRef(); // for 2nd Child Form...
const submitHandler = e => {
e.preventDefault();
// execute childForm1's function
child1Ref.current.someCalculations();
// execute childForm2's function
// finally do whatever you want with formData
console.log("form submitted");
};
const notifyCalcResult = (calcResult) => {
// update state based on calcResult
console.log('calcResult', calcResult);
};
const handleChildFormChange = data => {
setFormData(prev => ({ ...prev, ...data }));
};
return (
<Fragment>
<h1 className="large text-primary">Parent Child demo</h1>
<div>
<ChildForm1
notifyCalcResult={notifyCalcResult}
ref={child1Ref}
handleChange={handleChildFormChange} />
{/*{do the same for ChildForm2 and so on...}*/}
<button onClick={submitHandler}>Final Submit</button>
</div>
</Fragment>
);
};
export default Parent;
ChildFormComponent
import React, { useState, useEffect, forwardRef, useImperativeHandle } from "react";
const ChildForm1 = ({ handleChange, notifyCalcResult }, ref) => {
const [name, setName] = useState("");
const [calcResult, setCalcResult] = useState([]);
const someCalculations = () => {
let result = ["lot_of_data"];
// major calculations goes here..
// result = doMajorCalc();
setCalcResult(result);
};
useImperativeHandle(ref, () => ({ someCalculations }));
useEffect(() => {
// notifiy parent
notifyCalcResult(calcResult);
}, [calcResult]);
return (
<form className="form">
<div className="form-group">
<input
value={name}// //TODO: handle this...
onChange={() => handleChange(name)}//TODO: notify the value back to parent
type="text"
placeholder="Enter Name"
/>
</div>
</form>
);
};
export default forwardRef(ChildForm1);
Also as a best practice, consider to maintain state and functions in the parent component as much as possible and pass the required values/methods to the child as props.
I have a react component that looks like the one given below.
The form inputs are handled using the onInputChange function and form submit is handled by onFormSubmit
function RegisterForm() {
// formData stores all the register form inputs.
const [formData, setFormData] = useState(registerDefault);
const [errors, posting, postData] = useDataPoster();
function onInputChange(event: ChangeEvent<HTMLInputElement>) {
let update = { [event.target.name]: event.target.value };
setFormData(oldForm => Object.assign(oldForm, update));
}
function onFormSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const onSuccess: AxiosResponseHandler = response => {
setFormData(Object.assign(formData, response.data));
};
postData("/api/register", formData, onSuccess);
}
return (
<form onSubmit={onFormSubmit}>
<FormTextInput
name="full_name"
label="Name"
errors={errors.full_name}
onChange={onInputChange}
/>
<FormTextInput
name="email"
label="Email address"
type="email"
errors={errors.email}
onChange={onInputChange}
/>
<button type="submit" className="theme-btn submit" disabled={posting}>
{posting && <span className="fas fa-spin fa-circle-notch"></span>}
Create
</button>
</form>
);
}
My app has more than 50 similar forms and I wonder if I have to copy paste these two functions on all the other forms. onInputChange won't be changing a bit and the url is the only variable in onFormSubmit.
I am thinking of a class based approach with setFormData and postData as properties and the functions in question as class methods. But in that case, I have to bind the handlers with the class instance, so that handlers have a valid this instance.
Is there any other way to do this? How would you avoid the repetition of these two code blocks in all the form components?
Thanks
you could create a custom hook, something like this:
const [formState, setFormState] = useFormStateHandler({name: ''})
<input value={formState.name} onChange={event => setFormState(event, 'name')} />
where the definition looks like this:
export default function useFormStateHandler(initialState) {
const [state, setState] = useState(initialState)
const updater = (event, name) => {
setState({...state, [name]: event.target.value})
}
return [state, updater]
}
Create an HOC to inject input handlers to the form components with added params for url.
function RegisterForm(props) {
// specific function
const specific = () => {
const formData = props.formData; // use passed state values
// use form data
}
}
function withInputHandlers(Component, params) {
return function(props) {
// states
function onInputChange(...) {...}
function onFormSubmit(...) {
// use params.url when submitting
postData(params.url, formData, onSuccess);
}
// inject input handlers to component and state values
return (
<Component {...props} formData={formData} onChange={onInputChange} onSubmit={onFormSubmit} />
);
}
}
// Usage
const EnhancedRegisterForm = withInputHandlers(
RegisterForm,
{ url: 'register_url' } // params
);
const EnhancedSurveyForm = withInputHandlers(
Survey,
{ url: 'survey_url' } // params
)
This change may help you
function RegisterForm() {
// formData stores all the register form inputs.
const [formData, setFormData] = useState(registerDefault);
const [errors, posting, postData] = useDataPoster();
const onInputChange = name => event => {
let update = { [name]: event.target.value };
setFormData(oldForm => Object.assign(oldForm, update));
}
const onFormSubmit = url => event =>{
event.preventDefault();
const onSuccess: AxiosResponseHandler = response => {
setFormData(Object.assign(formData, response.data));
};
postData(url, formData, onSuccess);
}
return (
<form onSubmit={onFormSubmit("/api/register")}>
<FormTextInput
name="full_name"
label="Name"
errors={errors.full_name}
onChange={onInputChange("full_name")}
/>
<FormTextInput
name="email"
label="Email address"
type="email"
errors={errors.email}
onChange={onInputChange("email")}
/>
<button type="submit" className="theme-btn submit" disabled={posting}>
{posting && <span className="fas fa-spin fa-circle-notch"></span>}
Create
</button>
</form>
);
}