In React development, we often find ourselves reusing the same stateful logic across multiple components. This is where custom hooks come into play. In this tutorial, we need to:
A custom hook is simply a JavaScript function that starts with "use" and can call other hooks. This naming convention is important because it tells React that this function follows the rules of hooks.
Let's look at the current Form component to understand what logic we need to extract:
import { useState } from 'react';
import { textInputValidators } from '../utils/validations';
const Form = () => {
const [name, setName] = useState('');
const validatorResults = textInputValidators.map((validator) => validator(name));
const failedValidators = validatorResults.filter((validationObj) => !validationObj.pass);
const nameErrors = failedValidators.map((validationObj) => validationObj.msg);
return (
<>
<h1>Form Component</h1>
<form>
<ul>
{nameErrors.map(err => <li key={err}>{err}</li>)}
</ul>
<label htmlFor="name">
Name{" "}
<input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
</form>
</>
);
};
The Form component has:
Now let's implement our useTextInput hook in src/hooks/textInput.js:
import { useState } from 'react';
export const useTextInput = ({ validations = [], defaultValue = '' }) => {
const [value, setValue] = useState(defaultValue);
const validatorResults = validations.map((validator) => validator(value));
const failedValidators = validatorResults.filter((validationObj) => !validationObj.pass);
const errors = failedValidators.map((validationObj) => validationObj.msg);
return [value, setValue, errors];
};
The custom hook:
Now let's update the Form component to use our custom hook:
import { useTextInput } from '../hooks/textInput';
import { textInputValidators } from '../utils/validations';
const Form = () => {
const [name, setName, nameErrors] = useTextInput({
validations: textInputValidators
});
return (
<>
<h1>Form Component</h1>
<form>
<ul>
{nameErrors.map(err => <li key={err}>{err}</li>)}
</ul>
<label htmlFor="name">
Name{" "}
<input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
</form>
</>
);
};
We've simplified the Form component by:
To further encapsulate the logic, let's move the event handler into our custom hook:
import { useState } from 'react';
export const useTextInput = ({ validations = [], defaultValue = '' }) => {
const [value, setValue] = useState(defaultValue);
const validatorResults = validations.map((validator) => validator(value));
const failedValidators = validatorResults.filter((validationObj) => !validationObj.pass);
const errors = failedValidators.map((validationObj) => validationObj.msg);
const handleChange = (e) => {
setValue(e.target.value);
};
return [value, handleChange, errors];
};
Now our custom hook returns a ready-to-use event handler instead of the raw setValue function.
Let's update the Form component one more time to use our enhanced custom hook:
import { useTextInput } from '../hooks/textInput';
import { textInputValidators } from '../utils/validations';
const Form = () => {
const [name, handleNameChange, nameErrors] = useTextInput({
validations: textInputValidators
});
return (
<>
<h1>Form Component</h1>
<form>
<ul>
{nameErrors.map(err => <li key={err}>{err}</li>)}
</ul>
<label htmlFor="name">
Name{" "}
<input
id="name"
value={name}
onChange={handleNameChange}
/>
</label>
</form>
</>
);
};
Notice how we've simplified the onChange handler in the input element by directly using the handleNameChange function from our custom hook.
Here are the final versions of both files:
import { useState } from 'react';
export const useTextInput = ({ validations = [], defaultValue = '' }) => {
const [value, setValue] = useState(defaultValue);
const validatorResults = validations.map((validator) => validator(value));
const failedValidators = validatorResults.filter((validationObj) => !validationObj.pass);
const errors = failedValidators.map((validationObj) => validationObj.msg);
const handleChange = (e) => {
setValue(e.target.value);
};
return [value, handleChange, errors];
};
import { useTextInput } from '../hooks/textInput';
import { textInputValidators } from '../utils/validations';
const Form = () => {
const [name, handleNameChange, nameErrors] = useTextInput({
validations: textInputValidators
});
return (
<>
<h1>Form Component</h1>
<form>
<ul>
{nameErrors.map(err => <li key={err}>{err}</li>)}
</ul>
<label htmlFor="name">
Name{" "}
<input
id="name"
value={name}
onChange={handleNameChange}
/>
</label>
</form>
</>
);
};
export default Form;
Custom hooks in React are a powerful pattern that allows you to extract component logic into reusable functions. Here's what makes them special:
Our custom hook provides several benefits:
Custom hooks like useTextInput are incredibly useful in real-world applications:
Our hook could be extended in several ways:
To deepen your understanding of custom hooks, consider exploring: