DEV Community

Cover image for React Hooks
Kuk Hoon Ryou
Kuk Hoon Ryou

Posted on

React Hooks

In this post, we'll explore the concept of Hooks in React. Hooks are an essential concept in React, and they are crucial knowledge to master if you're preparing for an interview. We will delve into what Hooks are, how they are structured, and we will include examples to see how they are used.

React Hooks are special functions used in functional components to "hook into" state and other React features. Previously, state and lifecycle methods were only available in class components, but with the introduction of hooks, these features can now be utilized in functional components as well. Hooks significantly expand the potential of functional components, allowing for state management, side-effect handling, and context usage, among other React functionalities. The introduction of hooks makes React apps more concise and readable, and facilitates code reuse and logic separation.
Hooks were introduced in React version 16.8 and include several varieties.

  1. useState

useState is one of React's fundamental Hooks, allowing for state management in functional components. This Hook enables storing and updating state within a component without the need for class components.

const [state, setState] = useState(initialState);

  • state: Represents the current value of the state.

  • setState: This is the function used to update the state. When a new state value is passed to this function, the component re-renders, and the state is updated.

  • initialState: This is the initial value of the state. The state value can be of any type, such as string, number, object, array, etc.

Below is an example of a simple counter component. This component uses useState to manage the current count as its state.

import React, { useState } from 'react';

function Counter() {
  // Use useState to create the count state with an initial value of 0.
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Current Count: {count}</p>
      {/* When the button is clicked, call setCount to update the count state. */}
      <button onClick={() => setCount(count + 1)}>Increase</button>
      <button onClick={() => setCount(count - 1)}>Decrease</button>
    </div>
  );
}

export default Counter;
Enter fullscreen mode Exit fullscreen mode

In this example, useState(0) initializes a state named count with the initial value of 0. The setCount function is used to update count. When the user clicks the "Increase" or "Decrease" button, setCount is called to change the count value, causing the component to re-render and display the new count value on the screen.

  1. useEffect

Using useEffect allows for easy management of side effects in functional components and effective handling of various side effects.It is similar to combining the functionalities of lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount in class components. With useEffect, you can perform tasks such as fetching data, setting up subscriptions, and manually manipulating the DOM.

useEffect(() => {
// Code to execute for the side effect
return () => {
// Clean-up code (optional)
};
}, [dependency array]);
Enter fullscreen mode Exit fullscreen mode
  • The first argument is the side effect function to execute. This function runs after the component renders. Another function that can be returned from the side effect function is called when the component unmounts or before the next side effect runs. This is mainly used for clean-up tasks like removing event listeners or clearing timers.

  • The second argument is the dependency array. The side effect function will re-run whenever the values specified in the array change. If this array is left empty, the side effect will only run once when the component mounts.

Below is an example of a component that uses useEffect to asynchronously fetch data and manage loading and error states.

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

function DataFetcher() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('https://some-api.com/data') // Example URL
      .then(response => {
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        return response.json();
      })
      .then(data => {
        setData(data);
        setLoading(false);
      })
      .catch(error => {
        setError(error);
        setLoading(false);
      });
  }, []); // Dependency array is empty, so the effect runs only once when the component mounts

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{JSON.stringify(data)}</div>;
}

export default DataFetcher;

Enter fullscreen mode Exit fullscreen mode

In this example, useEffect is used to asynchronously fetch data when the component first renders. Since the dependency array is empty, this side effect runs only once when the component mounts. If data is successfully fetched, setData is used to update the state and setLoading is set to false to end the loading state. If an error occurs, setError is used to set the error state and the loading state is ended.

  1. useContext

Using useContext allows for easy reading and use of Context values without the need to pass props explicitly through the component tree, efficiently managing the use of global data. The Context API provides a way to share data globally across a React app, which is especially useful for handling global settings like themes, user preferences, and authentication details. By using the useContext Hook, you can directly read the current value of a context in functional components without having to use contextType in class components or Consumer components.

const value = useContext(MyContext);

  • MyContext: A Context object created by React's createContext function.

  • value: Represents the current context value stored in the Context object.

Below is an example that demonstrates how to use useContext to easily access and change the theme (dark or light) across the application.

  • First, create the Context.
import React, { createContext, useState } from 'react';

// Creating Context
const ThemeContext = createContext();

// ThemeProvider component provides the value for the Context.
export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light"); // Set initial theme

  // Function to toggle the theme
  function toggleTheme() {
    setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light"));
  }

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • Use useContext in a functional component to use the current theme.
import React, { useContext } from 'react';
import { ThemeContext } from './ThemeProvider'; // Import ThemeProvider

function ThemeToggleButton() {
  // Use useContext to get the current theme and the theme toggle function.
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <button onClick={toggleTheme}>
      Current Theme: {theme} (Click to change theme)
    </button>
  );
}

export default ThemeToggleButton;
Enter fullscreen mode Exit fullscreen mode

In this example, the ThemeProvider component uses ThemeContext.Provider to provide its child components with the current theme value and a toggleTheme function to change the theme. The ThemeToggleButton component accesses these values directly using useContext(ThemeContext) and can toggle the theme with each button click.

  1. useReducer

useReducer is preferred over useState for managing complex state logic within components. useReducer allows for the separation of state update logic from the component, improving code readability and maintainability. It is particularly useful for complex state update logic or managing large states with multiple sub-values.It is particularly useful when the state is composed of multiple sub-values, or when the next state depends on the previous one. Inspired by Redux, useReducer allows for the placement of state update logic outside of the component.

const [state, dispatch] = useReducer(reducer, initialState);

  • reducer: A function of the form (state, action) => newState that contains logic to transform the current state into the next state based on the received action.

  • initialState: The initial state for the reducer.

  • state: The current state value.

  • dispatch: A function that triggers actions, passing them to the reducer function to update the state.

Below is an example showing a simple counter application implemented using useReducer.

First, define the reducer function and the initial state.

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();
  }
}

const initialState = { count: 0 };
Enter fullscreen mode Exit fullscreen mode

Implement the counter component using useReducer.

import React, { useReducer } from 'react';

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

  return (
    <div>
      <p>Current Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increase</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrease</button>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

In this example, counterReducer takes the current state and an action object, returning a new state based on the action type. useReducer is used to obtain the state and dispatch function. The dispatch function is used to trigger actions defined in counterReducer, which updates the state.

  1. useCallback

useCallback is one of React's Hooks that returns a memoized callback function. This Hook is primarily used to prevent unnecessary re-renderings. Each time a functional component re-renders, a new instance of the function is created, which can lead to unnecessary re-rendering of child components when functions are passed as props. By using useCallback, a new instance of the function is only created when specific dependencies change, minimizing this issue. Using useCallback can reduce unnecessary re-renderings, especially in situations where performance optimization is required. However, it's not necessary to use useCallback on every function, and it should be used only when there are actual performance concerns.

const memoizedCallback = useCallback(() => {
  // The content of the function you want to execute
}, [dependency array]);
Enter fullscreen mode Exit fullscreen mode
  • The first argument is the callback function you want to memoize.

  • The second argument is the dependency array, and the callback function is recreated only when the values in this array change.

Below is an example showing how to use useCallback to memoize a function that responds to user clicks.

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

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

  const increment = useCallback(() => {
    setCount(count + 1);
  }, [count]); // The 'increment' function is recreated only when 'count' changes.

  const decrement = useCallback(() => {
    setCount(count - 1);
  }, [count]); // The 'decrement' function is recreated only when 'count' changes.

  return (
    <div>
      <p>Current Count: {count}</p>
      <button onClick={increment}>Increase</button>
      <button onClick={decrement}>Decrease</button>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

In this example, the increment and decrement functions depend on the count state and are recreated only when count changes. If these functions are passed as props to child components, the child components will not re-render unnecessarily due to the parent component's re-rendering unless the count state changes.

  1. useMemo

useMemo is one of React's Hooks that returns a memoized value. This Hook allows for the storage of the results of expensive computations in memory, preventing the need to repeatedly execute the same operations and thus optimizing performance. useMemo will only recompute the operation when the values in the dependency array change. Utilizing useMemo can minimize unnecessary operations that impact performance. However, it's not necessary to use useMemo for every value, and it should be used only when there is a real need for performance improvement.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

  • The first argument is a function that generates the value to be memoized.

  • The second argument is the dependency array, where the value is recalculated only when the values in the array change.

Below is an example demonstrating how to use useMemo to memoize the result of an expensive computation.

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

function ExpensiveComponent() {
  const [count, setCount] = useState(0);
  const [inputValue, setInputValue] = useState('');

  // A function that performs an expensive calculation
  const expensiveCalculation = (num) => {
    console.log('Calculating...');
    for (let i = 0; i < 1000000000; i++) {} // An example of an operation that takes a lot of time
    return num * 2;
  };

  // Using useMemo to memoize the result of expensiveCalculation
  const memoizedValue = useMemo(() => expensiveCalculation(count), [count]);

  return (
    <div>
      <p>{memoizedValue}</p>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>
      <input value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, the expensiveCalculation function performs a very costly computation. By using useMemo, the function is executed only when the count value changes, preventing unnecessary recalculations when other states unrelated to count, such as updating inputValue, occur.

  1. useRef

useRef is one of React's Hooks with two main uses: it creates a reference (ref) to access a DOM element directly or acts as a generic container to persist values between renders. useRef returns an object with a .current property initialized with the passed argument, which can change over the component's lifecycle without triggering a re-render.

  • Accessing DOM Elements

When using useRef to access DOM elements, you assign the returned object's .current property to the ref attribute of a DOM element, providing direct access to that DOM element. This is useful for directly manipulating the DOM or when a reference to a DOM element is needed (e.g., setting input focus, dynamically adjusting size).

import React, { useRef } from 'react';

function TextInputWithFocusButton() {
  const inputEl = useRef(null);

  const onButtonClick = () => {
    // `current` points to the referenced input element.
    inputEl.current.focus();
  };

  return (
    <div>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • Persisting Values Between Renders

useRef is also used to persist values between component renders. The .current property of the object created by useRef holds a value that does not get reset between re-renders and maintains the same reference.

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

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

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      setTimer((prevTimer) => prevTimer + 1);
    }, 1000);
    return () => clearInterval(intervalRef.current);
  }, []);

  return <div>Timer: {timer}</div>;
}
Enter fullscreen mode Exit fullscreen mode

In the TimerComponent example above, intervalRef is used to store the timer ID created by setInterval. This way, when the component unmounts, the timer can be accurately cleared with clearInterval. Using useRef ensures that the value persists through component re-renders, so the timer ID is not lost due to re-renders.

  1. useLayoutEffect

useLayoutEffect is one of React's Hooks, functioning very similarly to useEffect. However, the key difference is that the code inside useLayoutEffect runs synchronously before the browser paints the screen. This is useful when immediate additional work is needed after DOM changes. For instance, useLayoutEffect can be used for DOM measurements or applying direct changes to the DOM. useLayoutEffect is useful when immediate action is required after DOM changes, but it's generally better to use useEffect whenever possible. Code within useLayoutEffect can delay browser updates, potentially causing performance issues. Therefore, useLayoutEffect should be used only when truly necessary.

useLayoutEffect(() => {
  // Code to execute for the side effect
  return () => {
    // Clean-up code (optional)
  };
}, [dependency array]);
Enter fullscreen mode Exit fullscreen mode
  • The first argument is the side effect function to execute. This function runs immediately after DOM updates, but before the browser paints the screen.

  • The second argument is the dependency array. The side effect function will re-run whenever the specified values in the array change.

Below is an example using useLayoutEffect to measure the size of an element when the component mounts and store it in the state.

import React, { useLayoutEffect, useRef, useState } from 'react';

function MeasureExample() {
  const [size, setSize] = useState({});
  const ref = useRef(null);

  useLayoutEffect(() => {
    setSize({ width: ref.current.offsetWidth, height: ref.current.offsetHeight });
  }, []); // The dependency array is empty, so it runs only when the component mounts

  return (
    <div ref={ref}>
      <p>Width: {size.width}, Height: {size.height}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, useLayoutEffect runs right after the component mounts, before the screen is painted for the user. This allows for the safe reading of ref.current.offsetWidth and ref.current.offsetHeight values, which are then stored in the state via setSize.

  1. Custom Hooks

Custom Hooks in React are essentially functions that combine React's built-in hooks to make specific logic reusable. They allow you to modularize component logic, reduce code duplication, and create cleaner, more manageable code.

Custom Hooks offer several benefits:

  • Reusability: Similar logic can be reused across multiple components.

  • Simplicity: Complex component logic can be broken down into simple, clear functions.

  • Composability: Multiple hooks can be combined to create more powerful functionalities.

When using custom hooks, it's important to adhere to the rules of hooks set by React. Notably, hooks should be called at the top level of your components and not inside loops, conditions, or nested functions. By following these rules, you ensure that React's state and lifecycle events function in a predictable manner.

Here's an example of a custom hook called useToggle. This hook manages a boolean state and provides a function to toggle this state. It can be particularly useful for showing or hiding a part of the UI.

import { useState } from 'react';

// A custom hook named useToggle
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  // A function to invert the current state
  const toggle = () => setValue(!value);

  return [value, toggle];
}

// Usage example
function ToggleComponent() {
  const [isVisible, toggleVisibility] = useToggle();

  return (
    <div>
      <button onClick={toggleVisibility}>
        {isVisible ? 'Hide' : 'Show'}
      </button>
      {isVisible && <p>Now you can see me!</p>}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this example, the useToggle hook takes an initial value of false to create a boolean state. When the toggle function is called, this state is flipped. In the ToggleComponent, useToggle is used to manage the isVisible state, toggling it each time the button is clicked. Depending on the state, it implements a simple UI that can show or hide text.

Although useToggle is a very basic example, it illustrates the concept of custom hooks well. Such simple functionalities can be turned into reusable hooks, making them easily utilized across multiple components.

When using React Hooks, there are several best practices and precautions to keep in mind to ensure your components are efficient and bug-free

  1. Always Call Hooks at the Top Level
  2. Use Hooks from React Function Components or Custom Hooks
  3. Conditional Execution of Effects
  4. Use ESLint Plugin for React Hooks
  5. Think in Hooks

Thanks.

Top comments (0)