DEV Community

Cover image for React Hooks : A Comprehensive Beginner’s Guide
John Kamau
John Kamau

Posted on

React Hooks : A Comprehensive Beginner’s Guide

Introduction

React is an open-source JavaScript-based user interface library, highly popular for web and mobile app development. It adheres to the principle of component-based architecture, where a component serves as an isolated and reusable piece of code. Components in React can be classified into two types: class components and functional components.

Before version 16.8, React developers could only manage state and other features using class components. However, with the introduction of version 16.8, React introduced a new pattern known as Hooks. Hooks enable developers to utilize state and other React features within functional components, empowering them to apply functional programming principles in React.

Throughout this article, we will delve into the fundamentals of React Hooks, exploring core concepts such as useState, useEffect, useContext, and the art of crafting custom hooks. By the end, you'll not only grasp the syntax and usage of Hooks but also gain a deeper appreciation for their role in streamlining React development.

Understanding React Hooks

React Hooks are functions that enable functional components to “hook into” React features like state and life-cycle methods. They provide a more flexible and straightforward way to manage component logic, allowing developers to encapsulate behavior within functional components without using the class syntax.

Before the introduction of Hooks, managing state and side effects in React components was primarily done using class components. However, class components come with their own set of complexities, such as handling this binding, and this lead to verbose code. Hooks were introduced to fix these problems and give an easier way to handle component logic.

React Hooks offer several advantages over class components. Here are some of the key advantages:

  1. Simplified Syntax: Hooks offer cleaner and more concise code compared to class components.
  2. Reusability: Hooks enable the creation of custom reusable logic, enhancing modularity.
  3. Improved Readability: Co-locating logic with components improves code readability and reduces cognitive load.
  4. No this Keyword: Hooks eliminate the need for this, reducing confusion and potential bugs.
  5. Better Performance Optimization: Hooks enable React to optimize functional components more effectively.
  6. Easier Testing: Functional components with Hooks are easier to test compared to class components.
  7. Support for Functional Programming: Hooks encourage functional programming principles, leading to more predictable and maintainable code.

A Practical View

Now that we’ve discussed the two methods for creating React components — using functions and classes — let’s start by creating a simple class component. This component will display a counter with a button to increase the count. Afterward, we’ll convert it to a functional component using hooks and observe the differences.

Component without Hook: Class Component

import React, { Component } from 'react';

class CounterClass extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
    this.incrementCount = this.incrementCount.bind(this);
  }

  incrementCount() {
    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  }

  render() {
    return (
      <div>
        <h2>Counter (Class Component)</h2>
        <p>Count: {this.state.count}</p>
        <button onClick={this.incrementCount}>Increment</button>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Components with Hook:Functional Component:

import React, { useState } from 'react';

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

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

  return (
    <div>
      <h2>Counter (Function Component)</h2>
      <p>Count: {count}</p>
      <button onClick={incrementCount}>Increment</button>
    </div>
  );
}

export default CounterFunction;
Enter fullscreen mode Exit fullscreen mode

Both the class component and the functional component with Hooks achieve the same functionality — they both render a counter with a button to increment the count. You can experiment with both codes in practice.

In this practical comparison, we can see how the function component with Hooks achieves the same functionality as the class component but with less boilerplate code. The Hook’s version offers a more concise and readable implementation, demonstrating the simplicity and effectiveness of React Hooks.

Core React Hooks

When developing React applications, understanding the core React Hooks is paramount. These hooks, including useState, useEffect, and useContext, form the foundation for managing state and side effects within functional components. Each hook serves a specific purpose, offering developers powerful tools to streamline their development workflow and create robust, efficient applications.

In this section, we will explore each of these core React Hooks in detail, diving into their syntax, usage, and practical applications.

useState Hook

The useState hook is a fundamental tool in React for managing state within functional components. Prior to the introduction of hooks, state management was primarily handled in class components. However, with the useState hook, functional components gain the ability to manage state in a straightforward and concise manner.

The useState hook is used by importing it from the React library. It is then invoked within the functional component, typically during initialization. The hook returns an array containing the current state value and a function to update that state.

import React, { useState } from "react";

function FormComponent() {
  // Declare state variables for each input field
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [email, setEmail] = useState("");
  const [formSubmitted, setFormSubmitted] = useState(false);

  // Event handler for form submission
  const handleSubmit = (e) => {
    e.preventDefault();
    // Set formSubmitted state to true
    setFormSubmitted(true);
    // Do something with the form data, e.g., submit to a server
    console.log("Form submitted:", { firstName, lastName, email });
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="firstName">First Name:</label>
          <input
            type="text"
            id="firstName"
            name="firstName"
            value={firstName}
            onChange={(e) => setFirstName(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="lastName">Last Name:</label>
          <input
            type="text"
            id="lastName"
            name="lastName"
            value={lastName}
            onChange={(e) => setLastName(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="email">Email:</label>
          <input
            type="email"
            id="email"
            name="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <button type="submit">Submit</button>
      </form>
      {formSubmitted && (
        <div>
          <h2>Form Submitted States:</h2>
          <p>First Name: {firstName}</p>
          <p>Last Name: {lastName}</p>
          <p>Email: {email}</p>
        </div>
      )}
    </div>
  );
}

export default FormComponent;
Enter fullscreen mode Exit fullscreen mode

In this example, the useState hook is used to manage both the state of the form input fields (firstName, lastName, and email) and the state to track the form submission status (formSubmitted). Each input field is associated with its own state variable, enabling dynamic updates through the onChange event handler. Upon form submission, the formSubmitted state is toggled to true, triggering the display of the submitted form states.

useEffect Hook

The useEffect hook in React allows developers to perform side effects in functional components. These side effects can include fetching data from an API, subscribing to events, updating the DOM, and more.

useEffect is called after every render of the component, and it enables React developers to encapsulate logic that needs to be executed in response to certain conditions, such as component mount, unmount, or updates to specific props or state variables. This hook takes two arguments: a function containing the side effect logic, and an optional array of dependencies that specify when the effect should be re-executed.

useEffect with No Dependencies

When the useEffect hook is used without specifying any dependencies, it runs after the initial render and on every subsequent re-render.

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

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

  // Effect without dependencies: runs after initial render and on every re-render
  useEffect(() => {
    console.log('useEffect has been triggered')
    // Update document title with the current count
    document.title = `Clicked ${count} times`;
  });

  return (
    <div>
      <p>Click the button to increase count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>
    </div>
  );
}

export default CounterComponent;
Enter fullscreen mode Exit fullscreen mode

The useEffect function updates the document title with the current count value, ensuring it reflects the number of times the button has been clicked. When the button is clicked, the setCount function updates the state, triggering a re-render of the component and the execution of useEffect, which updates the document title again.

This component demonstrates the use of the useEffect hook to perform side effects (updating the document title) that should occur after every render, without depending on any specific variables or state values.

useEffect with Empty Dependency Array

The useEffect hook with an empty dependency array ([]) is used to perform side effects that should only occur once, after the initial render of the component. This means the effect runs only after the component mounts, and it doesn't depend on any variables or state.

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

function TimerComponent() {
  const [timer, setTimer] = useState(0);

  useEffect(() => {
    // Setup subscription to a timer when compnent mounts
    const intervalId = setInterval(() => {
      setTimer(prevTimer => prevTimer + 1);
    }, 1000);

    // Cleanup function: Clear the subscription when component unmounts
    return () => {
      clearInterval(intervalId);
    };
  }, []);

  return (
    <div>
      <p>Timer: {timer}</p>
    </div>
  );
}

export default TimerComponent;
Enter fullscreen mode Exit fullscreen mode

In this component, we utilize the useState hook to create a timer state variable for tracking elapsed time in secs. Within the useEffect hook, we establish a timer subscription using setInterval, incrementing the timer value every second. To prevent memory leaks, a cleanup function utilizing clearInterval is returned to clear the subscription upon component unmount.

With an empty dependency array ([]), the effect runs only once after the initial render, ensuring proper setup of the timer subscription. Finally, the component displays the current timer value in a paragraph element.

useEffect With Dependencies

When using the useEffect hook with dependencies, the effect will run after the initial render and then re-run whenever any of the dependencies change. This is useful for scenarios where you want to perform certain actions based on changes to specific variables or state values.

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

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

  useEffect(() => {
    console.log('Count has changed:', count);
  }, [count]); // Dependency array with 'count'

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>
    </div>
  );
}

export default CounterComponent;
Enter fullscreen mode Exit fullscreen mode

In this component, we initialize a counter variable count using useState. The useEffect hook is configured with a dependency array [count], so it runs whenever count changes. Inside the effect, we log a message to the console indicating the change in count. Clicking the button updates count, triggering the effect again. If there were multiple dependencies specified in the array, the effect would run whenever any of them change. This allows React developers to perform actions in response to changes in specific variables or state values.

In summary, the useEffect hook in React is exclusively used within function components and custom hooks to manage side effects. It allows developers to perform actions in response to changes in variables, state, or component lifecycle events. By specifying dependencies, developers can control when the effect runs, ensuring efficient execution. Cleanup functions can also be utilized to manage resource cleanup or subscription unsubscription.

useContext Hook

useContext is a React hook that offers a means to share data (context) across multiple components without explicitly passing it through props, thereby addressing the issue of prop drilling. It is part of the React Context API, which is integrated into the React library.

By utilizing useContext, components can consume context values provided by a Provider higher up in the component tree, eliminating the need to pass data through intermediate components. This approach streamlines code and improves scalability.

In a typical React project, you would organize your code into different files and folders. Here’s how the project might be structured:

src/
|-- components/
|   |-- ThemedComponent.jsx      // Contains ThemedComponent code
|-- contexts/
|   |-- ThemeContext.jsx           // Contains ThemeContext code
|-- App.jsx         
|-- main.jsx 
Enter fullscreen mode Exit fullscreen mode
//ThemedComponent.jsx
import React, { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';

const ThemedComponent = () => {
  const theme = useContext(ThemeContext);

  return (
    <div>
      <p>Current Theme: {theme}</p>
    </div>
  );
};

export default ThemedComponent;
Enter fullscreen mode Exit fullscreen mode
//ThemeContext.jsx
import React, { createContext } from 'react';

export const ThemeContext = createContext();

export const ThemeProvider = ({ children }) => {
  const theme = 'light'; // For simplicity, hardcoding the theme here
  return (
    <ThemeContext.Provider value={theme}>
      {children}
    </ThemeContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode
//App.jsx
import React from 'react';
import ThemedComponent from './components/ThemedComponent';
import { ThemeProvider } from './contexts/ThemeContext';

const App = () => {
  return (
    <ThemeProvider>
      <ThemedComponent />
    </ThemeProvider>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

In the provided code, the useContext hook is used within the ThemedComponent functional component to directly consume the theme context from the ThemeContext.Provider. This approach simplifies data access by eliminating the need for prop drilling or callback functions

By utilizing useContext, the component becomes independent of the specific location where the context is defined, enhancing its reusability and maintainability. Moreover, useContext facilitates cleaner component code, as it abstracts away the complexities of context usage, leading to more comprehensible components.

useReducer Hook

This hook is used for managing complex state logic in a more organized and predictable way. It is especially beneficial when dealing with state objects that have multiple properties or when state transitions depend on the previous state

The useReducer hook in React takes two arguments: a reducer function and an initial state value. The reducer function specifies how the state should be updated based on the action dispatched to it, taking the current state and action as arguments and returning the new state. The initial state value represents the starting state of the component and can be a simple value, object, or array.

import React, { useReducer } from 'react';

// Define the reducer function
const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
};

const Counter = () => {
  // Initialize state using useReducer
  const [state, dispatch] = useReducer(reducer, { count: 0 });

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

export default Counter;
Enter fullscreen mode Exit fullscreen mode

In this example, we utilize the useReducer hook to manage state in a React component. We start by defining a reducer function that handles state updates based on dispatched actions. Next, we initialize the state using useReducer, passing the reducer function and initial state as arguments. This hook returns an array containing the current state and a dispatch function.

Within the component, we render the current count value from the state object and two buttons that dispatch INCREMENT and DECREMENT actions. When an action is dispatched, the reducer function updates the state accordingly, triggering a re-render of the component with the updated state.

useCallback Hook

useCallback hook in React is used to memoize functions, optimizing performance by preventing unnecessary re-renders caused by function recreations. When a function is wrapped with useCallback, React will return the same memoized function reference on subsequent renders as long as the dependencies listed in the dependency array remain unchanged.

The useCallback hook takes two arguments: the callback function and an array of dependencies. If any of the dependencies in the array change, the callback function is re-created; otherwise, the cached version of the function is returned.

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

//CounterButton.jsx
const CounterButton = ({ onClick }) => {
  console.log('Rendering CounterButton');
  return <button onClick={onClick}>Click Me!</button>;
};

//App.jsx
const App = () => {
  const [count, setCount] = useState(0);

  // Define a memoized callback function using useCallback
  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []); // No dependencies, the function is created only once

  return (
    <div>
      <h1>Counter: {count}</h1>
      <CounterButton onClick={handleClick} />
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

In this example, we utilize the useCallback hook to create a memoized callback function named handleClick. This function increments the count state by 1 when invoked. By specifying an empty dependency array [], we ensure that handleClick is memoized and created only once, returning the same function reference on subsequent renders. This optimization prevents unnecessary re-renders of the CounterButton component, as it no longer reacts to changes in the handleClick function reference. As a result, the application's performance is improved by avoiding unnecessary component re-renders.

useMemo hook

useMemo hook in React is used to memoize the result of a computation, optimizing performance by caching expensive calculations and returning the cached value when the dependencies remain unchanged.

It takes a function as its first argument, which computes the value to be memoized, and an array of dependencies as its second argument. When any of the dependencies change, the memoized value is recalculated.

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

const fibonacci = n => {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
};

const App = () => {
  const [number, setNumber] = useState(20);
  const [otherState, setOtherState] = useState(false);

  // Memoize the result of the fibonacci calculation
  const fibResult = useMemo(() => fibonacci(number), [number]);

  return (
    <div>
      <h1>Calculate Fibonacci Number</h1>
      <p>Number: {number}</p>
      <p>Fibonacci Result: {fibResult}</p>
      <p>Other State: {otherState.toString()}</p>
      <button onClick={() => setNumber(prevNumber => prevNumber + 1)}>
        Increment Number
      </button>
      <button onClick={() => setOtherState(prevState => !prevState)}>
        Toggle State
      </button>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

In this example, we utilize the useMemo hook to optimize the performance of a Fibonacci calculation within a React functional component. The component manages a number state representing the input to the Fibonacci calculation using the useState hook. By employing useMemo, we memoize the result of the Fibonacci calculation based on the number state. This ensures that the expensive computation is only performed when the number state changes, preventing unnecessary recalculations during component re-renders.

When we click on the “Toggle State” button, the App component re-renders due to the state update of otherState. However, despite the component re-rendering, the Fibonacci calculation is not recomputed unnecessarily. This is because useMemo memoizes the result of the Fibonacci calculation based on the number state, which remains unchanged when toggling otherState.

useRef Hook

useRef hook in React is used to create a mutable reference that persists across re-renders of the component. Unlike useState, changes to a useRef value do not trigger a re-render of the component. useRef is commonly used to access and interact with DOM elements or to persist values across renders without triggering re-renders.

import React, { useRef } from 'react';

const App = () => {
  const inputRef = useRef(null);

  const handleFocus = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleFocus}>Focus Input</button>
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

In this example, we utilize the useRef hook to create a reference named inputRef, which is then assigned to the ref attribute of an input element. When the "Focus Input" button is clicked, invoking the handleFocus function, the input element gains focus via inputRef.current.focus(). Despite this focus action, the component does not re-render because useRef does not trigger re-renders.

Other React Hooks

useLayoutEffect Hook: is similar to useEffect, but it’s executed synchronously after all DOM mutations have been applied but before the browser paints the changes on the screen. This makes it suitable for tasks that require measurements or DOM manipulations and need to be performed before the user sees the updates.

useDebugValue hook: is a React hook that is used to provide additional debug information about custom hooks in React DevTools. It allows you to display custom labels and values for custom hooks in the React DevTools inspector, making it easier to debug and understand how custom hooks are being used in your application.

useImperativeHandle hook: is used to customize the instance value that is exposed by a parent component when using the forwardRef feature in React. It allows child components to expose specific methods or properties to their parent components, providing a way to interact with the child component's instance from its parent.

Custom Hooks

Custom hooks in React are JavaScript functions that utilize one or more built-in React hooks to encapsulate and share reusable logic across components. They allow developers to abstract complex logic into reusable pieces, promoting code reusability and composability.

They must follow a naming convention where their names start with the prefix “use”. This convention is enforced by React’s ESLint rules to help developers distinguish between regular functions and custom hooks.

Let’s create a useFetch custom hook that enables us to fetch data from a specified URL and handle loading states and errors.

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 () => {
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('Failed to fetch data');
        }
        const jsonData = await response.json();
        setData(jsonData);
        setLoading(false);
      } catch (error) {
        setError(error);
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

This custom hook useFetch takes a URL as its parameter and returns an object containing the fetched data (data), a boolean flag indicating whether the data is still loading (loading), and any error that may occur during the fetch (error).

You can use this custom hook in your components like this:

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

function MyComponent() {
  const { data, loading, error } = useFetch('https://api.example.com/data');

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

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

  return (
    <div>
      <h1>Data:</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

export default MyComponent;
Enter fullscreen mode Exit fullscreen mode

With this custom hook, you can easily fetch data from any API endpoint in your React components while handling loading states and errors in a clean and reusable manner.

Summary

React hooks have ushered in a new era of efficiency and effectiveness in web development. With hooks like useReducer for state management and useMemo and useCallback for performance optimization, developers can unlock new levels of productivity and innovation.

They are not limited to experts; developers of all skill levels can leverage these tools. Whether you’re a seasoned professional or new to React, integrating these hooks into your projects can simplify code, improve performance, and elevate user experiences.

In summary, React hooks are indispensable for React developers aiming to build faster and more responsive web applications.

Top comments (0)