React Hooks - rs-hash/Senior GitHub Wiki

Concepts

React hooks are a set of functions provided by React to add stateful logic and side effects to functional components. They were introduced in React 16.8 and allow developers to use state and other React features without writing a class. The main concepts of React hooks are:

  1. useState: The useState hook is used to add stateful logic to functional components. It allows you to declare and manage state variables within a function component.

  2. useEffect: The useEffect hook enables the management of side effects in functional components. It is used to perform actions after the component has rendered or to handle cleanup tasks when the component unmounts.

  3. useContext: The useContext hook provides a way to consume values from React's Context API within functional components. It allows you to access and update context values without using higher-order components or render props.

  4. useReducer: The useReducer hook is an alternative to useState for managing complex state logic that involves multiple actions. It follows the same pattern as Redux reducers.

  5. useCallback: The useCallback hook is used to memoize functions to prevent unnecessary re-renders when passing them down as props to child components.

  6. useMemo: The useMemo hook is used to memoize the result of a function, preventing expensive calculations from being re-computed on every render.

  7. useRef: The useRef hook is used to create mutable references that persist across renders. It is commonly used to access and manipulate DOM elements.

  8. useImperativeHandle: The useImperativeHandle hook allows a parent component to interact with child components that are using forward refs.

  9. useLayoutEffect: The useLayoutEffect hook is similar to useEffect, but it runs synchronously after all DOM mutations. It is useful when you need to measure or manipulate DOM elements before the browser repaints.

  10. useDebugValue: The useDebugValue hook is used to display custom labels in React DevTools when inspecting hooks.

Rules of Hooks

The Rules of Hooks are a set of guidelines and restrictions introduced by React to ensure that hooks are used correctly and consistently. Adhering to these rules is crucial to preventing bugs and unexpected behavior in React components. Here are the key rules of hooks:

  1. Only Call Hooks at the Top Level:

    • Hooks must be called at the top level of a function component or another custom hook. They should not be called inside loops, conditions, or nested functions.
  2. Call Hooks Only from React Functions:

    • Hooks should only be used in functional components, custom hooks, or other React-related functions. They should not be used in regular JavaScript functions or class components.
  3. Don't Call Hooks Conditionally:

    • Hooks should be called unconditionally, not inside conditional statements. React relies on the order of hooks, and calling hooks conditionally can lead to incorrect state updates.
  4. Always Call Hooks in the Same Order:

    • The order of hooks in a component must always be the same between renders. Do not re-arrange hooks or place them in conditional blocks.
  5. Never Call Hooks from Regular JavaScript Functions:

    • Hooks should only be called from within functional components, custom hooks, or other React-related functions. Do not call hooks from event handlers, callbacks, or regular JavaScript functions.
  6. Use Hooks in the Top-Level Function Component:

    • Hooks should be used directly within the top-level function component or a custom hook. Avoid defining and using hooks inside nested functions.
  7. Name Custom Hooks with "use" Prefix:

    • Custom hooks must be named with the prefix "use" to signal that they are hooks and follow the rules of hooks.
  8. Use React Hooks Only with React Versions Supporting Hooks:

    • React hooks are supported starting from React 16.8. Ensure you are using a compatible version of React to use hooks.

By following these rules, you ensure that your components work as expected, maintain a consistent state, and avoid potential bugs caused by improper usage of hooks. It is essential to understand and apply these rules when using hooks in your React applications.

Explanation of each React hook with detailed examples:

1. useState:

  • Description: useState is used to add state to functional components.
  • Example:
    import React, { useState } from 'react';
    
    function Counter() {
      const [count, setCount] = useState(0);
    
      const increment = () => {
        setCount(count + 1);
      };
    
      return (
        <div>
          <p>Count: {count}</p>
          <button onClick={increment}>Increment</button>
        </div>
      );
    }

Explanation: In the example above, the useState hook is used to add state to the Counter component. The count state variable is initialized to 0, and the setCount function is used to update the state. Clicking the "Increment" button calls the increment function, which updates the count by incrementing it.

Gotchas

  • only can use inside functional components
  • cannot use conditionally, must execute in same order

2. useEffect:

  • Description: useEffect is used to perform side effects in functional components.
  • Example:
    import React, { useState, useEffect } from 'react';
    
    function Timer() {
      const [seconds, setSeconds] = useState(0);
    
      useEffect(() => {
        const interval = setInterval(() => {
          setSeconds(seconds => seconds + 1);
        }, 1000);
    
        return () => {
          clearInterval(interval);
        };
      }, []);
    
      return (
        <div>
          <p>Seconds: {seconds}</p>
        </div>
      );
    }

Explanation: In the example above, the useEffect hook is used to start a timer when the component mounts. The effect runs only once (due to the empty dependency array []), sets up an interval to increment the seconds every second, and returns a cleanup function that clears the interval when the component unmounts.

3. useContext:

  • Description: useContext is used to access the value provided by the nearest Context.Provider in the component tree.
  • Example:
    import React, { useContext } from 'react';
    
    const ThemeContext = React.createContext('light');
    
    function ThemeButton() {
      const theme = useContext(ThemeContext);
    
      return (
        <button style={{ background: theme }}>
          Theme Button
        </button>
      );
    }

Explanation: In the example above, the useContext hook is used to access the value provided by the ThemeContext nearest in the component tree. The theme value is used to set the background color of the button.

4. useReducer:

  • Description: useReducer is used to manage complex state logic within a component using a reducer function.
  • Example:
    import React, { useReducer } from 'react';
    
    const initialState = { count: 0 };
    
    function reducer(state, action) {
      switch (action.type) {
        case 'increment':
          return { count: state.count + 1 };
        case 'decrement':
          return { count: state.count - 1 };
        default:
          return state;
      }
    }
    
    function Counter() {
      const [state, dispatch] = useReducer(reducer, initialState);
    
      return (
        <div>
          <p>Count: {state.count}</p>
          <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
          <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
        </div>
      );
    }

Explanation: In the example above, the useReducer hook is used to manage state using the reducer function. The state represents the current state object, and dispatch is a function to dispatch actions. Clicking the "Increment" button dispatches an action of type 'increment', and clicking the "Decrement" button dispatches an action of type 'decrement', which updates the state accordingly.

5. useMemo:

  • Description: useMemo is used to memoize the result of a function, avoiding unnecessary re-computations.
  • Example:
    import React, { useMemo } from 'react';
    
    function ExpensiveCalculation({ number }) {
      const result = useMemo(() => {
        // Perform expensive calculation here
        // ...
        return number * 2;
      }, [number]);
    
      return <p>Result: {result}</p>;
    }

Explanation: In the example above, the useMemo hook is used to memoize the result of an expensive calculation. The result is calculated only when the number prop changes, preventing unnecessary re-computations.

6. useCallback:

In React, the useCallback hook is used to memoize a function, preventing unnecessary re-creation of the function on each render. This is particularly useful when dealing with components that pass down functions as props to child components. By using useCallback, you can ensure that the function reference remains the same between renders, optimizing performance and preventing child components from re-rendering unless their dependencies change.

Example: Let's consider a parent component that renders a child component and provides it with a callback function as a prop. We'll use useCallback to ensure that the callback function is not recreated on each render of the parent component.

import React, { useState, useCallback } from 'react';
import ChildComponent from './ChildComponent';

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

  // Using useCallback to memoize the callback function
  const handleIncrement = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <h2>Parent Component</h2>
      <p>Count: {count}</p>
      {/* Pass the memoized callback function to the child component */}
      <ChildComponent onIncrement={handleIncrement} />
    </div>
  );
}

Now, in the child component, we can use the onIncrement prop to call the callback function passed down from the parent:

import React from 'react';

function ChildComponent({ onIncrement }) {
  return (
    <div>
      <h3>Child Component</h3>
      <button onClick={onIncrement}>Increment</button>
    </div>
  );
}

Without using useCallback in the parent component, the handleIncrement function would be recreated on every render, leading to unnecessary re-renders of the child component, even if the function itself hasn't changed. By using useCallback, the function is memoized, and the child component will only re-render if other dependencies of the child component have changed, providing a performance optimization.

7. useRef:

  • Description: useRef is used to create a mutable reference that persists across renders.
  • Example:
    import React, { useRef } from 'react';
    
    function InputFocus() {
      const inputRef = useRef(null);
    
      const handleClick = () => {
        inputRef.current.focus();
      };
    
      return (
        <div>
          <input ref={inputRef} type="text" />
          <button onClick={handleClick}>Focus Input</button>
        </div>
      );
    }

Explanation: In the example above, the useRef hook is used to create a reference to the input element. The inputRef object persists across renders, allowing us to access the DOM node and call the focus method to focus the input when the "Focus Input" button is clicked.

8. useLayoutEffect:

  • Description: useLayoutEffect is similar to useEffect, but it runs synchronously after the DOM has been updated but before the browser paints.
  • Example:
    import React, { useState, useLayoutEffect } from 'react';
    
    function MeasureElement() {
      const [width, setWidth] = useState(0);
      const ref = useRef(null);
    
      useLayoutEffect(() => {
        setWidth(ref.current.offsetWidth);
      }, []);
    
      return <div ref={ref}>Width: {width}px</div>;
    }

Explanation: In the example above, the useLayoutEffect hook is used to measure the width of an element after it has been rendered. The effect runs once (due to the empty dependency array []) and sets the width state based on the current element's offset width.

9. useDebugValue:

  • Description: useDebugValue is used to display a label for custom hooks in React DevTools.
  • Example:
    import { useDebugValue, useState } from 'react';
    
    function useCustomHook(initialValue) {
      const [value, setValue] = useState(initialValue);
      useDebugValue(value > 0 ? 'Positive' : 'Negative');
    
      return value;
    }

Explanation: In the example above, the useDebugValue hook is used within a custom hook. When the hook is inspected in React DevTools, it will display either 'Positive' or 'Negative' based on the value. This can be helpful for providing additional debugging information for custom hooks.

Purpose of using useCallback hook in combination with the useEffect

In React, the useCallback hook is often used in combination with the useEffect hook to optimize the performance of your components, particularly when dealing with function references.

useEffect is used to perform side effects in a functional component, such as data fetching, DOM manipulation, or subscriptions. It takes two arguments: a function that contains the code for the side effect, and an array of dependencies that determines when the effect should be re-run. If any value in the dependency array changes between renders, the effect will be re-executed.

useCallback is used to memoize functions, which means it returns a memoized version of the callback function that only changes if one of the dependencies has changed. This can be useful to prevent unnecessary re-renders of child components that receive callback functions as props.

The main purpose of using useCallback in conjunction with useEffect is to prevent unnecessary re-creation of function references inside the effect. If you pass a callback function to the dependency array of useEffect, and this callback is created anew in every render, it might cause the effect to run more often than necessary, impacting performance.

Here's a common scenario where useCallback and useEffect are used together:

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

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

  const handleClick = useCallback(() => {
    // Do something with count
    console.log("Button clicked with count:", count);
  }, [count]);

  useEffect(() => {
    // Effect code that depends on handleClick
    // This effect will only re-run if handleClick changes
    console.log("Effect running");
  }, [handleClick]);

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

In this example, handleClick is memoized using useCallback, ensuring that it only changes when count changes. This way, the effect depending on handleClick will only re-run when necessary.

Remember that optimizing with useCallback and useEffect should be done when there is a performance concern, as it might add some complexity to your code. Always consider the trade-offs between optimization and code readability/maintainability.

Cleanup in useEffect

Certainly! Here are examples of different scenarios where using a cleanup function in the useEffect hook is beneficial:

  1. Subscriptions and Event Listeners: If you're subscribing to external data sources or attaching event listeners, you should include a cleanup function to unsubscribe or remove the listeners when the component is unmounted.

    useEffect(() => {
      const subscription = externalDataSource.subscribe(updateData);
      window.addEventListener('resize', handleResize);
    
      return () => {
        subscription.unsubscribe();
        window.removeEventListener('resize', handleResize);
      };
    }, []);
  2. Data Fetching and Cleanup: If you're fetching data from an API or external source and need to cancel the fetch or release resources when the component unmounts, a cleanup function can be useful.

    useEffect(() => {
      const controller = new AbortController();
    
      fetchData(controller.signal)
        .then(data => setData(data))
        .catch(error => console.error(error));
    
      return () => {
        controller.abort();
      };
    }, []);
  3. Animations and Timers: If you're using animations or timers that need to be stopped or cleared to prevent them from continuing after the component is unmounted, include a cleanup function.

    useEffect(() => {
      const animationId = startAnimation();
      const timerId = setTimeout(() => {}, 1000);
    
      return () => {
        cancelAnimationFrame(animationId);
        clearTimeout(timerId);
      };
    }, []);
  4. Dependency Cleanup: When the effect's dependencies change and the effect is re-run, the previous cleanup function will be executed before the new effect. This can be useful for cleaning up resources associated with the previous run.

    useEffect(() => {
      const subscription = subscribeToDependency(dependency);
      
      return () => {
        subscription.unsubscribe();
      };
    }, [dependency]);
  5. Clean Up on Component Unmount: When the dependency array is empty, the effect runs only when the component mounts, and its cleanup function will be executed when the component unmounts.

    useEffect(() => {
      // Component setup code
    
      return () => {
        // Component cleanup code
      };
    }, []);

Remember that cleanup functions in useEffect help maintain proper memory management and prevent unintended behaviors. It's important to consider the specific needs of your use case and determine whether cleanup is necessary to ensure a well-behaved component.

⚠️ **GitHub.com Fallback** ⚠️