React Hooks Tutorial: Complete Guide to Custom Hooks with Examples 2024

Cover Image for React Hooks Tutorial: Complete Guide to Custom Hooks with Examples 2024
toofancoder
toofancoder

React Hooks: A Comprehensive Guide to React Hooks and Custom Hooks

React Hooks, introduced in React v16.8, are functions that let you “hook into” React state and lifecycle features from function components. Hooks make functional components more powerful by allowing them to manage their own state and side effects, replacing the need for class-based components in most cases.

In this article, we'll cover the basics of React Hooks, dive into custom hooks, and provide examples to demonstrate their use.

1. Understanding React Hooks

Hooks simplify the management of state and other React functionalities in functional components. The most commonly used hooks are:

  • useState: Manages component state.
  • useEffect: Handles side effects, such as data fetching and subscriptions.
  • useContext: Provides access to a global state or context.
  • useReducer: An alternative to useState for complex state management.
  • useRef: Creates references to DOM elements or mutable variables.
  • useMemo: Optimizes performance by memoizing expensive computations.
  • useCallback: Memoizes functions to prevent unnecessary re-renders.

Let’s briefly go over the essential ones before moving to custom hooks.


2. Core React Hooks in Action

Here are examples of the core hooks to provide a foundation:

a) useState

The useState hook initializes and manages state within a function component.

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Current count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

b) useEffect

The useEffect hook handles side effects, such as data fetching or updating the DOM.

import React, { useState, useEffect } from 'react';

function FetchData() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/posts/1')
      .then(response => response.json())
      .then(data => setData(data));
  }, []); // Empty dependency array ensures this runs only once

  return (
    <div>
      {data ? <p>{data.title}</p> : <p>Loading...</p>}
    </div>
  );
}

3. Custom Hooks: Enhancing Reusability

Custom hooks are JavaScript functions that start with use and can call other hooks within them. React custom hooks help to extract and reuse component logic in a clean and efficient way, making the codebase more maintainable.

a) Creating a Simple Custom Hook

Consider a use case where multiple components need to manage a timer. Instead of duplicating code, we can create a custom hook:

import { useState, useEffect } from 'react';

function useTimer(initialTime = 0) {
  const [time, setTime] = useState(initialTime);

  useEffect(() => {
    const timerId = setInterval(() => setTime(prevTime => prevTime + 1), 1000);
    return () => clearInterval(timerId);
  }, []);

  return time;
}

// Usage in a component
function TimerDisplay() {
  const time = useTimer();

  return <div>Time elapsed: {time} seconds</div>;
}

b) useFetch: Custom Hook for Data Fetching

This custom hook can manage data fetching logic for multiple components, reducing redundancy and making code easier to maintain.

import { useState, useEffect } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await fetch(url);
        if (!response.ok) throw new Error('Failed to fetch');
        const data = await response.json();
        setData(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [url]);

  return { data, loading, error };
}

// Using useFetch in a component
function UserList() {
  const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/users');

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Why Use Custom Hooks in React?

Custom hooks in React are a powerful way to encapsulate and reuse logic across different components. They provide a more organized and efficient code structure, making React applications cleaner, more maintainable, and easier to scale. Here’s a look at why custom hooks are so beneficial:

  1. Code Reusability When building React applications, you often find yourself implementing the same logic in multiple components. For example, fetching data from an API, managing form input, or setting up a timer. Custom hooks allow you to isolate this logic into reusable functions. Instead of rewriting the same code in every component, you create custom hooks once and call it wherever needed. This approach leads to a DRY (Don’t Repeat Yourself) codebase, reducing redundancy and making your app easier to maintain.
  2. Improved Readability and Organization Complex components can quickly become difficult to understand, especially when they handle multiple responsibilities. By moving logic into custom hooks, you can separate concerns and make each component’s code easier to read. Custom hooks allow you to abstract away internal workings, helping developers understand the component’s structure and purpose at a glance. For instance, a useFetch hook clearly indicates that the component will handle data fetching, improving code readability.
  3. Cleaner, More Modular Components Custom hooks enable a more modular approach to building React components. Instead of having all logic within a single component, you can break it out into smaller, focused units. This modular approach is beneficial for debugging and testing, as you can independently test the functionality of each hook.
  4. Better State Management Custom hooks can simplify state management within functional components. Hooks like useState and useEffect are often combined in custom hooks to manage local state and side effects in one place. For example, if you create a useForm hook, you can encapsulate all form-related state and validation logic in one hook, reducing the complexity of individual form components.
  5. Enhanced Testability By isolating complex logic within custom hooks, you can test each piece of functionality independently, outside the context of any UI components. This separation makes it easier to write unit tests, ensuring each part of your code functions correctly. Testing hooks independently of components also reduces the need for full render tests, speeding up the testing process.

4. Advanced Custom Hook Example: useLocalStorage

The useLocalStorage hook is a custom hook that manages state stored in localStorage, allowing data persistence across page reloads.

import { useState } from 'react';

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
}

// Usage of useLocalStorage in a component
function ThemeSwitcher() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  return (
    <div>
      <p>Current theme: {theme}</p>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </div>
  );
}

5. Best Practices with Custom Hooks

  • Start with use: To ensure React treats the function as a hook.
  • Use for Reusable Logic Only: Custom hooks should encapsulate logic that will be reused across components.
  • Keep Hooks Focused: Custom hooks should ideally focus on a single responsibility to avoid complexity.

6. Conclusion

React Hooks provide a powerful, efficient way to manage state, side effects, and reusable logic in functional components. By leveraging both built-in hooks and custom hooks, you can write cleaner, more maintainable code. Custom hooks are particularly valuable for encapsulating logic that can be reused across multiple components, making your application more modular and easier to manage.