English

Unlock the power of reusable logic in your React applications with custom hooks. Learn how to create and leverage custom hooks for cleaner, more maintainable code.

Custom Hooks: Reusable Logic Patterns in React

React Hooks revolutionized the way we write React components by introducing state and lifecycle features to functional components. Among the many benefits they offer, custom hooks stand out as a powerful mechanism for extracting and reusing logic across multiple components. This blog post will dive deep into the world of custom hooks, exploring their benefits, creation, and usage with practical examples.

What are Custom Hooks?

In essence, a custom hook is a JavaScript function that starts with the word "use" and can call other hooks. They allow you to extract component logic into reusable functions. This is a powerful way to share stateful logic, side effects, or other complex behaviors between components without resorting to render props, higher-order components, or other complex patterns.

Key Characteristics of Custom Hooks:

Benefits of Using Custom Hooks

Custom hooks offer several significant advantages in React development:

Creating Your First Custom Hook

Let's illustrate the creation of a custom hook with a practical example: a hook that tracks the window size.

Example: useWindowSize

This hook will return the current width and height of the browser window. It will also update these values when the window is resized.

import { useState, useEffect } from 'react';

function useWindowSize() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    function handleResize() {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }

    window.addEventListener('resize', handleResize);

    // Remove event listener on cleanup
    return () => window.removeEventListener('resize', handleResize);
  }, []); // Empty array ensures that effect is only run on mount

  return windowSize;
}

export default useWindowSize;

Explanation:

  1. Import Necessary Hooks: We import useState and useEffect from React.
  2. Define the Hook: We create a function named useWindowSize, adhering to the naming convention.
  3. Initialize State: We use useState to initialize the windowSize state with the initial width and height of the window.
  4. Set Up Event Listener: We use useEffect to add a resize event listener to the window. When the window is resized, the handleResize function updates the windowSize state.
  5. Cleanup: We return a cleanup function from useEffect to remove the event listener when the component unmounts. This prevents memory leaks.
  6. Return Values: The hook returns the windowSize object, containing the current width and height of the window.

Using the Custom Hook in a Component

Now that we have created our custom hook, let's see how to use it in a React component.

import React from 'react';
import useWindowSize from './useWindowSize';

function MyComponent() {
  const { width, height } = useWindowSize();

  return (
    

Window width: {width}px

Window height: {height}px

); } export default MyComponent;

Explanation:

  1. Import the Hook: We import the useWindowSize custom hook.
  2. Call the Hook: We call the useWindowSize hook within the component.
  3. Access Values: We destructure the returned object to get the width and height values.
  4. Render Values: We render the width and height values in the component's UI.

Any component that uses useWindowSize will automatically update when the window size changes.

More Complex Examples

Let's explore some more advanced use cases for custom hooks.

Example: useLocalStorage

This hook allows you to easily store and retrieve data from local storage.

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  // State to store our value
  // Pass initial value to useState so logic is only executed once
  const [storedValue, setStoredValue] = useState(() => {
    try {
      // Get from local storage by key
      const item = window.localStorage.getItem(key);
      // Parse stored json or if none return initialValue
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      // If error also return initialValue
      console.log(error);
      return initialValue;
    }
  });

  // Return a wrapped version of useState's setter function that ...
  // ... persists the new value to localStorage.
  const setValue = (value) => {
    try {
      // Allow value to be a function so we have same API as useState
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      // Save to local storage
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
      // Save state
      setStoredValue(valueToStore);
    } catch (error) {
      // A more advanced implementation would handle the error case
      console.log(error);
    }
  };

  useEffect(() => {
    try {
      const item = window.localStorage.getItem(key);
      setStoredValue(item ? JSON.parse(item) : initialValue);
    } catch (error) {
      console.log(error);
    }
  }, [key, initialValue]);

  return [storedValue, setValue];
}

export default useLocalStorage;

Usage:

import React from 'react';
import useLocalStorage from './useLocalStorage';

function MyComponent() {
  const [name, setName] = useLocalStorage('name', 'Guest');

  return (
    

Hello, {name}!

setName(e.target.value)} />
); } export default MyComponent;

Example: useFetch

This hook encapsulates the logic for fetching data from an API.

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    async function fetchData() {
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const json = await response.json();
        setData(json);
        setLoading(false);
      } catch (error) {
        setError(error);
        setLoading(false);
      }
    }

    fetchData();
  }, [url]);

  return { data, loading, error };
}

export default useFetch;

Usage:

import React from 'react';
import useFetch from './useFetch';

function MyComponent() {
  const { data, loading, error } = useFetch('https://jsonplaceholder.typicode.com/todos/1');

  if (loading) return 

Loading...

; if (error) return

Error: {error.message}

; return (

Title: {data.title}

Completed: {data.completed ? 'Yes' : 'No'}

); } export default MyComponent;

Best Practices for Custom Hooks

To ensure that your custom hooks are effective and maintainable, follow these best practices:

Common Pitfalls to Avoid

Advanced Patterns

Composing Custom Hooks

Custom hooks can be composed together to create more complex logic. For example, you could combine a useLocalStorage hook with a useFetch hook to automatically persist fetched data to local storage.

Sharing Logic Between Hooks

If multiple custom hooks share common logic, you can extract that logic into a separate utility function and reuse it in both hooks.

Using Context with Custom Hooks

Custom hooks can be used in conjunction with React Context to access and update global state. This allows you to create reusable components that are aware of and can interact with the application's global state.

Real-World Examples

Here are some examples of how custom hooks can be used in real-world applications:

Example : useGeolocation hook for cross-cultural applications like mapping or delivery services

import { useState, useEffect } from 'react';

function useGeolocation() {
  const [location, setLocation] = useState({
    latitude: null,
    longitude: null,
    error: null,
  });

  useEffect(() => {
    if (!navigator.geolocation) {
      setLocation({
        latitude: null,
        longitude: null,
        error: 'Geolocation is not supported by this browser.',
      });
      return;
    }

    const watchId = navigator.geolocation.watchPosition(
      (position) => {
        setLocation({
          latitude: position.coords.latitude,
          longitude: position.coords.longitude,
          error: null,
        });
      },
      (error) => {
        setLocation({
          latitude: null,
          longitude: null,
          error: error.message,
        });
      }
    );

    return () => navigator.geolocation.clearWatch(watchId);
  }, []);

  return location;
}

export default useGeolocation;

Conclusion

Custom hooks are a powerful tool for writing cleaner, more reusable, and more maintainable React code. By encapsulating complex logic in custom hooks, you can simplify your components, reduce code duplication, and improve the overall structure of your applications. Embrace custom hooks and unlock their potential to build more robust and scalable React applications.

Start by identifying areas in your existing codebase where logic is being repeated across multiple components. Then, refactor that logic into custom hooks. Over time, you'll build a library of reusable hooks that will accelerate your development process and improve the quality of your code.

Remember to follow best practices, avoid common pitfalls, and explore advanced patterns to get the most out of custom hooks. With practice and experience, you'll become a master of custom hooks and a more effective React developer.