3 Essential React Hooks to Supercharge Your Next Project

As a full-stack developer, I‘m always looking for ways to write cleaner, more efficient code. And when it comes to React, custom hooks are one of the most powerful tools in my arsenal.

Hooks let you extract and reuse stateful logic across your components, keeping your code DRY and your components focused on their UI. And while React comes with a handful of built-in hooks, the real magic happens when you start building your own.

In this post, I‘ll share three custom hooks that have become essential in my React projects. These hooks have saved me countless hours of development time and made my code more readable and maintainable. I‘ll explain how each one works, provide real-world examples, and share some tips and best practices I‘ve learned along the way.

1. useFetch: Simplify API calls and state management

One of the most common tasks in any web app is fetching data from an API. And while the browser‘s built-in fetch function gets the job done, using it directly in your components quickly leads to messy, hard-to-maintain code.

That‘s where the useFetch hook comes in. This hook abstracts away the details of making HTTP requests and managing loading and error states, letting you focus on working with your data.

Here‘s a basic example of how you might use useFetch to fetch a list of blog posts:

import useFetch from ‘react-fetch-hook‘;

function BlogPosts() {
  const { isLoading, error, data } = useFetch(‘https://api.example.com/posts‘);

  if (isLoading) return <div>Loading...</div>; 
  if (error) return <div>Error: {error.message}</div>;

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

Under the hood, useFetch uses the useEffect and useState hooks to manage the request lifecycle. When the component mounts, it makes a GET request to the specified URL and updates the data, isLoading, and error states accordingly.

But useFetch doesn‘t just handle GET requests. You can also use it to make POST, PUT, DELETE, and other types of requests by passing an options object as the second argument:

const { data } = useFetch(‘/api/todos‘, {
  method: ‘POST‘,
  body: JSON.stringify({ title: ‘New todo‘ }),
  headers: { ‘Content-Type‘: ‘application/json‘ },
});

This flexibility makes useFetch a great fit for a wide range of API interactions. And because it‘s a custom hook, you can easily tweak its behavior to fit your specific needs.

For example, you might want to add a refetch function that lets you manually trigger a new request:

function useFetch(url, options) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);
      try {
        const res = await fetch(url, options);
        const json = await res.json();
        setData(json);
        setError(null);
      } catch (error) {
        setError(error);
        setData(null);
      }
      setIsLoading(false);
    };

    fetchData();
  }, [url, options]);

  const refetch = () => {
    fetchData();
  };

  return { data, error, isLoading, refetch };
}

Or you might want to add caching to avoid making unnecessary requests for data that hasn‘t changed. Libraries like SWR and React Query provide this functionality out of the box, but you can also build it into your own custom hook.

The beauty of useFetch is that it lets you encapsulate all of this complexity in a single, reusable function. And because it returns a simple data object, it‘s easy to integrate into your components without coupling them to a specific data fetching library.

2. useForm: Painless form state management and validation

Forms are a critical part of many web apps, but managing form state in React can quickly get tedious and error-prone. You have to keep track of the values of each input, handle changes and submissions, and validate user input – all while keeping your JSX readable.

The useForm hook simplifies this process by providing a set of utilities for managing form state and validation. Here‘s a simple example of how you might use it to build a login form:

import { useForm } from ‘react-hook-form‘;

function LoginForm() {
  const { register, handleSubmit, errors } = useForm();

  function onSubmit(data) {
    console.log(data);
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        name="email"
        ref={register({ required: true, pattern: /^\S+@\S+$/i })}
      />
      {errors.email && <span>This field is required</span>}

      <input
        name="password"
        type="password"
        ref={register({ required: true, minLength: 6 })}
      />
      {errors.password && <span>Password must be at least 6 characters</span>}

      <button type="submit">Log in</button>
    </form>
  );
}

Here‘s how it works:

  1. You call useForm in your component to get a set of helper functions and state values
  2. You pass the register function to each input, along with any validation rules
  3. You pass the handleSubmit function to your form‘s onSubmit prop, along with a callback to handle the form data
  4. You use the errors object to display validation errors next to each input

This might not seem like much, but it‘s a huge improvement over managing form state manually. No more creating a separate state variable for each input, writing change handlers, or manually checking for errors.

But the real power of useForm comes when you start building more complex forms. For example, you can easily add support for different input types:

<select name="color" ref={register({ required: true })}>
  <option value="">Select a color</option>
  <option value="red">Red</option>
  <option value="green">Green</option>
  <option value="blue">Blue</option>
</select>

<input
  name="age"
  type="number"
  ref={register({ min: 18, max: 99 })}
/>

<input
  name="terms"
  type="checkbox"
  ref={register({ required: true })}
/>

You can also define your validation rules as functions for more complex logic:

const { register, errors } = useForm({
  validate: {
    password: value => value.length >= 6 || ‘Password is too short‘,
    confirmPassword: (value, { password }) => 
      value === password || ‘Passwords must match‘
  }
});

And if you need to handle more advanced use cases like conditional fields or multi-step forms, useForm has you covered with its watch and getValues functions.

But perhaps the best part about useForm is how seamlessly it integrates with other libraries. For example, you can easily use it with UI libraries like Material UI or Bootstrap to build accessible, mobile-friendly forms with minimal boilerplate.

import { TextField, Button } from ‘@material-ui/core‘;

function LoginForm() {
  const { register, handleSubmit } = useForm();

  function onSubmit(data) {
    console.log(data);
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <TextField
        name="email"
        label="Email"
        inputRef={register({ required: true })}
      />
      <TextField
        name="password"
        label="Password"
        type="password"
        inputRef={register({ required: true })}
      />
      <Button type="submit" variant="contained" color="primary">
        Log in
      </Button>
    </form>
  );
}

And if you‘re using TypeScript, useForm has excellent type definitions out of the box, so you can get type safety and autocompletion without any extra setup.

In my experience, useForm is one of the most impactful hooks you can add to your React toolkit. It eliminates a huge amount of boilerplate and makes form handling a breeze, while still being flexible enough to handle complex use cases. If you‘re building any kind of form in React, it‘s definitely worth checking out.

3. useLocalStorage: Persist state effortlessly

Sometimes you need to persist data between page loads or sessions, but don‘t want to deal with the hassle of setting up a backend or database. That‘s where the useLocalStorage hook comes in.

As the name suggests, useLocalStorage lets you store and retrieve data from the browser‘s local storage with the same API as the useState hook. Here‘s a basic example:

import { useLocalStorage } from ‘react-use‘;

function DarkModeToggle() {
  const [isDarkMode, setIsDarkMode] = useLocalStorage(‘darkMode‘, false);

  return (
    <button onClick={() => setIsDarkMode(prev => !prev)}>
      {isDarkMode ? ‘Light mode‘ : ‘Dark mode‘}
    </button>
  );
}

In this example, the isDarkMode state is initialized from local storage (or false if no value is set), and any changes to it are automatically persisted back to storage. So if the user toggles dark mode and then refreshes the page, their preference will be remembered.

Under the hood, useLocalStorage uses the Web Storage API to read and write data as JSON strings. This means you can store any type of data that can be serialized as JSON, including numbers, booleans, arrays, and objects.

But what happens if the user‘s browser doesn‘t support local storage, or if you‘re rendering your app on the server? That‘s where the beauty of useLocalStorage shines. If local storage isn‘t available, it will gracefully fall back to an in-memory store that behaves just like the real thing.

This makes useLocalStorage a great choice for storing user preferences, cache data, or any other small pieces of data that you want to persist across sessions. And because it‘s a custom hook, you can easily modify it to use a different storage backend or add encryption for sensitive data.

Here are a few more examples of how you might use useLocalStorage in your app:

// Store a user‘s preferred language
const [language, setLanguage] = useLocalStorage(‘language‘, ‘en‘);

// Cache expensive calculations or API responses
const [cache, setCache] = useLocalStorage(‘dataCache‘, {});

// Store a user‘s session token for authentication
const [token, setToken] = useLocalStorage(‘authToken‘, null);

Of course, local storage isn‘t a silver bullet. It has a limited capacity (usually around 5MB), and it‘s not a secure way to store sensitive data like passwords or credit card numbers. And in some cases, using a real database or server-side session storage might be a better fit for your app‘s needs.

But for many common use cases, useLocalStorage is a simple and effective way to add persistence to your React app without any backend code. And by encapsulating the details of serialization and storage access, it makes working with local storage as easy as using regular component state.

Conclusion

Custom hooks are a game-changer for React development. By extracting and reusing stateful logic, they let you write cleaner, more maintainable code and focus on what matters most – building great user interfaces.

In this post, I‘ve shared three hooks that have become indispensable in my own React projects:

  • useFetch for simplifying API requests and state management
  • useForm for painless form handling and validation
  • useLocalStorage for persisting state effortlessly

But this is just the tip of the iceberg. The React ecosystem is full of amazing custom hooks for everything from animations and data visualization to accessibility and performance monitoring. And if you can‘t find a hook that fits your needs, you can always build your own!

As a full-stack developer, I‘m always looking for ways to streamline my development process and write better code. And in my experience, custom hooks are one of the most powerful tools for doing just that. By encapsulating complex logic in reusable functions, they let you focus on the unique parts of your app and avoid reinventing the wheel.

So the next time you find yourself writing the same stateful logic over and over, consider extracting it into a custom hook. Your future self (and your teammates) will thank you.

And if you‘re new to custom hooks, don‘t be afraid to start small. Even a simple hook like useLocalStorage can make a big difference in your code‘s readability and maintainability. As you gain more experience, you‘ll start to see opportunities for custom hooks everywhere.

Happy coding! And if you have any favorite custom hooks of your own, I‘d love to hear about them in the comments.

Similar Posts