Next.js & React-Use: A Beginner’s Guide to Custom Hooks

In the world of React and Next.js, building reusable and maintainable components is key to efficient development. As projects grow, you’ll find yourself repeating similar logic across different components. This is where custom hooks come to the rescue. They allow you to extract stateful logic from functional components, making your code cleaner, more organized, and easier to test. This tutorial will dive into the power of custom hooks, specifically using the react-use library, a collection of useful React hooks, and guide you through practical examples to enhance your Next.js applications.

Why Custom Hooks and React-Use Matter

Imagine you’re building a website with features like fetching data, handling user input, or managing the browser’s online status. Without custom hooks, you’d likely duplicate this logic across multiple components, leading to potential inconsistencies and a maintenance nightmare. Custom hooks, combined with libraries like react-use, solve this by encapsulating these functionalities into reusable units. They are essentially JavaScript functions whose names start with “use” and can call other hooks.

React-use is a fantastic library because it provides a wide array of pre-built custom hooks that cover common use cases. This saves you time and effort, allowing you to focus on the core logic of your application.

Setting Up Your Next.js Project

Before we begin, ensure you have Node.js and npm (or yarn) installed. If you don’t have a Next.js project set up, create one using the following command:

npx create-next-app my-react-use-app
cd my-react-use-app

Next, install the react-use library:

npm install react-use
# or
yarn add react-use

Understanding the Basics of Custom Hooks

A custom hook is a JavaScript function whose name starts with “use” and that may call other hooks. The key is that it *must* call other hooks inside of it, such as useState, useEffect, or other custom hooks. This allows you to encapsulate stateful logic and re-use it across multiple components. Let’s create a simple custom hook to illustrate this.

Example: A Simple Counter Hook

Create a file named useCounter.js in your project’s /components directory. This hook will manage a counter:

import { useState } from 'react';

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

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

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

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

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

export default useCounter;

This hook uses the useState hook to manage the counter’s state and provides functions to increment, decrement, and reset the counter. Notice the “use” prefix.

Now, let’s use this hook in a component. Create a file named CounterComponent.js in the /components directory:

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

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

  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;

In this component, we import and use our useCounter hook. The hook returns the current count and functions to manipulate it. This keeps the component clean and focused on rendering the UI.

Finally, to render this component, modify your pages/index.js file to include:

import CounterComponent from '../components/CounterComponent';

function HomePage() {
  return (
    <div>
      <CounterComponent />
    </div>
  );
}

export default HomePage;

Run your Next.js development server (npm run dev or yarn dev) and you should see a counter that you can increment, decrement, and reset.

Exploring React-Use Hooks

React-use offers a variety of hooks for different use cases. Let’s explore some of the most useful ones with practical examples.

1. useLocalStorage

This hook allows you to easily manage data in the browser’s local storage. This is extremely helpful for persisting user preferences or application state.

Example: Storing and Retrieving a Theme Preference

Create a new component file, ThemeSwitcher.js, in your /components directory:

import React from 'react';
import { useLocalStorage } from 'react-use';

function ThemeSwitcher() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  const toggleTheme = () => {
    setTheme(theme === 'light' ? 'dark' : 'light');
  };

  return (
    <div>
      <p>Current Theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}

export default ThemeSwitcher;

In this example, useLocalStorage takes two arguments: the key for the local storage item ('theme') and the default value ('light'). It returns an array, where the first element is the current value, and the second is a function to update it. The theme is toggled between “light” and “dark” on button click. Now, add this component to your pages/index.js.

import CounterComponent from '../components/CounterComponent';
import ThemeSwitcher from '../components/ThemeSwitcher';

function HomePage() {
  return (
    <div>
      <CounterComponent />
      <ThemeSwitcher />
    </div>
  );
}

export default HomePage;

When you reload the page, the theme selection will persist even if you close and reopen the browser tab. This demonstrates the power of useLocalStorage.

2. useFetch

This hook simplifies making API requests. It handles the loading state, error handling, and data fetching, making asynchronous operations much cleaner.

Example: Fetching and Displaying Data from a Public API

Create a new component, DataFetcher.js, in the /components directory:

import React from 'react';
import { useFetch } from 'react-use';

function DataFetcher() {
  const { value, loading, error } = useFetch('https://rickandmortyapi.com/api/character/1');

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

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

  if (!value) {
    return null;
  }

  return (
    <div>
      <h3>{value.name}</h3>
      <p>Status: {value.status}</p>
      <img src={value.image} alt={value.name} />
    </div>
  );
}

export default DataFetcher;

This component fetches data from the Rick and Morty API. The useFetch hook automatically handles the loading state, and error. It returns an object containing the fetched value, a loading boolean, and an error object. Add this component to your pages/index.js.

import CounterComponent from '../components/CounterComponent';
import ThemeSwitcher from '../components/ThemeSwitcher';
import DataFetcher from '../components/DataFetcher';

function HomePage() {
  return (
    <div>
      <CounterComponent />
      <ThemeSwitcher />
      <DataFetcher />
    </div>
  );
}

export default HomePage;

When you load the page, you’ll see a “Loading…” message while the data is fetched, followed by the character’s information.

3. useDebounce

This hook is useful for debouncing function calls, often used in scenarios like search input fields or window resizing, to prevent excessive updates.

Example: Debouncing a Search Input

Create a new component, SearchInput.js, in the /components directory:

import React, { useState, useCallback } from 'react';
import { useDebounce } from 'react-use';

function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('');
  const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');

  const debouncedCallback = useDebounce(
    () => {
      setDebouncedSearchTerm(searchTerm);
      // Simulate making an API call with the debounced search term
      console.log('Searching for:', searchTerm);
    },
    500, // Debounce time in milliseconds
    [searchTerm] // Dependencies
  );

  const handleInputChange = useCallback((event) => {
    setSearchTerm(event.target.value);
  }, []);

  // Run the debounced callback whenever the debouncedSearchTerm changes
  React.useEffect(() => {
    debouncedCallback();
  }, [debouncedCallback]);

  return (
    <div>
      <input
        type="text"
        placeholder="Search..."
        value={searchTerm}
        onChange={handleInputChange}
      />
      <p>Debounced Search Term: {debouncedSearchTerm}</p>
    </div>
  );
}

export default SearchInput;

This component includes an input field. The useDebounce hook delays the execution of a callback function. In this case, the callback logs the search term to the console. The second argument, 500, sets the debounce time to 500 milliseconds. The third argument, an array of dependencies, ensures the callback is re-created if the dependencies change. Add this component to your pages/index.js.

import CounterComponent from '../components/CounterComponent';
import ThemeSwitcher from '../components/ThemeSwitcher';
import DataFetcher from '../components/DataFetcher';
import SearchInput from '../components/SearchInput';

function HomePage() {
  return (
    <div>
      <CounterComponent />
      <ThemeSwitcher />
      <DataFetcher />
      <SearchInput />
    </div>
  );
}

export default HomePage;

Type in the search field. You’ll notice that the “Searching for:” message only appears in the console after you’ve stopped typing for half a second. This demonstrates how useDebounce can optimize performance by reducing the number of function calls, especially when dealing with APIs.

Common Mistakes and How to Fix Them

While custom hooks and react-use are powerful, there are some common pitfalls to avoid.

  • Incorrect Dependency Arrays: When using useEffect or other hooks that require dependencies, make sure the dependency array includes *all* the values that are used inside the hook. Missing dependencies can lead to stale data or unexpected behavior.
  • Not Following the “use” Naming Convention: Custom hooks *must* start with “use” for React to recognize them correctly.
  • Calling Hooks Conditionally: Hooks should only be called at the top level of your function components or custom hooks, not inside loops, conditions, or nested functions.
  • Overusing Hooks: While custom hooks promote reusability, avoid creating excessively complex hooks that are difficult to understand or maintain. Keep them focused on specific tasks.

Step-by-Step Instructions Summary

  1. Set up your Next.js project: Use create-next-app to create a new project.
  2. Install react-use: Run npm install react-use or yarn add react-use.
  3. Understand the basics of custom hooks: Learn how to create your own hooks, starting with the “use” prefix.
  4. Explore and use react-use hooks: Start with hooks like useLocalStorage, useFetch, and useDebounce.
  5. Integrate hooks into your components: Use the hooks to manage state, fetch data, and handle side effects.
  6. Test your components: Ensure that the hooks function as expected.
  7. Refactor and optimize: Continuously refine your code for better performance and maintainability.

Key Takeaways

  • Custom hooks are a powerful way to encapsulate and reuse stateful logic in React and Next.js.
  • The react-use library provides a rich set of pre-built hooks for common tasks.
  • Using custom hooks leads to cleaner, more maintainable, and testable code.
  • Always pay attention to dependencies when using hooks like useEffect.

FAQ

Q: Can I create my own custom hooks that use other custom hooks?

A: Yes! Custom hooks can call other custom hooks, which is an excellent way to compose complex functionality.

Q: Are custom hooks only for functional components?

A: Yes. Custom hooks are designed to be used within functional components or other custom hooks.

Q: How do I test custom hooks?

A: You can test custom hooks by creating test components that use your hook and then asserting that the behavior is correct. Libraries like Jest and React Testing Library are commonly used for testing.

Q: What are some alternative libraries to react-use?

A: While react-use is a great starting point, other libraries like use-immer (for immutable state updates), or libraries focused on specific functionalities like state management (e.g., Zustand, Jotai) can be helpful depending on your project needs.

Q: Can I use custom hooks with class components?

A: No, hooks are designed to be used in functional components. If you have existing class components, you’ll need to refactor them to functional components to use custom hooks.

By leveraging custom hooks and the react-use library, you can significantly streamline your Next.js development workflow. From managing local storage to fetching data and debouncing input, custom hooks offer a versatile and efficient approach to building robust and maintainable web applications. Mastering these concepts will not only improve your code quality but also enhance your overall understanding of React and Next.js, making you a more effective and productive developer.