Implement a useInputControl hook that manages a controlled input value and tracks additional form input states like:
| Property | Tracks | When it becomes true | When it becomes false |
|---|---|---|---|
| Touched | If input has been focused then blurred | When the user blurs the input (focus -> blur) | Never resets automatically |
| Dirty | If the value has changed at least once | When the user types something | Never resets automatically |
| Different | If value is different from the original | When the value is different from the initial | When the value is same as the initial |
The handleX functions returned by the hook are meant to be called on the relevant event handlers of <input> in order for the hook to work as intended.
export default function Component() {const nameInput = useInputControl('Oliver');return (<form><div><label htmlFor="name">Name</label><inputid="name"value={nameInput.value}onChange={nameInput.handleChange}onBlur={nameInput.handleBlur}/></div><p>Touched: {nameInput.touched.toString()}</p><p>Dirty: {nameInput.dirty.toString()}</p><p>Different: {nameInput.different.toString()}</p><button type="submit" disabled={!nameInput.different}>Submit</button><button type="button" onClick={nameInput.reset}>Reset</button></form>);}
initialValue: string: The initial value of the inputThe hook returns an object with the following properties:
value: string: The current value of the inputdirty: boolean: Whether the input value has been modified at least oncetouched: boolean: Whether the input was focused and blurreddifferent: boolean: Whether the value is different from the initial valuehandleChange: (event: React.ChangeEvent<HTMLInputElement>) => void: A function that updates the value of the inputhandleBlur: () => void: A function that should be called when the input is blurredreset: () => void: A function that resets the input value and all derived flags back to their initial stateuseInputControl(initialValue) returns a small form-state bundle: the current value, dirty and touched flags, the derived different flag, and the handlers needed to drive them. The state model deliberately mixes stored state with one derived value.
The main correctness concern is freezing the baseline value from mount. useState owns the mutable UI state, while useRef stores the original initialValue so different and reset() are based on a stable snapshot instead of later re-renders.
useInputControl mixes stored state and derived state.
value, dirty, and touched are stored in React state.different is derived by comparing the current value with the original value captured on first render.handleChange and handleBlur map directly to common <input> event handlers.reset restores the original snapshot and clears the extra flags.To recap what fields are returned:
value: The current input valuetouched: Whether the input has been focused then blurreddirty: Whether the value has been changed beforedifferent: Whether the value is different from the originalLet's go through each field and how to implement them:
valuevalue is a string tracked using React state. A handleChange handler is used to update the value from the input's change event.
touchedtouched is a boolean value tracked using React state. It will be set to true when the input is blurred. As the hook does not know when the <input> element is blurred, we return a handleBlur function that sets touched to true and let the consumer call it from an onBlur handler.
dirtydirty is a boolean value tracked using React state. Since it becomes true after the user changes the value at least once, handleChange is the right place to update it.
differentThis field does not require a state as it is derived state that can be computed by comparing the initial value and the current value. However, the comparison should not be done against the initialValue argument as the value might be different during re-renders!
Instead, we track the first render's initialValue using useRef. Why not state? Because initialValueRef does not ever change after the hook is mounted.
const different = initialValueRef.current !== value;
reset functionThis function resets all the states within the hook. Simply call the various state setters with their initial values. The initial value to set to can be obtained from initialValueRef.
import { useCallback, useRef, useState } from 'react';const defaultDirty = false;const defaultTouched = false;/*** @param {string} initialValue* @returns {{* value: string,* dirty: boolean,* touched: boolean,* different: boolean,* handleChange: (event: import('react').ChangeEvent<HTMLInputElement>) => void,* handleBlur: () => void,* reset: () => void,* }}*/export default function useInputControl(initialValue) {// Keep the first value stable so reset() and different compare against the original input.const initialValueRef = useRef(initialValue);const [value, setValue] = useState(initialValue);const [dirty, setDirty] = useState(defaultDirty);const [touched, setTouched] = useState(defaultTouched);const handleChange = useCallback((event) => {setValue(event.currentTarget.value);setDirty(true);}, []);const handleBlur = useCallback(() => {setTouched(true);}, []);const reset = useCallback(() => {setValue(initialValueRef.current);setDirty(defaultDirty);setTouched(defaultTouched);}, []);// different is derived from the frozen initial value rather than the latest prop.const different = initialValueRef.current !== value;return {value,dirty,touched,different,handleChange,handleBlur,reset,};}
different is derived, so it should update automatically whenever value changes.reset() should restore the original mounted value, clear dirty, and clear touched.console.log() statements will appear here.