You are currently viewing Mastering React Custom Hooks: Comprehensive Tutorial with Practical Examples
Reactjs

Mastering React Custom Hooks: Comprehensive Tutorial with Practical Examples

Unleashing the Power of React Custom Hooks

Since React version 16.8, the introduction of React Hooks has revolutionized the way we manage state and features within our components. Alongside familiar built-in Hooks like useState, useEffect, and useCallback, we now have the ability to craft our own Hooks, enabling us to encapsulate complex component logic into reusable functions.

Key Characteristics of Custom Hooks:

  1. Functionality as a Function: A Custom Hook takes specific inputs and produces desired outputs, enhancing reusability and modularity.
  2. Name Convention: Custom Hooks are denoted by names starting with ‘use’, such as useQuery or useMedia.
  3. Return of Non-JSX Data: Diverging from functional components, Custom Hooks yield conventional non-JSX data.
  4. Hook Harmony: Custom Hooks seamlessly integrate with other Hooks like useState, useRef, and even with fellow custom hooks.

Explore Beyond Limits:
These versatile tools extend beyond the basics. Libraries like React Hook Form introduce hooks like useForm, while frameworks like MUI offer useMediaQuery.

Embrace the future of React development by mastering the art of crafting and employing Custom Hooks to streamline your code and elevate your components.

Why and When to use React Custom Hooks

Custom hooks give us following benefits:

  • Completely separate logic from user interface.
  • Reusable in many different components with the same processing logic. Therefore, the logic only needs to be fixed in one place if it changes.
  • Share logic between components.
  • Hide code with complex logic in a component, make the component easier to read.

So, when to use React custom hook?
– When a piece of code (logic) is reused in many places (it’s easy to see when you copy a whole piece of code without editing anything, except for the parameter passed. Split like how you separate a function).

– When the logic is too long and complicated, you want to write it in another file, so that your component is shorter and easier to read because you don’t need to care about the logic of that hook anymore.

React Custom Hook example

Let’s say that we build a React application with the following 2 components:
– TutorialsList: get a list of Tutorials from an API call (GET /tutorials) and display the list.
– Tutorial: get a Tutorial’s details from an API call (GET /tutorials/:id) and display it, but the interface will be different.

import React from "react";
import { Routes, Route } from "react-router-dom";

import Tutorial from "./components/Tutorial";
import TutorialsList from "./components/TutorialsList";

function App() {
  return (
    <div>
      ...

      <div>
        <Routes>
          <Route path="/tutorials" element={<TutorialsList />} />
          <Route path="/tutorials/:id" element={<Tutorial />} />
        </Routes>
      </div>
    </div>
  );
}

export default App;

Exploring Alternatives to React Custom Hooks
Examining Traditional Approach: API Calls in TutorialsList and Tutorial Components

In this section, we’ll delve into a scenario where React Custom Hooks are not utilized for handling API calls. Specifically, we’ll explore the implementation within the TutorialsList and Tutorial components.

Initial Configuration: Establishing axios Base URL and Headers

import axios from "axios";

export default axios.create({
  baseURL: "http://localhost:8080/api",
  headers: {
    "Content-type": "application/json"
  }
});

Then we use axios.get() to fetch data from API with response result or error.

components/TutorialsList.js

import axios from "../http-common.js";

const TutorialsList = () => {
  const [tutorials, setTutorials] = useState([]);
  const [currentTutorial, setCurrentTutorial] = useState(null);
  const [searchTitle, setSearchTitle] = useState("");

  useEffect(() => {
    retrieveTutorials();
  }, []);

  const retrieveTutorials = () => {
    axios.get("/tutorials")
      .then(response => {
        setTutorials(response.data);
        console.log(response.data);
      })
      .catch(e => {
        console.log(e);
      });
  };

  const findByTitle = () => {
    axios.get(`/tutorials?title=${searchTitle}`)
      .then(response => {
        setTutorials(response.data);
        console.log(response.data);
      })
      .catch(e => {
        console.log(e);
      });
  };

  return (...);
}

components/Tutorial.js

import { useParams} from 'react-router-dom';

const Tutorial = props => {
  const { id }= useParams();

  const initialTutorialState = ...;
  const [currentTutorial, setCurrentTutorial] = useState(initialTutorialState);

  const getTutorial = id => {
    axios.get(`/tutorials/${id}`)
      .then(response => {
        setCurrentTutorial(response.data);
        console.log(response.data);
      })
      .catch(e => {
        console.log(e);
      });
  };

  useEffect(() => {
    if (id)
      getTutorial(id);
  }, [id]);

  return (...);
}

Using React Custom Hook

Look at the code above, you can see that both components above have a very similar logic. They all call API to get data, save the response data into the state to update again when the data is successfully retrieved. The only difference is that they render different UI and different URL when calling API.

axios.get(...)
  .then(response => {
    ...
  })
  .catch(e => {
    ...
  });

We can reduce the repetition by creating a custom hook useAxiosFetch() for reuse as follows:

customer-hooks/useAxiosFetch.js

import { useState, useEffect } from "react";
import axios from "axios";

axios.defaults.baseURL = "http://localhost:8080/api";

export const useAxiosFetch = (url) => {
  const [data, setData] = useState(undefined);
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(true);

  const fetchData = async () => {
    try {
      const response = await axios.get(url);
      setData(response.data);
    } catch (error) {
      setError(error);
      setLoading(false);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchData();
  }, []);

  return { data, error, loading };
};

From now, in 2 components TutorialsList and Tutorial, we just need to use custom hook useAxiosFetch without worrying too much about the logic inside it. Just know it receives url and returns 3 values: dataloading and error.

We can make the custom hook more dynamic. For example, we want to pass more details of the request (methodurlparamsbody…) instead of only url. Furthermore, we may need to call fetchData() method outside the hook.

Let’s modify a few code like this.

useAxiosFetch.js

import { useState, useEffect } from "react";
import axios from "axios";

axios.defaults.baseURL = "http://localhost:8080/api";

export const useAxiosFetch = (axiosParams) => {
  const [data, setData] = useState(undefined);
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(true);

  const fetchData = async () => {
    try {
      const response = await axios.request(axiosParams);
      setData(response.data);
    } catch (error) {
      setError(error);
      setLoading(false);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchData();
  }, []);

  return { data, error, loading, fetchData };
};

Let’s use this React custom Hook in our components:

components/TutorialsList.js

import React, { useState, useEffect } from "react";
import { useAxiosFetch } from "../custom-hooks/useAxiosFetch";

const TutorialsList = () => {
  const [tutorials, setTutorials] = useState([]);
  const [searchTitle, setSearchTitle] = useState("");

  const { fetchData, data, loading, error } = useAxiosFetch({
    method: "GET",
    url: "/tutorials",
    params: {
      title: searchTitle,
    },
  });

  useEffect(() => {
    if (data) {
      setTutorials(data);
      console.log(data);
    } else {
      setTutorials([]);
    }
  }, [data]);

  useEffect(() => {
    if (error) {
      console.log(error);
    }
  }, [error]);

  useEffect(() => {
    if (loading) {
      console.log("retrieving tutorials...");
    }
  }, [loading]);

  const onChangeSearchTitle = (e) => {
    const searchTitle = e.target.value;
    setSearchTitle(searchTitle);
  };

  const findByTitle = () => {
    fetchData();
  };

  // ...

  return (
    <div>
      <div>
        <input
          type="text"
          placeholder="Search by title"
          value={searchTitle}
          onChange={onChangeSearchTitle}
        />

        <button type="button" onClick={findByTitle} >
          Search
        </button>
      </div>

      <div>
        <h4>Tutorials List</h4>

        {loading && <p>loading...</p>}

        <ul className="list-group">
          {tutorials &&
            tutorials.map((tutorial, index) => (
              <li key={index} >
                {tutorial.title}
              </li>
            ))}
        </ul>
      </div>
    </div>
  );
};

export default TutorialsList;

components/Tutorial.js

import React, { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { useAxiosFetch } from "../custom-hooks/useAxiosFetch";

const Tutorial = () => {
  const { id } = useParams();

  const initialTutorialState = ...;
  const [currentTutorial, setCurrentTutorial] = useState(initialTutorialState);

  const { data, loading, error } = useAxiosFetch({
    method: "GET",
    url: "/tutorials/" + id,
  });

  useEffect(() => {
    if (data) {
      setCurrentTutorial(data);
      console.log(data);
    }
  }, [data]);

  useEffect(() => {
    if (error) {
      console.log(error);
    }
  }, [error]);

  useEffect(() => {
    if (loading) {
      console.log("getting tutorial...");
    }
  }, [loading]);

  const handleInputChange = (event) => {
    const { name, value } = event.target;
    setCurrentTutorial({ ...currentTutorial, [name]: value });
  };

  // ...

  return (
    <div>
      {currentTutorial ? (
        <div>
          <h4>Tutorial</h4>

          { loading && <p>loading...</p>}

          <form>
            <div>
              <label htmlFor="title">Title</label>
              <input
                type="text"
                id="title"
                name="title"
                value={currentTutorial.title}
                onChange={handleInputChange}
              />
            </div>
            <div>
              <label htmlFor="description">Description</label>
              <input
                type="text"
                id="description"
                name="description"
                value={currentTutorial.description}
                onChange={handleInputChange}
              />
            </div>

            <div>
              <label>
                <strong>Status:</strong>
              </label>
              {currentTutorial.published ? "Published" : "Pending"}
            </div>
          </form>

          ...

        </div>
      ) : (
        <div>
          <br />
          <p>Please click on a Tutorial...</p>
        </div>
      )}
    </div>
  );
};

export default Tutorial;

Leave a Reply