React Optimization through Custom Hooks
Optimizing Data Fetching, Debouncing Search Inputs, and Filtering Results in a React Application using 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:
Fetching data using a custom
useFetchData
hook.Keeping track of the search value entered by the user.
Debouncing the search input to optimize performance with the
useDebouncedSearch
hook.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:
Code Reusability and Maintainability: By encapsulating complex logic in custom hooks, they can be reused across different components, making the codebase easier to maintain.
Separation of Concerns: Each hook has its own responsibility, which makes the code easier to reason about and test.
Performance Optimization: Debouncing user input saves computational resources by avoiding unnecessary re-renders or API calls.
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