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.
So I placed the Form component and the FormGroup component in the sample login page like this:
pages/Login.js
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 (); } export default LoginPage;Go CupLog in to your Go Cup account
As you can see in the code above, I use the FormGroup component and pass the onValidityChange
and onReset
properties to get 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
isValid
and reset
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 isLoginValid
and isPasswordValid
states defiend in the login page to store the updated isValid
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.
State is managed inside the input component, I have these states:
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 (isInputValid = isValid || !isInputTouched
) to decide to display the verification message.
These states and values are managed in a custom hook I created, useInput
, like this:
hooks/use-state.js
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;
I am currently using this custom hook in Input.js like this:
components/UI/Input.js
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 ( ); } export default Input;
In the input component, I directly use the isInputValid
state to add the invalid CSS class to the input. But I also pass the isInputValid
, message
, isValid
status and reset
functions to the parent component to used in it. To pass these states and functions, I use the onIsInputValidOrMessageChange
, onValidityChange
, onReset
functions defined in props (props drilldown but direction Instead, from child to parent).
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):
components/UI/FormGroup.js
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 = ( ); if(props.sideLabelElement) { labelCmp = ({labelCmp} {props.sideLabelElement}); } function isInputValidOrMessageChangeHandler(changedIsInputValid, changedMessage) { setIsInputValid(changedIsInputValid); setMessage(changedMessage); } return ({labelCmp} {!isInputValid &&); } export default FormGroup;{message}
}
As you can see from the above code, I defined the message
and isInputValid
states to store the updated message
and isInputValid
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.
The question is: 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?
Note: Since I am new to React, I am open to all your suggestions on all my coding, from simple mistakes to general mistakes. Thanks!
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 recommendReact Hook Formas 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 likeflux architecturethen 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 unpredictableside effectswith this abstraction. Additionally, youmirror 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