React alternative support drilling (reverse, child to parent) way to process forms
P粉419164700
P粉419164700 2023-09-01 19:45:14
0
2
566
<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>
P粉419164700
P粉419164700

reply all(2)
P粉627136450

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:

const [name, setName] = useState("");

return <input value={name} onChange={(e) => setName(e.currentTarget.value)} />

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:

const name = useRef(null);
const onSubmit = () => {
    const nameContent = name.current.value;
}
return <input ref={name} defaultValue="" />

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 problems react-hook-form and react-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:

  1. Keep a source of truth at the highest common ancestor
  2. Avoid mirroring and copying states
  3. Use useMemo and useRef to re-render as little as possible
P粉596191963

This 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

  • If the distance between interactive components is more than 2 edges, use redux
  • Use redux for sibling components, i.e. subcomponents that share a parent component and need to communicate with each other (select a tree item in the side panel and display the details of the selected item in the main component)

Since implementation

  • I find useInput to be an over-refactoring, your input component should be enough to manage input related operations, better abstract validation etc.
  • You can trigger all validations on form submission, in which case you don't need controlled input (append validation on the form's onSubmit event)
  • However, if your form contains too many fields (say >5) and you want to validate the field before submitting, you can use the onBlur event of the input field, or use onInput with a debounce action (such as debounce from lodash operation) or implement it like this

function debounce(func, timeout = 300){
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => { func.apply(this, args); }, timeout);
  };
}
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template