React hooks

React hooks

January 10, 2024

Hooks are essential tools for React developers.

They allow you to easily add interactivity and transform a static website into an engaging and user-friendly experience.

But what exactly are hooks, and how can you use them effectively?

In this post, you’ll learn everything you need to know about hooks, with examples and best practices.

What Are Hooks?

Hooks are built-in functions in React that give you access to various features.

They are recognizable by their prefix, use and include useState, useEffect, useContext... and many more.

But before exploring the existing Hooks, why do we need them in the first place?

Why hooks?

React Hooks address several challenges encountered in React development.

The story goes like this:

React originally only supported Class Components.

Function Components came in React 16.8 , with the intention to simplify the creation of stateless components.

With Function Components, it takes less (boilerplate) code to achieve the same thing.

import React, { Component } from "react";

// 😒 with class component:
class HelloClassComponent extends Component {
  render() {
    return (
      <p>Hello World!</p>);
  }
}

// 💪 with function component:
// ✅ less code, more readable
const HelloFunctionComponent = () => <p>Hello World!</p>;

So Hooks were introduced for two main reasons:

  1. empowering the function components
  2. address the problems emerging from using class components

Problem #1: Reusable Stateful Logic

React originally lacked a method to easily reuse code with class components.

This lead to complex anti-patterns like the render props and higher-order components.

Hooks allow extracting logic from components to functions.

This means easier code sharing and testing.

Problem #2: Complex Component Management

Big, complex components became hard to understand due to a mix of unrelated logic in lifecycle methods.

With hooks complex logic can be broken down to smaller, reusable & combinable functions.

Problem #3: OOP vs Functional Programming

Classes in React introduce barriers to learning and can lead to confusion, especially regarding the use of this in JavaScript (Not as simple as you might think).

Hooks eliminate the need for classes, providing a more straightforward approach to React development.

Beginners also don’t need to learn Object-Oriented Programming to get started with react.

React Hooks

The must-know React Hooks are useState, useEffect, useContext, useMemoand useCallback.

useState

useState is arguably the most used hook in React.

It allows you to define a variable in a component, that can be used when rendering the ui.

Take this simple toggle as an example: We can use a state variable to achieve this effect with useState:

Off

import { useState } from "react";
import { Switch, Text } from "@mantine/core";

function Toggle() {
  const [toggled, setToggled] = useState(false);

  return (
    <Text as="label" size="2">
      <Switch checked={toggled} onClick={() => setToggled(!toggled)} /> 
      {toggled ? "On": "Off"}
    </Text>
  );
}

Explanation: We use the useState hook to define a variable named toggled that is used to render the correct icon.

useState takes the initial value as an argument (in this case true) and returns a variable and an update function to set the value.

Each time the icon is clicked, toggle's value is flipped (true→false→true).

The state is used to conditionaly render the correct icon "ToggleLeft" or "ToggleRight".

useEffect

The useEffect hook in React is used for side effects in functional components.

Here's the basic syntax:

useEffect(() => {
  // Side effect code goes here
}, [dependencies]);
// This code will run each time the dependencies change

Let's break down the components of this syntax:

  • useEffect: This is the hook provided by React to perform side effects in functional components.
  • () => { /* Side effect code */ }: This is the function that contains the code for the side effect. It will run after the component renders. The function can return a cleanup function (optional), which will be executed before the component unmounts or before the next useEffect execution.
  • [dependencies]: This is an optional array of dependencies. If provided, the effect will only re-run if any of the dependencies have changed between renders. If no dependencies are provided ([]), the effect will only run once.

This can be used to cover many use cases.

Example 1: loading data from a server

Checkout this simple example where you can see a GitHub users details:

👇Enter your Github handle

Loading...

import React, { useState, useEffect } from "react";

const GithubProfile = ({ username }) => {
  const [profileData, setProfileData] = useState(null);

  useEffect(() => {
    const fetchProfile = async () => {
      try {
        const response = await fetch(`https://api.github.com/users/${username}`);
        const data = await response.json();
        setProfileData(data);
      } catch (error) {
        console.error("Error fetching GitHub profile:", error);
      }
    };

    fetchProfile();
// depends on username. 
// the effect will run each time it changes
  }, [username]);

  return (
    <div>
      {profileData ? (
        <div>
          <h2>{profileData.login}</h2>
          <img src={profileData.avatar_url} alt={`${profileData.login}'s avatar`} />
          <p>{profileData.bio}</p>
        </div>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
};
export default GithubProfile;

useContext

The useContext hook allows you to access a React context inside a function component.

Think of context as a global state that is available to all its children, eliminating the need for "prop drilling"—the tedious process of passing props down multiple layers just to reach a deeply nested component.

Example: Theme Context

Imagine a simple app where you want to toggle between light and dark themes. Using useContext, you can manage and share this theme state easily across components:

import React, { createContext, useContext, useState } from "react";

const ThemeContext = createContext();

const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState("light");
  const toggleTheme = () => setTheme(theme === "light" ? "dark" : "light");
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

const ThemeSwitcher = () => {
  const { theme, toggleTheme } = useContext(ThemeContext);
  return (
    <button onClick={toggleTheme}>
      Switch to {theme === "light" ? "dark" : "light"} mode
    </button>
  );
};

const App = () => (
  <ThemeProvider>
    <ThemeSwitcher />
  </ThemeProvider>
);

export default App;

With useContext, any component inside ThemeProvider can access the theme state without explicitly passing it through props!

useMemo

useMemo is used to optimize performance by memoizing values—only recalculating them when their dependencies change.

Example: Avoiding Unnecessary Computations

Imagine you have a function that processes a large dataset. You don't want it to run on every render unless necessary:

import React, { useState, useMemo } from "react";

const slowFunction = (num) => {
  console.log("Running slow function...");
  return num * 2;
};

const MemoExample = () => {
  const [count, setCount] = useState(0);
  const [number, setNumber] = useState(10);

  const computedValue = useMemo(() => slowFunction(number), [number]);

  return (
    <div>
      <h2>Computed Value: {computedValue}</h2>
      <button onClick={() => setNumber(number + 1)}>Change Number</button>
      <button onClick={() => setCount(count + 1)}>
        Re-render (Count: {count})
      </button>
    </div>
  );
};

export default MemoExample;

The useMemo hook ensures that slowFunction only runs when number changes, rather than on every re-render.

useCallback

Similar to useMemo, useCallback memoizes functions to prevent unnecessary re-creations.

Example: Preventing Unnecessary Function Re-Creation

When passing functions as props to child components, React re-creates them on every render. This can cause unnecessary re-renders. useCallback helps avoid that:

import React, { useState, useCallback } from "react";

const Button = React.memo(({ handleClick }) => {
  console.log("Button re-rendered");
  return <button onClick={handleClick}>Click Me</button>;
});

const CallbackExample = () => {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <h2>Count: {count}</h2>
      <Button handleClick={increment} />
    </div>
  );
};

export default CallbackExample;

Without useCallback, increment would be recreated on every render, causing the Button component to re-render unnecessarily.

Rare Hooks

While useState, useEffect, useContext, useMemo, and useCallback are the most commonly used hooks, React provides several other hooks for specific use cases:

  • useRef – For accessing and persisting DOM elements or values across renders without causing re-renders.
  • useImperativeHandle – Customizes a ref instance when using React.forwardRef.
  • useLayoutEffect – Similar to useEffect, but runs synchronously after all DOM mutations.
  • useDebugValue – Adds labels for custom hooks in React DevTools.
  • useTransition – Helps manage UI transitions smoothly.
  • useDeferredValue – Defers rendering of a value to prevent UI lag.
  • useSyncExternalStore – For subscribing to external stores.
  • useId – Generates unique IDs useful for accessibility attributes.

What’s Next?

Hooks have revolutionized how we build React applications by making code more readable, maintainable, and reusable.

The next step is mastering custom hooks! Learn how to create your own hooks to encapsulate reusable logic and supercharge your React apps.

🚀 Check out our guide on Creating Custom Hooks in React to take your skills to the next level!