Module: Advanced Hooks

Custom Hooks

Custom Hooks are a powerful feature in React that allow you to extract component logic into reusable functions. They’re a key part of writing clean, maintainable, and testable React code. Essentially, they let you “hook into” React state and lifecycle features from JavaScript functions, without needing to be inside a component.

What are Custom Hooks?

  • JavaScript functions:Custom Hooks are just regular JavaScript functions.
  • useprefix:By convention, the name of a custom Hookalwaysstarts withuse. This is crucial for React to understand that it’s a Hook and should follow the Rules of Hooks.
  • Call other Hooks:Custom Hooks can call other Hooks (likeuseState,useEffect,useContext, etc.) to leverage React’s features.
  • Reusable logic:They encapsulate and reuse stateful logic across multiple components.
  • Not a component:They arenotReact components themselves. They don’t render anything.

Why Use Custom Hooks?

  • Code Reusability:Avoid repeating the same logic in multiple components.
  • Improved Readability:Extract complex logic into separate functions, making components cleaner and easier to understand.
  • Testability:Easier to test logic isolated in a function than within a component.
  • Separation of Concerns:Keep components focused on rendering UI, while logic resides in Hooks.
  • Maintainability:Changes to the logic only need to be made in one place (the Hook) instead of multiple components.

Creating a Custom Hook

Let’s create a simple custom Hook calleduseCounter. This Hook will manage a counter state and provide functions to increment and decrement it.


import { useState } from 'react';

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

  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };

  const decrement = () => {
    setCount(prevCount => prevCount - 1);
  };

  const reset = () => {
    setCount(initialValue);
  };

  return {
    count,
    increment,
    decrement,
    reset,
  };
}

export default useCounter;

Explanation:

  1. import { useState } from 'react';: We import the useState Hook from React.
  2. function useCounter(initialValue = 0): We define a function named useCounter. It accepts an optional initialValue for the counter. The name starts with use as required.
  3. const [count, setCount] = useState(initialValue);: We use useState to create a state variable count and a function setCount to update it.
  4. increment, decrement, reset functions: These functions update the count state using setCount. Note the use of the functional update form (prevCount => prevCount + 1) which is best practice when the new state depends on the previous state.
  5. return { count, increment, decrement, reset };: The Hook returns an object containing the count state and the functions to manipulate it. This is what components will use.

Using the Custom Hook in a Component

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

function CounterComponent() {
  const { count, increment, decrement, reset } = useCounter();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

export default CounterComponent;

Explanation:

  1. import useCounter from './useCounter';: We import the useCounter Hook.
  2. const { count, increment, decrement, reset } = useCounter();: We call the useCounter Hook inside the component. This returns the object we defined in the Hook, and we destructure it to get the count state and the increment, decrement, and reset functions.
  3. Using the returned values: We use the count state to display the current count and the functions as event handlers for the buttons.

Passing Arguments to Custom Hooks

Custom Hooks can accept arguments, just like regular JavaScript functions. This allows you to customize the Hook's behavior based on the component's needs. In our useCounter example, we already passed an initialValue.

Example: useFetch Hook

Let's create a more complex custom Hook called useFetch to fetch 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(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`);
        }
        const json = await response.json();
        setData(json);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    // Cleanup function (optional, but good practice)
    return () => {
      // You could abort the fetch request here if needed
    };
  }, [url]); // Dependency array: useEffect runs when 'url' changes

  return { data, loading, error };
}

export default useFetch;

Explanation:

  1. useState for data, loading, and error: We use useState to manage the data fetched from the API, a loading state, and any potential errors.
  2. useEffect for fetching data: We use useEffect to perform the data fetching side effect.
  3. fetchData async function: This function handles the actual API call.
  4. Error handling: We use try...catch to handle potential errors during the fetch process.
  5. finally block: The finally block ensures that setLoading(false) is always called, regardless of whether the fetch was successful or not.
  6. Dependency array: The [url] dependency array tells useEffect to re-run the effect whenever the url prop changes.
  7. Return values: The Hook returns an object containing the data, loading, and error states.

Using the useFetch Hook

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

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

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

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

  return (
    <div>
      <h1>Data from API:</h1>
      <p>Title: {data.title}</p>
      <p>Completed: {data.completed ? 'Yes' : 'No'}</p>
    </div>
  );
}

export default DataComponent;

Rules of Hooks

Custom Hooks must follow the same rules as built-in Hooks:

  1. Only call Hooks at the top level: Don't call Hooks inside loops, conditions, or nested functions.
  2. Only call Hooks from React function components: Don't call Hooks from class components or regular JavaScript functions.
  3. Don't return anything other than values: Custom Hooks should only return values. Don't return functions or objects with methods. (Though returning functions within the returned object is perfectly fine, as demonstrated in the examples).

Best Practices

  • Keep Hooks focused: Each Hook should have a single, well-defined purpose.
  • Use descriptive names: Choose names that clearly indicate the Hook's functionality.
  • Document your Hooks: Explain what the Hook does, what arguments it accepts, and what it returns.
  • Consider reusability: Design Hooks to be as generic and reusable as possible.
  • Test your Hooks: Write unit tests to ensure that your Hooks are working correctly.

Custom Hooks are a powerful tool for building complex React applications. By extracting reusable logic into Hooks, you can write cleaner, more maintainable, and more testable code. ```