Skip to main content

Command Palette

Search for a command to run...

The 5 React Hooks you'll ever need

Updated
6 min read
The 5 React Hooks you'll ever need

In recent years, React's ecosystem has witnessed a transformation, and it's largely thanks to Hooks. With them, stateful logic has never been more reusable and React code has never looked cleaner. If you're curious about why and how, let's embark on this enlightening journey through the world of React Hooks.

Intro to React Hooks

Once upon a time, state and lifecycle features were exclusive to class components in React. That changed with Hooks. Suddenly, these capabilities were available to functional components, and the community's excitement was palpable. Beyond just offering enhanced features, Hooks championed a philosophy of cleaner and more intuitive design patterns.

The Evolution: Functional vs. Class-Based Components

Before Hooks, the React community was sharply divided between two camps: the Functional Components and the Class-Based Components.

Functional components, known for their simplicity, were mainly used for presentational needs. They had less boilerplate, were considered efficient but lacked the ability to handle state or lifecycle events.

On the other side, class components came with their robustness. They could manage state, handle lifecycle events, and support props with ease. However, they also carried the baggage of extra boilerplate and sometimes convoluted logic, especially for newcomers.

1. useState: Simplifying State Management

State management, once a daunting task requiring a constructor and this.state, was simplified by the useState hook.

import { useState } from "react";

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

  function handleClick() {
    setCount(count + 1);
  }

  return <button onClick={handleClick}>You pressed me {count} times</button>;
}

With useState, we not only have a more concise way to manage state, but it also eliminates the need to bind this to functions, further simplifying function components.

2. useEffect: Side Effects Made Easy

If useState revolutionized state, useEffect did the same for side effects. This hook can mimic componentDidMount, componentDidUpdate, and componentWillUnmount lifecycle methods, making it a powerful tool in a developer's arsenal.

2.1. Updating the Document Title

One of the simplest uses of useEffect is to update the document title based on a state change, giving users a dynamic experience.

import { useState, useEffect } from "react";

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  }, [count]); // This effect runs whenever 'count' changes
}

This code updates the browser's tab title every time the count state changes, demonstrating how useEffect can be used for side effects related to state changes.

2.2. Fetching Data with useEffect

useEffect is frequently used for data fetching, allowing functional components to retrieve data right after they're mounted.

import { useState, useEffect } from "react";

function FetchData() {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchData() {
      const response = await fetch("https://api.example.com/data");
      const result = await response.json();
      setData(result);
      setLoading(false);
    }

    fetchData();
  }, []); // Empty dependency array means this useEffect runs once after component mounts
}

3. useContext: Managing Global State with Elegance

When working on larger React applications, managing state and passing data through multiple layers of components can get cumbersome. This phenomenon, often referred to as "prop drilling," can make your codebase harder to maintain. The useContext hook in React aims to solve this problem by allowing you to create global state that can be accessed and modified from anywhere within your component tree, without having to pass props down manually.

3.1. How useContext Works

At its core, useContext is a combination of two main parts:

  1. React.createContext: This creates a new context. It returns an object with two values, Provider and Consumer. While you'd mainly use the Provider in conjunction with useContext, there are still scenarios where the Consumer might come in handy, especially in class components.

  2. useContext Hook: This allows your functional components to tap into the provided context without having to wrap the component in a Consumer.

3.2. Practical Example

Let's see useContext in action with a theme toggler for our application.

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

// 1. Create a new context
const ThemeContext = React.createContext();

function App() {
  const [theme, setTheme] = useState('light');

  // The value prop on Provider will provide these values to all children that tap into this context
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Navbar />
      <Content />
    </ThemeContext.Provider>
  );
}

function Navbar() {
  const { theme, setTheme } = useContext(ThemeContext); // Accessing our context

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

function Content() {
  const { theme } = useContext(ThemeContext); // Accessing our context

  return (
    <div style={{ background: theme === 'light' ? '#eee' : '#333', color: theme === 'light' ? '#333' : '#eee' }}>
      This is our main content.
    </div>
  );
}

In the example above, we've created a context for theme management. The Navbar component can toggle the theme, and the Content component reacts to the theme change. All of this is achieved without having to pass the theme or setTheme function manually as props, showcasing the elegance and power of useContext.

4. useReducer: Tackling Complex State Logic

State management can sometimes get complex, especially when dealing with intertwined actions that can mutate our state in various ways. This is where useReducer shines. Inspired by the Redux pattern, useReducer provides a structured way to manage more intricate state logic in our React applications.

4.1. The Core Concept

useReducer is fundamentally about using the reducer pattern within a component. A reducer is a function that determines changes to an application's state. It uses the current state and returns a new state.

Typically, with useReducer, you define an initial state and a reducer function, which specifies how the state changes in response to different actions.

4.2. Practical Example

Let's explore useReducer with a counter that can both increase and decrease its value.

import React, { useReducer } from 'react';

// Reducer function
function counterReducer(state, action) {
  switch(action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      throw new Error("Unsupported action type!");
  }
}

// Initial state
const initialState = { count: 0 };

function CounterApp() {
  const [state, dispatch] = useReducer(counterReducer, initialState);

  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
    </div>
  );
}

In the example, we define a reducer function (counterReducer) and an initial state (initialState). The CounterApp component then uses useReducer to provide state management.

The reducer listens to dispatched actions. When we click the buttons, we dispatch actions (INCREMENT or DECREMENT) to the reducer. The reducer, in turn, decides how to update the state based on these actions.

What's more, the power of useReducer becomes truly evident in more complex state structures, where multiple intertwined actions can influence state changes.

5. useMemo: Boosting Performance

Performance optimization is crucial. With useMemo, developers can avoid unnecessary re-renders by memorizing computed values, leading to efficient resource utilization.

import { useMemo } from "react";

const expensiveValue = useMemo(() => {
  // Some expensive computation here
}, [dependencies]);

The Power of Custom Hooks

While React provides an impressive array of built-in hooks, it also grants developers the liberty to craft their custom hooks. These hooks encapsulate logic, making it shareable across components.

For instance, a usePost hook could simplify data fetching, abstracting the logic and making it reusable across your application.

Embracing the Future with Hooks

With Hooks, the future beckons. They promote:

  • A clear shift towards functional components.

  • Enhanced code reusability.

  • Optimized performance and leaner code structures.

  • Intuitive design patterns and better separation of concerns.

React Hooks are more than just a feature; they're a testament to React's commitment to innovation, always ensuring developers have the best tools at hand. As we continue to build, let's harness the power of Hooks to create intuitive, maintainable, and scalable applications.

Interested in a visual walkthrough? Click here to watch my tech talk on React Hooks.