Creating a Custom React Hook

Understanding the Problem

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.

Devising a Plan

  1. Understand the current Form component and its text input validation logic
  2. Design the API for our custom useTextInput hook
  3. Move the relevant logic from the Form component to the custom hook
  4. Update the Form component to use our new custom hook
  5. Enhance the hook by moving the event handler logic inside it
  6. Test the implementation

Carrying Out the Plan

Step 1: Analyzing the Form Component

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:

Step 2: Creating the Custom Hook

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:

Step 3: Updating the Form Component

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:

Step 4: Enhancing the Custom Hook with Event Handler

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.

Step 5: Updating the Form Component Again

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.

Final Solution

Here are the final versions of both files:

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);
  
  const handleChange = (e) => {
    setValue(e.target.value);
  };
  
  return [value, handleChange, errors];
};

src/components/Form.jsx

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;

Looking Back

Understanding Custom Hooks

Custom hooks in React are a powerful pattern that allows you to extract component logic into reusable functions. Here's what makes them special:

Benefits of Our useTextInput Hook

Our custom hook provides several benefits:

Real-World Applications

Custom hooks like useTextInput are incredibly useful in real-world applications:

Extending Our Hook

Our hook could be extended in several ways:

Further Learning

To deepen your understanding of custom hooks, consider exploring: