React alternative support drilling (reverse, child to parent) way to process forms
P粉419164700
2023-09-01 19:45:14
<p>I am new to React and learning it through some hands-on projects. I'm currently working on form processing and validation. I'm using React Router's Form component in a SPA, and inside the form I have a FormGroup element, which renders label inputs and error messages. I also use my own input component within the FormGroup component to separate the logic and state management of the inputs used in the form. </p>
<p>So I placed the Form component and the FormGroup component in the sample login page like this: </p>
<p><em>pages/Login.js</em></p>
<pre class="brush:js;toolbar:false;">import { useState } from 'react';
import { Link, Form, useNavigate, useSubmit } from 'react-router-dom';
import FormGroup from '../components/UI/FormGroup';
import Button from '../components/UI/Button';
import Card from '../components/UI/Card';
import './Login.scss';
function LoginPage() {
const navigate = useNavigate();
const submit = useSubmit();
const [isLoginValid, setIsLoginValid] = useState(false);
const [isPasswordValid, setIsPasswordValid] = useState(false);
var resetLoginInput = null;
var resetPasswordInput = null;
let isFormValid = false;
if(isLoginValid && isPasswordValid) {
isFormValid = true;
}
function formSubmitHandler(event) {
event.preventDefault();
if(!isFormValid) {
return;
}
resetLoginInput();
resetPasswordInput();
submit(event.currentTarget);
}
function loginValidityChangeHandler(isValid) {
setIsLoginValid(isValid);
}
function passwordValidityChangeHandler(isValid) {
setIsPasswordValid(isValid);
}
function resetLoginInputHandler(reset) {
resetLoginInput = reset;
}
function resetPasswordInputHandler(reset) {
resetPasswordInput = reset;
}
function switchToSignupHandler() {
navigate('/signup');
}
return (
<div className="login">
<div className="login__logo">
Go Cup
</div>
<p className="login__description">
Log in to your Go Cup account
</p>
<Card border>
<Form onSubmit={formSubmitHandler}>
<FormGroup
id="login"
label="User name or e-mail address"
inputProps={{
type: "text",
name: "login",
validity: (value) => {
value = value.trim();
if(!value) {
return [false, 'Username or e-mail address is required.']
} else if(value.length < 3 || value.length > 30) {
return [false, 'Username or e-mail address must have at least 3 and at maximum 30 characters'];
} else {
return [true, null];
}
},
onValidityChange: loginValidityChangeHandler,
onReset: resetLoginInputHandler
}}
/>
<FormGroup
id="password"
label="Password"
sideLabelElement={
<Link to="/password-reset">
Forgot password?
</Link>
}
inputProps={{
type: "password",
name: "password",
validity: (value) => {
value = value.trim();
if(!value) {
return [false, 'Password is required.']
} else if(value.length < 4 || value.length > 1024) {
return [false, 'Password must be at least 4 or at maximum 1024 characters long.'];
} else {
return [true, null];
}
},
onValidityChange: passwordValidityChangeHandler,
onReset: resetPasswordInputHandler
}}/>
<div className="text-center">
<Button className="w-100" type="submit">
Log in
</Button>
<span className="login__or">
or
</span>
<Button className="w-100" onClick={switchToSignupHandler}>
Sign up
</Button>
</div>
</Form>
</Card>
</div>
);
}
export default LoginPage;
</pre>
<p>As you can see in the code above, I use the FormGroup component and pass the <code>onValidityChange</code> and <code>onReset</code> properties to get <code>isValid</ The updated value of the code> value. Changes and reset functions to reset input after form submission, etc. Use my custom hook useInput to create the <code>isValid</code> and <code>reset</code> functions in the input component. I am passing the isValid value when the value changes and passing the reset function from the input component using props defined in the FormGroup component. I'm also using <code>isLoginValid</code> and <code>isPasswordValid</code> states defiend in the login page to store the updated <code>isValid</code> state value passed from the child input component. So I have defined states in the input component and passed them to the parent component using props and stored their values in other states created in that parent component. The prop drilling that was going on made me feel a little uncomfortable. </p>
<p>State is managed inside the input component, I have these states: </p>
<ul>
<li><strong>Value: </strong>Enter the value of the element. </li>
<li><strong>isInputTouched</strong>: Determines whether the user has touched/focused the input to determine whether to display a validation error message, if any. </li>
</ul>
<p>I combine and apply some functions (such as the validation function passed to the input component) to these two states to create other variable values to collect information about the input and its validity, such as whether the value is valid (isValid ), whether there is message verification (message), if the input is valid (<code>isInputValid = isValid || !isInputTouched</code>) to decide to display the verification message.</p>
<p>These states and values are managed in a custom hook I created, <code>useInput</code>, like this: </p>
<p><em>hooks/use-state.js</em></p>
<pre class="brush:js;toolbar:false;">import { useState, useCallback } from 'react';
function useInput(validityFn) {
const [value, setValue] = useState('');
const [isInputTouched, setIsInputTouched] = useState(false);
const [isValid, message] = typeof validityFn === 'function' ? validityFn(value) : [true, null];
const isInputValid = isValid || !isInputTouched;
const inputChangeHandler = useCallback(event => {
setValue(event.target.value);
if(!isInputTouched) {
setIsInputTouched(true);
}
}, [isInputTouched]);
const inputBlurHandler = useCallback(() => {
setIsInputTouched(true);
}, []);
const reset = useCallback(() => {
setValue('');
setIsInputTouched(false);
}, []);
return {
value,
isValid,
isInputValid,
message,
inputChangeHandler,
inputBlurHandler,
reset
};
}
export default useInput;
</pre>
<p>I am currently using this custom hook in Input.js like this: </p>
<p><em>components/UI/Input.js</em></p>
<pre class="brush:js;toolbar:false;">import { useEffect } from 'react';
import useInput from '../../hooks/use-input';
import './Input.scss';
function Input(props) {
const {
value,
isValid,
isInputValid,
message,
inputChangeHandler,
inputBlurHandler,
reset
} = useInput(props.validity);
const {
onIsInputValidOrMessageChange,
onValidityChange,
onReset
} = props;
let className = 'form-control';
if(!isInputValid) {
className = `${className} form-control--invalid`;
}
if(props.className) {
className = `${className} ${props.className}`;
}
useEffect(() => {
if(onIsInputValidOrMessageChange && typeof onIsInputValidOrMessageChange === 'function') {
onIsInputValidOrMessageChange(isInputValid, message);
}
}, [onIsInputValidOrMessageChange, isInputValid, message]);
useEffect(() => {
if(onValidityChange && typeof onValidityChange === 'function') {
onValidityChange(isValid);
}
}, [onValidityChange, isValid]);
useEffect(() => {
if(onReset && typeof onReset === 'function') {
onReset(reset);
}
}, [onReset, reset]);
return (
<input
{...props}
className={className}
value={value}onChange={inputChangeHandler}
onBlur={inputBlurHandler}
/>
);
}
export default Input;
</pre>
<p>In the input component, I directly use the <code>isInputValid</code> state to add the invalid CSS class to the input. But I also pass the <code>isInputValid</code>, <code>message</code>, <code>isValid</code> status and <code>reset</code> functions to the parent component to used in it. To pass these states and functions, I use the <code>onIsInputValidOrMessageChange</code>, <code>onValidityChange</code>, <code>onReset</code> functions defined in props (props drilldown but direction Instead, from child to parent). </p>
<p>This is the definition of the FormGroup component and how I use the input state inside the FormGroup to display the validation message (if any): </p>
<p><em>components/UI/FormGroup.js</em></p>
<pre class="brush:js;toolbar:false;">import { useState } from 'react';
import Input from './Input';
import './FormGroup.scss';
function FormGroup(props) {
const [message, setMessage] = useState(null);
const [isInputValid, setIsInputValid] = useState(false);
let className = 'form-group';
if(props.className) {
className = `form-group ${props.className}`;
}
let labelCmp = (
<label htmlFor={props.id}>
{props.label}
</label>
);
if(props.sideLabelElement) {
labelCmp = (
<div className="form-label-group">
{labelCmp}
{props.sideLabelElement}
</div>
);
}
function isInputValidOrMessageChangeHandler(changedIsInputValid, changedMessage) {
setIsInputValid(changedIsInputValid);
setMessage(changedMessage);
}
return (
<div className={className}>
{labelCmp}
<Input
id={props.id}
onIsInputValidOrMessageChange={isInputValidOrMessageChangeHandler}
{...props.inputProps}
/>
{!isInputValid && <p>{message}</p>}
</div>
);
}
export default FormGroup;
</pre>
<p>As you can see from the above code, I defined the <code>message</code> and <code>isInputValid</code> states to store the updated <code>message</code> and <code>isInputValid</code> code> The state passed from the input component. I have defined 2 states in the input component to hold these values, but I need to define another 2 states in this component to store the updated and passed values in the input component. This is a bit weird and doesn't seem like the best way to me. </p>
<p><strong>The question is: </strong>I think I can use React Context (useContext) or React Redux to solve the prop drilling problem here. But I'm not sure if my current state management is bad and can be improved using React Context or React Redux. Because from what I understand, React Context can be terrible in situations where the state changes frequently, but if the Context is used application-wide, then this works. Here I can create a context to store and update the entire form, allowing for form-wide expansion. React Redux, on the other hand, might not be the best fit for the silo, and might be a bit overkill. What do you think? What might be a better alternative for this particular situation? </p>
<p><strong>Note: </strong>Since I am new to React, I am open to all your suggestions on all my coding, from simple mistakes to general mistakes. Thanks! </p>
There are two main schools of thought about React state management: controlled and uncontrolled. Controlled forms may be controlled using a React context where values can be accessed from anywhere to provide reactivity. However, controlled input can cause performance issues, especially when updating the entire form on each input. This is where uncontrolled forms come in. With this paradigm, all state management must leverage the browser's native capabilities for displaying state. The main problem with this approach is that you lose the React aspect of the form, you need to manually collect the form data on submission, and maintaining multiple references for this can be tedious.
Controlled input looks like this:
EDIT: As @Arkellys pointed out, you don't necessarily need references to collect form data, Here's an example using
FormData
And out of control:
It's obvious from these two examples that maintaining multi-component forms using either method is tedious, so libraries are often used to help you manage your forms. I personally recommend React Hook Form as a battle-tested, well-maintained, and easy-to-use forms library. It takes an uncontrolled form for optimal performance while still allowing you to watch a single input for reactive rendering.
Regarding whether to use Redux, React context, or any other state management system, there is generally no difference in terms of performance, assuming you implement it correctly. If you like flux architecture then by all means use Redux, but in most cases the React context is both performant and sufficient.
Your
useInput
custom hook looks like a valiant but misguided attempt to solve problemsreact-hook-form
andreact-final-form
Code > Already solved. You are creating unnecessary complexity and unpredictable side effects with this abstraction. Additionally, you mirror props a> This is often an anti-pattern in React.If you really want to implement your own form logic (which I recommend not to do unless it's for educational purposes), you can follow these guidelines:
useMemo
anduseRef
to re-render as little as possibleThis is a straightforward aspect that I use to decide between a publish-subscribe library like Redux and propagating state through the component tree.
If two components have a parent-child relationship and are at most two edges away from each other, propagate the child state to the parent
Parent -> child1-level1 -> child1-level2 ------ OK
Parent -> child1-level1 ------ OK
Parent -> child1-level1 -> child1-level2 -> child1-level3 --> Too many trips to change status from child1-level3 to parent
Since implementation