ReactJS : Building Clean Components with Custom Data Fetching Hooks

ReactJS : Building Clean Components with Custom Data Fetching Hooks

Introduction

Managing HTTP requests in React components presents challenges, especially regarding code cleanliness and maintainability. This article delves into the creation of clean and modular components through the utilization of custom data fetching hooks.

The Initial App Component

Here's the existing App component involved in making HTTP requests, including knowledge of request types, endpoints, and cancellation using the abort controller. We don't want this in our components because our components should be primarily responsible for returning markup and handling user interactions at a high level.

// src\App.tsx

import axios, { CanceledError } from 'axios';
import { useEffect, useState } from 'react';

interface User {
  id: number;
  name: string;
}

const App = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState('');
  const [isLoading, setLoading] = useState(false);

  useEffect(() => {
    const controller = new AbortController();

    setLoading(true);
    axios
      .get<User[]>('https://jsonplaceholder.typicode.com/users', {
        signal: controller.signal,
      })
      .then((res) => {
        setUsers(res.data);
        setLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setLoading(false);
      });
    return () => controller.abort();
  }, []);
  return (
    <>
      {error && <p>{error}</p>}
      {isLoading && <p>Loading...</p>}
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </>
  );
};

export default App;

Creating a Centralized API Client Module

To improve the code, a centralized 'services' folder is created within the 'src' directory, to contain functionality separate from the UI. Inside this folder, the 'api-client.ts' file is created to store default configuration settings for HTTP calls, showcasing the use of axios.create for a reusable axios instance.

We start by importing Axios, then use axios.create to set up a customized axios instance with a specific base URL—connecting it to our backend. This module is designed with reusability in mind; we keep the base URL here, and if needed, we can add optional HTTP headers, like API keys.

//src\services\api-client.ts

import axios from "axios";

export default axios.create({
    baseURL: 'https://jsonplaceholder.typicode.com',
    // headers:{
    //     'api-key':'***'
    // }                 
})

The result is an efficient axios client configuration, neatly packaged and exported as the default object from 'api-client.ts'. Now, this centralized API client is ready for seamless integration across our application, ensuring a cleaner and more organized development structure.

Integrating API Client into the App Component

The 'api-client' is imported into the App component to replace direct axios references. So with this change, our code is cleaner.

//src\App.tsx

import { CanceledError } from 'axios';
import { useEffect, useState } from 'react';
import apiClient from './services/api-client';

interface User {
  id: number;
  name: string;
}

const App = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState('');
  const [isLoading, setLoading] = useState(false);

  useEffect(() => {
    const controller = new AbortController();

    setLoading(true);
    apiClient
      .get<User[]>('/users', {
        signal: controller.signal,
      })
      .then((res) => {
        setUsers(res.data);
        setLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message);
        setLoading(false);
      });
    return () => controller.abort();
  }, []);
  return (
    <>
      {error && <p>{error}</p>}
      {isLoading && <p>Loading...</p>}
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </>
  );
};

export default App;

Identifying Code Duplication and Separation of Concerns

While there is nothing particularly wrong with the existing implementation as seen above, in scenarios where multiple components require data fetching, we may spot potential code duplication. To address this, we introduce custom hooks, a powerful concept for separating concerns and boosting code modularity and reusability.

Imagine another component needing to fetch a user list. Dealing with three state variables and repeating the fetching logic leads to code duplication. This is where custom hooks shine—they efficiently share functionality across components.

Think of hooks not just for sharing functionality but also for separating concerns, making code more modular and reusable. Let's create a custom hook for fetching the users and moving the entire logic inside this hook for a cleaner and more efficient approach.

Introduction to Custom Hooks

Custom hooks are your key to encapsulating reusable logic in functions. By convention custom hooks typically start with 'use', just like we have useState, useEffect and useRef.

Building the useUsers Custom Hook

In the source directory, create a 'hooks' folder and add 'useUsers.ts' within it. Now this is just a regular TypeScript module,inside this module we define a function named 'useUsers' and export it.

We need to move the entire logic, meaning the logic for fetching users, managing state variables, and handling errors into the 'useUsers' custom hook. By doing so, we promote cleaner code and improved organization.

In this hook, we return an object containing essential properties – 'users,' 'error,' and 'isLoading.' This ensures our state variables are readily available for use in the app component, contributing to a more modular and concise structure.

//src\hooks\useUsers.ts
import { useEffect, useState } from 'react';
import apiClient from '../services/api-client';
import { CanceledError } from "axios";

interface User {
  id: number;
  name: string;
}


const useUsers = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [error, setError] = useState('');
const [isLoading, setLoading] = useState(false);


  useEffect(() => {
    const controller = new AbortController();

    setLoading(true);
    apiClient
      .get<User[]>('/users', { signal: controller.signal })
      .then((res) => {
        setUsers(res.data);
        setLoading(false);
      })
      .catch((err) => {
        if (err instanceof CanceledError) return;
        setError(err.message)
        setLoading(false);
      });
  return () => controller.abort();
  }, []);

  return  { users, error, isLoading };
};

export default useUsers;

Utilizing Custom Hook in the App Component

Now, let's apply our 'useUsers' custom hook in the App component. By importing and implementing this hook, we achieve a streamlined and modular structure. In the App component, a simple call to useUsers() allows us to effortlessly access the list of users, error details, and loading properties. This showcases the power of using custom hooks for efficient data fetching.

//src\App.tsx

import useUsers from './hooks/useUsers';

const App = () => {
  const { users, error, isLoading } = useUsers();
  return (
    <>
      {error && <p>{error}</p>}
      {isLoading && <p>Loading...</p>}
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </>
  );
};

export default App;

Benefits of Custom Hooks

Let's highlight the perks of using custom hooks in React:

  • Code Modularity and Reusability: Custom hooks enhance code modularity, making it cleaner and more reusable across components.

  • Separation of Concerns: By using custom hooks, we ensure that components focus on their primary responsibilities, promoting a clear separation of concerns.

Now our component is a lot cleaner. It doesn't know anything about making HTTP requests, endpoint, and cancelling requests, it's primarily responsible for returning some markup.

Conclusion

In conclusion, adopting custom data fetching hooks in React has significantly improved code cleanliness and maintainability. Centralizing the API client and leveraging custom hooks, like 'useUsers,' streamlines code, minimizes duplication, and enhances modularity. This approach ensures a clear separation of concerns in components, simplifying development and saving valuable time. Ultimately, it represents an evolution in best practices, yielding cleaner, more maintainable, and extensible codebases.

Thank you for reading! Happy coding!