React Optimization through Custom Hooks

Optimizing Data Fetching, Debouncing Search Inputs, and Filtering Results in a React Application using Custom Hooks

React Optimization through Custom Hooks

In today's blog post, we're going to discuss an optimized implementation of a search feature in a React application using custom hooks. Our application fetches a list of tasks from an API and provides search functionality to filter through them.

App Component

Our main application component, App, is where we initialize and handle our data fetching, search functionality, and rendering. The code for App component is as follows:

import { useEffect, useState } from "react";
import useDebouncedSearch from "./useDebouncedSearch";
import useSearchResults from "./useSearchResults";

const useFetchData = (url) => {
  const [state, setState] = useState({
    data: [],
    isLoading: true,
    error: null
  });

  useEffect(() => {
    const fetchData = async () => {
      try {
        const res = await fetch(url);
        if (!res.ok) {
          throw new Error(`HTTP error! status: ${res.status}`);
        }
        const data = await res.json();
        setState({ data, isLoading: false, error: null });
      } catch (err) {
        setState({ data: [], isLoading: false, error: err.message });
      }
    };

    fetchData();
  }, [url]);

  return state;
};

export default function App() {
  const { data, isLoading, error } = useFetchData(
    "https://jsonplaceholder.typicode.com/todos/"
  );

  const [searchVal, setSearchVal] = useState("");
  const debouncedSearch = useDebouncedSearch(searchVal, 500);
  const results = useSearchResults(debouncedSearch, data ?? []);

  if (isLoading) {
    return <p>Loading...</p>;
  }

  if (error) {
    return <p>{error}</p>;
  }

  const listItems = results.length ? results : data;

  return (
    <>
      <label htmlFor="search">Search</label>
      <input
        id="search"
        type="text"
        onChange={(e) => setSearchVal(e.target.value)}
      />
      <ol>
        {listItems.map((item) => (
          <li key={item.id}>
            <span>{item.title}</span>
          </li>
        ))}
      </ol>
    </>
  );
}

The App component is responsible for:

  1. Fetching data using a custom useFetchData hook.

  2. Keeping track of the search value entered by the user.

  3. Debouncing the search input to optimize performance with the useDebouncedSearch hook.

  4. Filtering the data based on the debounced search input using the useSearchResults hook.

useFetchData Hook

useFetchData is a custom hook that abstracts the fetching of data from an API. It takes a URL as a parameter, manages the state of the data (isLoading, error, data), and returns that state. By encapsulating the data fetching logic in a custom hook, we make it reusable across different components, enhancing maintainability.

Here's the entire useFetchData hook:

const useFetchData = (url) => {
  const [state, setState] = useState({
    data: [],
    isLoading: true,
    error: null
  });

  useEffect(() => {
    const fetchData = async () => {
      try {
        const res = await fetch(url);
        if (!res.ok) {
          throw new Error(`HTTP error! status: ${res.status}`);
        }
        const data = await res.json();
        setState({ data, isLoading: false, error: null });
      } catch (err) {
        setState({ data: [], isLoading: false, error: err.message });
      }
    };

    fetchData();
  }, [url]);

  return state;
};

useDebouncedSearch Hook

useDebouncedSearch is a custom hook that debounces a value that changes over time. This helps delay the execution of a function until a given amount of time has passed without it being called. This is particularly useful for optimizing performance when handling user input for filtering or searching.

Here's the full useDebouncedSearch hook:

import { useEffect, useState } from "react";

function useDebouncedSearch(search, delay) {
  const [debouncedSearch, setDebouncedSearch] = useState(search);

  useEffect(() => {
    const timeoutId = setTimeout(() => {
      setDebouncedSearch(search);
    }, delay);

    return () => {
      clearTimeout(timeoutId);
    };
  }, [search, delay]);

  return debouncedSearch;
}

export default useDebouncedSearch;

useSearchResults Hook

useSearchResults is a custom hook that filters the fetched data based on the debounced search value. It maintains the single-responsibility principle by keeping this filtering logic out of the main component.

Here's the complete useSearchResults hook:

import { useEffect, useState } from "react";

function useSearchResults(debouncedSearch, data) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    const searchResults = data.filter((item) =>
      item.title.includes(debouncedSearch)
    );
    setResults(searchResults);
  }, [debouncedSearch, data]);

  return results;
}

export default useSearchResults;

Benefits

The benefits of this approach are numerous:

  1. Code Reusability and Maintainability: By encapsulating complex logic in custom hooks, they can be reused across different components, making the codebase easier to maintain.

  2. Separation of Concerns: Each hook has its own responsibility, which makes the code easier to reason about and test.

  3. Performance Optimization: Debouncing user input saves computational resources by avoiding unnecessary re-renders or API calls.

  4. Readability: The main component App becomes cleaner and easier to understand when hooks are used to separate out different functionalities.

This approach demonstrates the power of custom hooks in React. They allow us to create cleaner, more optimized, and maintainable codebases, and they truly shine when encapsulating complex logic and state management for UI components.

Try on codesandbox

https://codesandbox.io/s/debounce-search-htn48q?file=/src/App.js

Did you find this article valuable?

Support Adeesh Sharma by becoming a sponsor. Any amount is appreciated!