What are React Hooks?

React Hooks are functions that let you "hook into" React state and lifecycle features from functional components. Introduced in React 16.8, hooks allow you to use state and other React features without writing a class component.

Why Hooks?

  • Simplify component logic and reduce boilerplate
  • Promote code reuse through custom hooks
  • Avoid the confusion of this keyword
  • Group related code by concern rather than lifecycle methods

Hooks Categorized by Complexity

Basic Intermediate Performance Advanced
Hook Name Category Description When to Use
useState Basic Manage local state in functional components When component needs to track and update data
useEffect Basic Perform side effects such as API calls, subscriptions, etc. When component needs to interact with external systems
useContext Intermediate Consume context values directly inside a component When data needs to be accessible by many components at different nesting levels
useRef Intermediate Access DOM nodes or store mutable variables without causing re-renders When you need to interact with DOM elements or persist values between renders
useMemo Performance Optimize performance by memoizing expensive calculations When expensive calculations should be cached and reused
useCallback Performance Memoize functions to prevent unnecessary re-renders When passing callbacks to optimized child components
useReducer Advanced Manage complex state logic (similar to Redux reducer) When state logic involves multiple sub-values or complex transitions
useLayoutEffect Advanced Runs synchronously after DOM mutations (before painting) When measurements need to be taken before the browser paints
useImperativeHandle Advanced Customize the instance value that is exposed when using ref When a parent component needs to call methods on a child component
useId Intermediate Generate unique IDs for accessibility or SSR hydration When generating unique IDs for accessibility attributes
useDeferredValue Performance Defer re-rendering a non-critical value for better UX When you need to deprioritize certain UI updates
useTransition Performance Manage concurrent UI transitions (defer less important updates) When you need to indicate loading state during a transition

Detailed Hook Explanations

useState Basic

The useState hook allows functional components to manage local state. It returns a stateful value and a function to update it.

Syntax

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

Example: Counter Component

import React, { useState } from 'react';

function Counter() {
  // Declare a state variable named "count" with initial value 0
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Key Points

  • The initial state is only used during the first render
  • You can use multiple useState hooks in a single component
  • State updates are asynchronous and batched for performance
  • For complex state logic, consider useReducer instead
  • When updating state based on previous state, use the functional update form:
    setCount(prevCount => prevCount + 1);

Best Practices

  • Keep state minimal and derive data when possible
  • Split complex state into multiple state variables
  • Use the functional update form when new state depends on old state
  • For objects and arrays, always create new copies when updating

useEffect Basic

The useEffect hook lets you perform side effects in functional components. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount in class components, but unified into a single API.

Syntax

useEffect(() => {
  // Side effect code
  
  return () => {
    // Cleanup code (optional)
  };
}, [dependencies]);

Example: Data Fetching

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

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    // Reset state when userId changes
    setLoading(true);
    setError(null);
    
    // Fetch user data
    fetch(`https://api.example.com/users/${userId}`)
      .then(response => {
        if (!response.ok) throw new Error('Failed to fetch');
        return response.json();
      })
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
      
    // Cleanup function
    return () => {
      // Cancel any pending requests or subscriptions
    };
  }, [userId]); // Re-run effect when userId changes
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return null;
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
    </div>
  );
}

Key Points

  • Effects run after every completed render
  • The dependency array controls when the effect runs:
    • Empty array ([]): Run once after initial render
    • With dependencies: Run when any dependency changes
    • No array: Run after every render
  • The cleanup function runs before the component unmounts and before re-running the effect
  • Effects are deferred until after the browser has painted

Best Practices

  • Include all values from the component scope that change over time and are used by the effect
  • Use multiple effects to separate concerns
  • Always clean up subscriptions and event listeners
  • For synchronous layout measurements, use useLayoutEffect instead

useContext Intermediate

The useContext hook accepts a context object (created by React.createContext) and returns the current context value. It lets you consume context in a more concise way than the Context.Consumer component.

Syntax

const value = useContext(MyContext);

Example: Theme Switcher

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

// Create a context with a default value
const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {},
});

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

// Consumer component using useContext
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  
  return (
    <button
      onClick={toggleTheme}
      style={{
        background: theme === 'light' ? '#ffffff' : '#333333',
        color: theme === 'light' ? '#333333' : '#ffffff',
        padding: '10px 15px',
        border: '1px solid #cccccc',
        borderRadius: '4px',
      }}
    >
      Toggle Theme (Current: {theme})
    </button>
  );
}

// App component
function App() {
  return (
    <ThemeProvider>
      <div style={{ padding: '20px' }}>
        <h1>Theme Example</h1>
        <ThemedButton />
      </div>
    </ThemeProvider>
  );
}

Key Points

  • The component will re-render whenever the context value changes
  • Works with the default value from createContext when no matching Provider is found
  • Can be used with multiple contexts in the same component
  • Context is primarily used for global state that many components need to access

Best Practices

  • Use context for truly global state (themes, user data, etc.)
  • Split contexts by domain for better performance
  • Consider using a reducer with context for complex state
  • Avoid excessive nesting of context providers

useRef Intermediate

The useRef hook returns a mutable ref object whose .current property is initialized to the passed argument. The returned object will persist for the full lifetime of the component.

Syntax

const refContainer = useRef(initialValue);

Example 1: Accessing DOM Elements

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

function AutoFocusInput() {
  // Create a ref
  const inputRef = useRef(null);
  
  // Focus the input when component mounts
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  
  return (
    <div>
      <label htmlFor="auto-focus">This input will focus on mount:</label>
      <input 
        id="auto-focus"
        ref={inputRef} 
        type="text" 
        placeholder="I'll be focused!" 
      />
    </div>
  );
}

Example 2: Storing Previous Values

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

function CounterWithPrevious() {
  const [count, setCount] = useState(0);
  // Store previous count value
  const prevCountRef = useRef();
  
  useEffect(() => {
    // Update the ref after render
    prevCountRef.current = count;
  });
  
  const prevCount = prevCountRef.current;
  
  return (
    <div>
      <h2>Current: {count}, Previous: {prevCount !== undefined ? prevCount : 'N/A'}</h2>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Key Points

  • Changing ref.current doesn't cause a re-render
  • Refs persist between renders (unlike regular variables)
  • Common uses:
    • Accessing DOM elements
    • Storing previous values
    • Keeping mutable values without triggering re-renders
    • Storing instance variables (timers, intervals)

Best Practices

  • Use refs for values that don't affect the visual output
  • Avoid overusing refs for state that should trigger re-renders
  • Don't modify DOM elements directly with refs except for focus, selection, or media playback
  • Clean up any resources (timers, subscriptions) stored in refs

useMemo Performance

The useMemo hook returns a memoized value. It only recomputes the memoized value when one of the dependencies has changed, which helps optimize performance by avoiding expensive calculations on every render.

Syntax

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

Example: Expensive Calculation

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

function PrimeCalculator() {
  const [number, setNumber] = useState(1);
  const [darkMode, setDarkMode] = useState(false);
  
  // This expensive calculation will only run when 'number' changes
  const isPrime = useMemo(() => {
    console.log('Calculating if', number, 'is prime...');
    if (number <= 1) return false;
    if (number <= 3) return true;
    if (number % 2 === 0 || number % 3 === 0) return false;
    
    let i = 5;
    while (i * i <= number) {
      if (number % i === 0 || number % (i + 2) === 0) return false;
      i += 6;
    }
    
    return true;
  }, [number]);
  
  // Theme styles that change when darkMode changes
  const theme = {
    backgroundColor: darkMode ? '#333' : '#fff',
    color: darkMode ? '#fff' : '#333',
    padding: '20px',
    borderRadius: '8px',
    transition: 'all 0.3s ease'
  };
  
  return (
    <div style={theme}>
      <div>
        <label htmlFor="number-input">Enter a number: </label>
        <input
          id="number-input"
          type="number"
          value={number}
          onChange={e => setNumber(parseInt(e.target.value) || 0)}
          min="1"
        />
      </div>
      
      <div style={{ margin: '20px 0' }}>
        <strong>{number}</strong> is {isPrime ? 'a prime number!' : 'not a prime number.'}
      </div>
      
      <button onClick={() => setDarkMode(prev => !prev)}>
        Toggle {darkMode ? 'Light' : 'Dark'} Mode
      </button>
    </div>
  );
}

Key Points

  • The function passed to useMemo runs during rendering
  • Returns the cached result when dependencies haven't changed
  • Helps avoid expensive calculations on every render
  • Can be used to prevent unnecessary re-renders of child components
  • React may discard memoized values for performance reasons

Best Practices

  • Only use for computationally expensive operations
  • Include all values from the component scope used by the memoized function in the dependency array
  • Don't overuse - the memoization itself has a cost
  • Consider using for referential equality of objects and arrays passed to child components

useCallback Performance

The useCallback hook returns a memoized callback function. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders.

Syntax

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

Example: Optimized Child Component

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

// Child component that only re-renders when props change
const ExpensiveComponent = memo(function ExpensiveComponent({ onClick, name }) {
  console.log(`Rendering ExpensiveComponent for ${name}`);
  
  return (
    <div style={{ margin: '10px 0', padding: '10px', border: '1px solid #ccc' }}>
      <p>Expensive component for {name}</p>
      <button onClick={() => onClick(name)}>Click me</button>
    </div>
  );
});

// Parent component
function CallbackExample() {
  const [count, setCount] = useState(0);
  const [names] = useState(['Alice', 'Bob', 'Charlie']);
  
  // Without useCallback, this function would be recreated on every render
  // causing ExpensiveComponent to re-render unnecessarily
  const handleClick = useCallback((name) => {
    console.log(`${name} clicked!`);
    // Note: count is captured from closure and won't update unless added to deps
  }, []); // Empty deps means this callback never changes
  
  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={() => setCount(c => c + 1)}>
        Increment count
      </button>
      
      <div style={{ marginTop: '20px' }}>
        {names.map(name => (
          <ExpensiveComponent 
            key={name} 
            name={name} 
            onClick={handleClick} 
          />
        ))}
      </div>
      
      <p><small>Check console to see which components render</small></p>
    </div>
  );
}

Key Points

  • useCallback(fn, deps) is equivalent to useMemo(() => fn, deps)
  • Returns the same function instance between renders if dependencies haven't changed
  • Primarily useful when passing callbacks to optimized child components
  • Works well with React.memo() for preventing unnecessary re-renders

Best Practices

  • Include all values from the component scope used by the callback in the dependency array
  • Only use when passing callbacks to optimized components that rely on reference equality
  • Consider using with useReducer for callbacks that update state
  • Don't overuse - only apply when there's a measurable performance benefit

useReducer Advanced

The useReducer hook is an alternative to useState for managing complex state logic. It's especially useful when the next state depends on the previous state or when state transitions are complex.

Syntax

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

Example: Shopping Cart

import React, { useReducer } from 'react';

// Reducer function
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      // Check if item already exists
      const existingItem = state.items.find(item => item.id === action.payload.id);
      if (existingItem) {
        // Update quantity of existing item
        return {
          ...state,
          items: state.items.map(item => 
            item.id === action.payload.id 
              ? { ...item, quantity: item.quantity + 1 } 
              : item
          ),
          total: state.total + action.payload.price
        };
      } else {
        // Add new item
        return {
          ...state,
          items: [...state.items, { ...action.payload, quantity: 1 }],
          total: state.total + action.payload.price
        };
      }
    
    case 'REMOVE_ITEM':
      const itemToRemove = state.items.find(item => item.id === action.payload);
      if (!itemToRemove) return state;
      
      // If quantity is 1, remove the item completely
      if (itemToRemove.quantity === 1) {
        return {
          ...state,
          items: state.items.filter(item => item.id !== action.payload),
          total: state.total - itemToRemove.price
        };
      } else {
        // Otherwise decrease quantity
        return {
          ...state,
          items: state.items.map(item => 
            item.id === action.payload 
              ? { ...item, quantity: item.quantity - 1 } 
              : item
          ),
          total: state.total - itemToRemove.price
        };
      }
    
    case 'CLEAR_CART':
      return {
        items: [],
        total: 0
      };
      
    default:
      return state;
  }
}

// Component using the reducer
function ShoppingCart() {
  const [cart, dispatch] = useReducer(cartReducer, { items: [], total: 0 });
  
  // Sample products
  const products = [
    { id: 1, name: 'Product 1', price: 10.99 },
    { id: 2, name: 'Product 2', price: 24.99 },
    { id: 3, name: 'Product 3', price: 5.99 }
  ];
  
  return (
    <div>
      <h2>Shopping Cart</h2>
      
      <div className="products">
        <h3>Products</h3>
        {products.map(product => (
          <div key={product.id} style={{ margin: '10px 0', padding: '10px', border: '1px solid #eee' }}>
            <p>{product.name} - ${product.price.toFixed(2)}</p>
            <button 
              onClick={() => dispatch({ 
                type: 'ADD_ITEM', 
                payload: product 
              })}
            >
              Add to Cart
            </button>
          </div>
        ))}
      </div>
      
      <div className="cart" style={{ marginTop: '20px' }}>
        <h3>Cart Items</h3>
        {cart.items.length === 0 ? (
          <p>Your cart is empty</p>
        ) : (
          <>
            {cart.items.map(item => (
              <div key={item.id} style={{ margin: '10px 0', padding: '10px', border: '1px solid #eee' }}>
                <p>
                  {item.name} - ${item.price.toFixed(2)} x {item.quantity} = ${(item.price * item.quantity).toFixed(2)}
                </p>
                <button 
                  onClick={() => dispatch({ 
                    type: 'REMOVE_ITEM', 
                    payload: item.id 
                  })}
                >
                  Remove One
                </button>
              </div>
            ))}
            <div style={{ marginTop: '20px', fontWeight: 'bold' }}>
              Total: ${cart.total.toFixed(2)}
            </div>
            <button 
              onClick={() => dispatch({ type: 'CLEAR_CART' })}
              style={{ marginTop: '10px' }}
            >
              Clear Cart
            </button>
          </>
        )}
      </div>
    </div>
  );
}

Key Points

  • The reducer function takes (state, action) and returns the new state
  • The dispatch function is used to send actions to the reducer
  • Actions are typically objects with a type property and optional payload
  • The optional init function can be used to lazily create the initial state
  • Similar to Redux but scoped to a component

Best Practices

  • Use for complex state logic with multiple sub-values or when next state depends on previous state
  • Keep reducers pure - no side effects, API calls, or mutations
  • Use action constants to avoid typos
  • Consider combining with useContext for global state management
  • Split large reducers into smaller functions for maintainability

useLayoutEffect Advanced

The useLayoutEffect hook is identical to useEffect, but it fires synchronously after all DOM mutations and before the browser has a chance to paint. Use this to read layout from the DOM and synchronously re-render.

Syntax

useLayoutEffect(() => {
  // DOM mutations/measurements
  return () => {
    // cleanup
  };
}, [dependencies]);

Example: Measuring and Positioning an Element

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

function Tooltip({ text, children }) {
  const [tooltipStyles, setTooltipStyles] = useState({});
  const childRef = useRef(null);
  const tooltipRef = useRef(null);
  const [showTooltip, setShowTooltip] = useState(false);
  
  // Position the tooltip relative to the child element
  useLayoutEffect(() => {
    if (showTooltip && childRef.current && tooltipRef.current) {
      const childRect = childRef.current.getBoundingClientRect();
      const tooltipRect = tooltipRef.current.getBoundingClientRect();
      
      // Position tooltip above the element
      setTooltipStyles({
        position: 'absolute',
        top: `${childRect.top - tooltipRect.height - 10}px`,
        left: `${childRect.left + (childRect.width / 2) - (tooltipRect.width / 2)}px`,
        opacity: 1
      });
    }
  }, [showTooltip]);
  
  return (
    <div style={{ position: 'relative', display: 'inline-block' }}>
      <div 
        ref={childRef}
        onMouseEnter={() => setShowTooltip(true)}
        onMouseLeave={() => setShowTooltip(false)}
      >
        {children}
      </div>
      
      {showTooltip && (
        <div 
          ref={tooltipRef}
          style={{
            background: '#333',
            color: 'white',
            padding: '5px 10px',
            borderRadius: '4px',
            position: 'absolute',
            opacity: 0,
            transition: 'opacity 0.2s',
            zIndex: 100,
            ...tooltipStyles
          }}
        >
          {text}
        </div>
      )}
    </div>
  );
}

// Usage
function LayoutEffectExample() {
  return (
    <div style={{ padding: '100px 20px' }}>
      <Tooltip text="This is a tooltip!">
        <button>Hover me</button>
      </Tooltip>
    </div>
  );
}

Key Points

  • Runs synchronously after DOM mutations but before browser paint
  • Blocks visual updates until the effect completes
  • Useful for:
    • Measuring DOM nodes (width, height, position)
    • Moving elements based on layout
    • Preventing visual flicker when updating the DOM
  • Has the same dependency array behavior as useEffect

Best Practices

  • Prefer useEffect when possible to avoid blocking visual updates
  • Only use for DOM measurements and updates that must happen before paint
  • Keep the logic inside as minimal as possible
  • Be careful with infinite loops when updating state inside

useImperativeHandle Advanced

The useImperativeHandle hook customizes the instance value that is exposed when using React.forwardRef. It lets a parent component access specific functions or properties from a child component.

Syntax

useImperativeHandle(ref, createHandle, [dependencies]);

Example: Custom Input with Focus Method

import React, { useRef, useImperativeHandle, forwardRef, useState } from 'react';

// Custom input component with exposed methods
const CustomInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);
  const [value, setValue] = useState('');
  
  // Expose only specific methods to parent
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    clear: () => {
      setValue('');
      inputRef.current.focus();
    },
    getValue: () => {
      return value;
    }
  }), [value]);
  
  return (
    <input
      ref={inputRef}
      value={value}
      onChange={(e) => setValue(e.target.value)}
      style={{
        padding: '8px 12px',
        border: '2px solid #3498db',
        borderRadius: '4px',
        fontSize: '16px'
      }}
      {...props}
    />
  );
});

// Parent component that uses the custom input
function ImperativeHandleExample() {
  const inputRef = useRef(null);
  
  const handleFocusClick = () => {
    inputRef.current.focus();
  };
  
  const handleClearClick = () => {
    inputRef.current.clear();
  };
  
  const handleGetValueClick = () => {
    alert(`Current value: "${inputRef.current.getValue()}"`);
  };
  
  return (
    <div>
      <h3>Custom Input with Imperative Handle</h3>
      
      <div style={{ margin: '20px 0' }}>
        <CustomInput ref={inputRef} placeholder="Type something..." />
      </div>
      
      <div>
        <button 
          onClick={handleFocusClick}
          style={{ marginRight: '10px' }}
        >
          Focus Input
        </button>
        
        <button 
          onClick={handleClearClick}
          style={{ marginRight: '10px' }}
        >
          Clear Input
        </button>
        
        <button onClick={handleGetValueClick}>
          Get Value
        </button>
      </div>
    </div>
  );
}

Key Points

  • Must be used with forwardRef to work properly
  • Allows parent components to call methods on child components
  • Helps maintain encapsulation by exposing only specific functionality
  • The second argument to useImperativeHandle is a function that returns an object with the exposed methods/properties
  • The optional dependency array works like other hooks

Best Practices

  • Use sparingly - prefer props and state for most parent-child interactions
  • Only expose methods that are necessary for the parent component
  • Use for imperative actions like focus, scroll, or media playback
  • Consider if the functionality could be implemented with props instead

useId Intermediate

The useId hook generates a unique ID that can be used for accessibility attributes or for any case where a stable, unique ID is required across server and client.

Syntax

const id = useId();

Example: Accessible Form Controls

import React, { useId } from 'react';

function AccessibleForm() {
  // Generate unique IDs for form elements
  const nameId = useId();
  const emailId = useId();
  const passwordId = useId();
  
  return (
    <form style={{ maxWidth: '400px', margin: '0 auto' }}>
      <h2>Registration Form</h2>
      
      <div style={{ marginBottom: '15px' }}>
        <label 
          htmlFor={nameId}
          style={{ display: 'block', marginBottom: '5px' }}
        >
          Name
        </label>
        <input
          id={nameId}
          type="text"
          aria-describedby={`${nameId}-help`}
          style={{
            width: '100%',
            padding: '8px',
            borderRadius: '4px',
            border: '1px solid #ccc'
          }}
        />
        <small 
          id={`${nameId}-help`}
          style={{ display: 'block', marginTop: '5px', color: '#666' }}
        >
          Enter your full name
        </small>
      </div>
      
      <div style={{ marginBottom: '15px' }}>
        <label 
          htmlFor={emailId}
          style={{ display: 'block', marginBottom: '5px' }}
        >
          Email
        </label>
        <input
          id={emailId}
          type="email"
          aria-describedby={`${emailId}-help`}
          style={{
            width: '100%',
            padding: '8px',
            borderRadius: '4px',
            border: '1px solid #ccc'
          }}
        />
        <small 
          id={`${emailId}-help`}
          style={{ display: 'block', marginTop: '5px', color: '#666' }}
        >
          We'll never share your email
        </small>
      </div>
      
      <div style={{ marginBottom: '15px' }}>
        <label 
          htmlFor={passwordId}
          style={{ display: 'block', marginBottom: '5px' }}
        >
          Password
        </label>
        <input
          id={passwordId}
          type="password"
          aria-describedby={`${passwordId}-help`}
          style={{
            width: '100%',
            padding: '8px',
            borderRadius: '4px',
            border: '1px solid #ccc'
          }}
        />
        <small 
          id={`${passwordId}-help`}
          style={{ display: 'block', marginTop: '5px', color: '#666' }}
        >
          Password must be at least 8 characters
        </small>
      </div>
      
      <button
        type="submit"
        style={{
          padding: '10px 15px',
          backgroundColor: '#3498db',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: 'pointer'
        }}
      >
        Register
      </button>
    </form>
  );
}

Key Points

  • Generates a unique, stable ID string
  • IDs are consistent across server and client rendering
  • Helps avoid hydration mismatches in SSR applications
  • Preferable to manually created IDs or random generators
  • Multiple calls to useId in the same component will generate different IDs

Best Practices

  • Use for accessibility attributes like htmlFor, aria-describedby, etc.
  • Add a prefix if needed with string concatenation: const id = useId(); const checkboxId = `${id}-checkbox`;
  • Don't use for keys in lists (use data IDs or indexes instead)
  • Don't use for CSS selectors (use classes instead)

useDeferredValue Performance

The useDeferredValue hook accepts a value and returns a new copy of the value that will defer to more urgent updates. This is similar to debouncing or throttling but built into React's rendering mechanism.

Syntax

const deferredValue = useDeferredValue(value);

Example: Search with Deferred Results

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

// Simulated list of items
const generateItems = (count) => {
  return Array.from({ length: count }, (_, i) => ({
    id: i,
    name: `Item ${i + 1}`
  }));
};

const allItems = generateItems(10000);

function SearchableList() {
  const [query, setQuery] = useState('');
  
  // Defer the value to avoid blocking input
  const deferredQuery = useDeferredValue(query);
  
  // Indicate when deferred value is different from actual value
  const isStale = query !== deferredQuery;
  
  // Expensive filtering operation
  const filteredItems = useMemo(() => {
    console.log(`Filtering with query: "${deferredQuery}"`);
    
    if (!deferredQuery.trim()) return allItems.slice(0, 100);
    
    return allItems.filter(item => 
      item.name.toLowerCase().includes(deferredQuery.toLowerCase())
    );
  }, [deferredQuery]);
  
  return (
    <div>
      <h2>Search Large List</h2>
      
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Type to search..."
        style={{
          width: '100%',
          padding: '10px',
          fontSize: '16px',
          marginBottom: '20px',
          borderRadius: '4px',
          border: '1px solid #ccc'
        }}
      />
      
      <div style={{ opacity: isStale ? 0.7 : 1, transition: 'opacity 0.2s' }}>
        <p>
          {isStale ? 'Updating results...' : `Showing ${filteredItems.length} results`}
        </p>
        
        <ul style={{ 
          height: '400px', 
          overflowY: 'auto',
          border: '1px solid #eee',
          padding: '10px',
          borderRadius: '4px'
        }}>
          {filteredItems.map(item => (
            <li 
              key={item.id}
              style={{
                padding: '8px',
                borderBottom: '1px solid #f0f0f0'
              }}
            >
              {item.name}
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

Key Points

  • Allows React to defer updating less critical parts of the UI
  • Keeps the UI responsive during expensive rendering operations
  • Works with React's concurrent rendering features
  • Similar to debouncing/throttling but integrated with React's rendering system
  • The deferred value might be "stale" temporarily while more urgent updates are processed

Best Practices

  • Use for expensive rendering operations that don't need to update immediately
  • Combine with useMemo to avoid recalculating during each render
  • Consider adding visual feedback when the deferred value is stale
  • Test performance with and without deferring to ensure it's beneficial

useTransition Performance

The useTransition hook returns a stateful value for the pending state of the transition and a function to start the transition. It allows you to mark updates as transitions, which tells React they can be interrupted and don't need to block the UI.

Syntax

const [isPending, startTransition] = useTransition();

Example: Tab Switching with Transition

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

// Simulated expensive component
function ExpensiveComponent({ text }) {
  // Force the component to be slow to render
  const startTime = performance.now();
  while (performance.now() - startTime < 100) {
    // Artificial delay - simulate expensive rendering
  }
  
  return (
    <div style={{ padding: '20px', background: '#f9f9f9', borderRadius: '4px' }}>
      <h3>Expensive Component</h3>
      <p>{text}</p>
      <div>
        {Array.from({ length: 50 }, (_, i) => (
          <div key={i} style={{ padding: '5px', margin: '5px 0', background: '#eee' }}>
            Item {i + 1}
          </div>
        ))}
      </div>
    </div>
  );
}

function TabsWithTransition() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();
  
  const tabs = [
    { id: 'home', label: 'Home', content: 'Welcome to the home tab!' },
    { id: 'profile', label: 'Profile', content: 'This is your profile information.' },
    { id: 'settings', label: 'Settings', content: 'Adjust your settings here.' },
    { id: 'dashboard', label: 'Dashboard', content: 'View your analytics dashboard.' }
  ];
  
  const handleTabChange = (newTab) => {
    // Mark the tab change as a transition
    startTransition(() => {
      setTab(newTab);
    });
  };
  
  return (
    <div>
      <h2>Tabs with Transition</h2>
      
      <div style={{ marginBottom: '20px' }}>
        {tabs.map(tabItem => (
          <button
            key={tabItem.id}
            onClick={() => handleTabChange(tabItem.id)}
            style={{
              padding: '10px 15px',
              margin: '0 5px 0 0',
              background: tab === tabItem.id ? '#3498db' : '#f0f0f0',
              color: tab === tabItem.id ? 'white' : 'black',
              border: 'none',
              borderRadius: '4px',
              cursor: 'pointer',
              opacity: isPending ? 0.7 : 1
            }}
            disabled={isPending}
          >
            {tabItem.label}
          </button>
        ))}
      </div>
      
      {isPending && (
        <div style={{ 
          padding: '10px', 
          background: '#fff9e6', 
          border: '1px solid #ffe58f',
          borderRadius: '4px',
          marginBottom: '20px'
        }}>
          Loading tab content...
        </div>
      )}
      
      <div style={{ opacity: isPending ? 0.6 : 1, transition: 'opacity 0.2s' }}>
        {tabs.map(tabItem => (
          tab === tabItem.id && (
            <ExpensiveComponent 
              key={tabItem.id} 
              text={tabItem.content} 
            />
          )
        ))}
      </div>
    </div>
  );
}

Key Points

  • Returns a boolean isPending flag and a startTransition function
  • Updates wrapped in startTransition are treated as non-urgent
  • Transitions can be interrupted by more urgent updates
  • Helps keep the UI responsive during expensive state updates
  • Works with React's concurrent rendering features

Best Practices

  • Use for state updates that might lead to significant UI changes
  • Don't use for immediate feedback like input fields
  • Use the isPending flag to show loading indicators
  • Combine with Suspense for more complex loading states
  • Consider using useDeferredValue for simpler cases

Creating Custom Hooks

Custom hooks are JavaScript functions that start with "use" and may call other hooks. They let you extract component logic into reusable functions.

Example: useLocalStorage Custom Hook

import { useState, useEffect } from 'react';

// Custom hook for persisting state to localStorage
function useLocalStorage(key, initialValue) {
  // Get stored value from localStorage or use initialValue
  const [storedValue, setStoredValue] = useState(() => {
    if (typeof window === 'undefined') {
      return initialValue;
    }
    
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error('Error reading from localStorage:', error);
      return initialValue;
    }
  });
  
  // Update localStorage when state changes
  useEffect(() => {
    if (typeof window !== 'undefined') {
      try {
        window.localStorage.setItem(key, JSON.stringify(storedValue));
      } catch (error) {
        console.error('Error writing to localStorage:', error);
      }
    }
  }, [key, storedValue]);
  
  return [storedValue, setStoredValue];
}

// Usage example
function LocalStorageDemo() {
  const [name, setName] = useLocalStorage('name', '');
  const [darkMode, setDarkMode] = useLocalStorage('darkMode', false);
  
  return (
    <div style={{
      padding: '20px',
      background: darkMode ? '#333' : '#fff',
      color: darkMode ? '#fff' : '#333',
      borderRadius: '8px',
      transition: 'all 0.3s ease'
    }}>
      <h3>localStorage Demo</h3>
      
      <div style={{ marginBottom: '15px' }}>
        <label htmlFor="name" style={{ display: 'block', marginBottom: '5px' }}>
          Your Name:
        </label>
        <input
          id="name"
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          style={{
            padding: '8px',
            borderRadius: '4px',
            border: '1px solid #ccc',
            width: '100%',
            maxWidth: '300px'
          }}
        />
      </div>
      
      <div style={{ marginBottom: '15px' }}>
        <label style={{ display: 'flex', alignItems: 'center' }}>
          <input
            type="checkbox"
            checked={darkMode}
            onChange={(e) => setDarkMode(e.target.checked)}
            style={{ marginRight: '10px' }}
          />
          Dark Mode
        </label>
      </div>
      
      <p>
        <small>
          Your preferences will be remembered when you reload the page!
        </small>
      </p>
    </div>
  );
}

Custom Hook Best Practices

  • Always start custom hook names with "use" to follow React's conventions
  • Keep custom hooks focused on a single concern
  • Return only what's necessary from the hook
  • Document your custom hooks with comments or TypeScript types
  • Test custom hooks independently from components
  • Consider publishing reusable hooks as npm packages