DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 11: React Hooks

React Hooks

Introduction to React Hooks

React Hooks, introduced in React 16.8, revolutionized how developers write React components by enabling state and other React features in functional components. Before Hooks, functional components were considered “stateless” and developers had to use class components for state management and lifecycle methods. Hooks solved this limitation while promoting better code organization and reusability.

What Are Hooks?

Hooks are functions that “hook into” React state and lifecycle features from functional components. They don’t work inside classes – they let you use React without classes.

Built-in Hooks

useState

The useState Hook allows you to add state to functional components. It returns a stateful value and a function to update it.

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Key features of useState:

  1. Initial state: Pass the initial state to useState()
  2. Array destructuring: useState returns a pair of values that we typically destructure
  3. Multiple states: You can use useState multiple times in a single component
  4. Functional updates: You can pass a function to the state updater
// Functional update pattern
setCount(prevCount => prevCount + 1);

useEffect

The useEffect Hook lets you perform side effects in functional components. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount in React classes.

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

function DocumentTitleUpdater() {
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    // This runs after every render
    document.title = `You clicked ${count} times`;
   
    // Optional cleanup function
    return () => {
      document.title = 'React App';
    };
  }, [count]); // Only re-run if count changes
 
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Key features of useEffect:

  1. Dependency array: The second argument controls when the effect runs
  2. Cleanup function: Return a function to clean up resources
  3. Multiple effects: You can use useEffect multiple times in a component
  4. Empty dependency array: [] means “run only once after initial render”
  5. No dependency array: No second argument means “run after every render”

useContext

The useContext Hook accepts a context object (created by React.createContext) and returns the current context value. This makes consuming context much more straightforward.

import React, { useContext } from 'react';

// Create a context
const ThemeContext = React.createContext('light');

function ThemedButton() {
  // Consume the context
  const theme = useContext(ThemeContext);
 
  return (
    <button style={{ background: theme === 'dark' ? '#333' : '#fff',
                    color: theme === 'dark' ? '#fff' : '#333' }}>
      I am styled based on the theme context!
    </button>
  );
}

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ThemedButton />
    </ThemeContext.Provider>
  );
}

useReducer

The useReducer Hook is an alternative to useState for complex state logic. It’s especially useful when the next state depends on the previous state.

import React, { useReducer } from 'react';

// Reducer function
function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      throw new Error('Unexpected action');
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });
 
  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}

useCallback

The useCallback Hook returns a memoized callback function that only changes if one of the dependencies has changed. This is useful for preventing unnecessary renders in child components that rely on reference equality.

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

function ParentComponent() {
  const [count, setCount] = useState(0);
 
  // This function is recreated only when count changes
  const handleClick = useCallback(() => {
    console.log(`Button clicked with count: ${count}`);
  }, [count]);
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

// This component re-renders only when props actually change
const ChildComponent = React.memo(function ChildComponent({ onClick }) {
  console.log('ChildComponent rendered');
  return <button onClick={onClick}>Click me</button>;
});

useMemo

The useMemo Hook returns a memoized value. It’s similar to useCallback, but for values instead of functions. Use it to avoid expensive calculations on every render.

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

function ExpensiveCalculation({ items }) {
  const [count, setCount] = useState(0);
 
  // This calculation runs only when items changes
  const expensiveResult = useMemo(() => {
    console.log('Calculating...');
    return items.reduce((total, item) => total + item, 0);
  }, [items]);
 
  return (
    <div>
      <p>Count: {count}</p>
      <p>Expensive Calculation Result: {expensiveResult}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

useRef

The useRef Hook creates a mutable ref object whose .current property is initialized to the passed argument. The object persists for the full lifetime of the component.

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

function FocusInput() {
  // Create a ref
  const inputRef = useRef(null);
 
  // Focus the input when component mounts
  useEffect(() => {
    inputRef.current.focus();
  }, []);
 
  return (
    <input ref={inputRef} type="text" placeholder="I'll be focused on mount" />
  );
}

Common use cases for useRef:

  1. Accessing DOM elements directly
  2. Keeping mutable values that don’t trigger re-renders
  3. Storing previous state values

useLayoutEffect

The useLayoutEffect Hook is identical to useEffect, but it fires synchronously after all DOM mutations. Use it when you need to read layout from the DOM and synchronously re-render.

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

function Tooltip() {
  const [tooltipHeight, setTooltipHeight] = useState(0);
  const tooltipRef = useRef(null);
 
  useLayoutEffect(() => {
    const height = tooltipRef.current.offsetHeight;
    // This runs synchronously after DOM updates but before browser paint
    setTooltipHeight(height);
  }, []);
 
  return (
    <div>
      <div ref={tooltipRef} className="tooltip">
        This is a tooltip
      </div>
      <p>The tooltip height is: {tooltipHeight}px</p>
    </div>
  );
}

useImperativeHandle

The useImperativeHandle Hook customizes the instance value that is exposed when using React.forwardRef. It lets parent components call methods on child components.

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

// Child component with forwarded ref
const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);
 
  // Expose only specific methods to parent
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    reset: () => {
      inputRef.current.value = '';
    }
  }));
 
  return <input ref={inputRef} {...props} />;
});

// Parent component using the custom methods
function Parent() {
  const fancyInputRef = useRef(null);
 
  return (
    <div>
      <FancyInput ref={fancyInputRef} />
      <button onClick={() => fancyInputRef.current.focus()}>
        Focus Input
      </button>
      <button onClick={() => fancyInputRef.current.reset()}>
        Reset Input
      </button>
    </div>
  );
}

useDebugValue

The useDebugValue Hook can be used to display a label for custom hooks in React DevTools.

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

// Custom hook with debug value
function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);
 
  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);
   
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
   
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
 
  // Shows "Online: true" or "Online: false" in React DevTools
  useDebugValue(isOnline ? 'Online' : 'Offline');
 
  return isOnline;
}

function StatusIndicator() {
  const isOnline = useOnlineStatus();
 
  return <p>You are {isOnline ? 'online' : 'offline'}</p>;
}

Rules of Hooks

To ensure hooks work correctly, you must follow these two rules:

1. Only Call Hooks at the Top Level

Don’t call hooks inside loops, conditions, or nested functions. This ensures the hooks are called in the same order each time a component renders.

// ❌ Wrong: Hook inside a condition
function BadComponent() {
  const [count, setCount] = useState(0);
 
  if (count > 0) {
    // This breaks the rules of Hooks
    const [name, setName] = useState('');
  }
 
  // ...
}

// ✅ Correct: All Hooks at the top level
function GoodComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
 
  // Use the state variables normally
  if (count > 0) {
    // ...
  }
 
  // ...
}

2. Only Call Hooks from React Functions

Only call hooks from React functional components or from custom hooks. Don’t call hooks from regular JavaScript functions.

// ✅ Good: Called from a functional component
function MyComponent() {
  const [count, setCount] = useState(0);
  // ...
}

// ✅ Good: Called from a custom Hook
function useCustomHook() {
  const [value, setValue] = useState(0);
  // ...
  return value;
}

// ❌ Bad: Called from a regular function
function regularFunction() {
  const [value, setValue] = useState(0); // This breaks the rules
  // ...
}

Creating Custom Hooks

One of the most powerful features of Hooks is the ability to create your own custom hooks. A custom hook is a JavaScript function that starts with “use” and can call other hooks.

import { useState, useEffect } from 'react';

// Custom hook for fetching data
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const abortController = new AbortController();
   
    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url, { signal: abortController.signal });
       
        if (!response.ok) {
          throw new Error(`HTTP error! Status: ${response.status}`);
        }
       
        const result = await response.json();
        setData(result);
        setError(null);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
          setData(null);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchData();
   
    // Cleanup function to abort fetch on unmount or URL change
    return () => abortController.abort();
  }, [url]);

  return { data, loading, error };
}

// Using the custom hook
function UserProfile({ userId }) {
  const { data, loading, error } = useFetch(`https://api.example.com/users/${userId}`);
 
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
 
  return (
    <div>
      <h1>{data.name}</h1>
      <p>Email: {data.email}</p>
    </div>
  );
}

More Custom Hook Examples

useLocalStorage

A hook to persist state in localStorage.

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  // Initialize state from localStorage or use initialValue
  const [value, setValue] = useState(() => {
    try {
      const item = 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(() => {
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error('Error writing to localStorage:', error);
    }
  }, [key, value]);
 
  return [value, setValue];
}

// Example usage
function PersistentForm() {
  const [name, setName] = useLocalStorage('username', '');
 
  return (
    <div>
      <input
        type="text"
        value={name}
        onChange={e => setName(e.target.value)}
        placeholder="Enter your name"
      />
      <p>Your name will persist even after refresh!</p>
    </div>
  );
}

useMedia

A hook to track media query matches.

import { useState, useEffect } from 'react';

function useMedia(query) {
  // Initialize with current match state
  const [matches, setMatches] = useState(() =>
    window.matchMedia(query).matches
  );
 
  useEffect(() => {
    const mediaQuery = window.matchMedia(query);
   
    // Update state on change
    const updateMatches = () => setMatches(mediaQuery.matches);
   
    // Modern browsers
    mediaQuery.addEventListener('change', updateMatches);
   
    // Initial check
    updateMatches();
   
    // Cleanup
    return () => {
      mediaQuery.removeEventListener('change', updateMatches);
    };
  }, [query]);
 
  return matches;
}

// Example usage
function ResponsiveComponent() {
  const isMobile = useMedia('(max-width: 768px)');
 
  return (
    <div>
      <h1>This website is optimized for {isMobile ? 'mobile' : 'desktop'}</h1>
      {isMobile ? <MobileLayout /> : <DesktopLayout />}
    </div>
  );
}

Hooks vs. Classes

React Hooks offer several advantages over class components:

FeatureHooksClasses
Syntax complexitySimpler, more conciseMore verbose, requires this
State managementuseState or useReducerthis.state and this.setState()
Side effectsuseEffect combines lifecycle methodsScattered across multiple lifecycle methods
Code reuseCustom hooksHigher-Order Components or render props
Mental modelFunctions and closuresClasses and instances
Learning curveEasier for JavaScript developersRequires understanding of this binding
TypeScript supportBetter type inferenceSometimes requires explicit types
Bundle sizeGenerally smallerSlightly larger

Advanced Patterns with Hooks

Optimizing Performance

Memoization with useCallback and useMemo

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

function SearchResults() {
  const [query, setQuery] = useState('');
  const [sortOrder, setSortOrder] = useState('asc');
  const [results, setResults] = useState([]);
 
  // Memoized handler - recreated only when query changes
  const handleSearch = useCallback(() => {
    // Fetch search results
    fetchResults(query).then(setResults);
  }, [query]);
 
  // Memoized derived data - recalculated only when results or sortOrder changes
  const sortedResults = useMemo(() => {
    console.log('Sorting results');
    return [...results].sort((a, b) => {
      if (sortOrder === 'asc') {
        return a.name.localeCompare(b.name);
      } else {
        return b.name.localeCompare(a.name);
      }
    });
  }, [results, sortOrder]);
 
  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <button onClick={handleSearch}>Search</button>
      <select value={sortOrder} onChange={e => setSortOrder(e.target.value)}>
        <option value="asc">A-Z</option>
        <option value="desc">Z-A</option>
      </select>
      <ul>
        {sortedResults.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

State Initialization Patterns

Lazy Initial State

function ExpensiveInitialState() {
  // Bad: This function runs on every render
  const [state, setState] = useState(calculateExpensiveInitialState());

  // Good: This function runs only once during initialization
  const [state, setState] = useState(() => calculateExpensiveInitialState());
 
  // ...
}

Abstracting Complex Logic

Custom Hook for Form Handling

import { useState } from 'react';

function useForm(initialValues) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
 
  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues({
      ...values,
      [name]: value
    });
  };
 
  const handleBlur = (e) => {
    const { name } = e.target;
    setTouched({
      ...touched,
      [name]: true
    });
  };
 
  const handleSubmit = (onSubmit) => (e) => {
    e.preventDefault();
    const validationErrors = validate(values);
    setErrors(validationErrors);
   
    if (Object.keys(validationErrors).length === 0) {
      setIsSubmitting(true);
      onSubmit(values);
    }
  };
 
  // Example validation function
  const validate = (values) => {
    const errors = {};
   
    if (!values.email) {
      errors.email = 'Required';
    } else if (!/\S+@\S+\.\S+/.test(values.email)) {
      errors.email = 'Invalid email format';
    }
   
    if (!values.password) {
      errors.password = 'Required';
    } else if (values.password.length < 8) {
      errors.password = 'Password must be at least 8 characters';
    }
   
    return errors;
  };
 
  return {
    values,
    errors,
    touched,
    isSubmitting,
    handleChange,
    handleBlur,
    handleSubmit
  };
}

// Example usage
function SignupForm() {
  const {
    values,
    errors,
    touched,
    isSubmitting,
    handleChange,
    handleBlur,
    handleSubmit
  } = useForm({ email: '', password: '' });
 
  const submitForm = (values) => {
    console.log('Form submitted with:', values);
    // API call here
  };
 
  return (
    <form onSubmit={handleSubmit(submitForm)}>
      <div>
        <label>Email</label>
        <input
          type="email"
          name="email"
          value={values.email}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.email && errors.email && <span>{errors.email}</span>}
      </div>
     
      <div>
        <label>Password</label>
        <input
          type="password"
          name="password"
          value={values.password}
          onChange={handleChange}
          onBlur={handleBlur}
        />
        {touched.password && errors.password && <span>{errors.password}</span>}
      </div>
     
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}

Common Hooks Pitfalls and How to Avoid Them

1. Dependency Array Issues

// ❌ Missing dependencies
useEffect(() => {
  console.log(`Value changed to: ${value}`);
  // value is used but not listed in dependencies
}, []);

// ✅ Correct dependencies
useEffect(() => {
  console.log(`Value changed to: ${value}`);
}, [value]);

2. Stale Closures

// ❌ Stale closure problem
function Counter() {
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    const interval = setInterval(() => {
      // This always uses the initial value of count (0)
      setCount(count + 1);
    }, 1000);
   
    return () => clearInterval(interval);
  }, []); // Empty dependency array
 
  return <div>{count}</div>;
}

// ✅ Functional update to solve stale closure
function Counter() {
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    const interval = setInterval(() => {
      // This uses the latest state value
      setCount(prevCount => prevCount + 1);
    }, 1000);
   
    return () => clearInterval(interval);
  }, []); // Empty dependency array is fine now
 
  return <div>{count}</div>;
}

3. Infinite Loops

// ❌ Infinite loop
function BadComponent() {
  const [data, setData] = useState([]);
 
  // This effect runs after every render
  useEffect(() => {
    fetchData().then(newData => setData(newData));
  }); // No dependency array
 
  return <div>{/* render data */}</div>;
}

// ✅ Correct implementation
function GoodComponent() {
  const [data, setData] = useState([]);
 
  // This effect runs only once after the initial render
  useEffect(() => {
    fetchData().then(newData => setData(newData));
  }, []); // Empty dependency array
 
  return <div>{/* render data */}</div>;
}

4. Over-optimizing with useMemo and useCallback

// ❌ Unnecessary optimization
function ListItem({ item }) {
  // This is overkill for simple values
  const displayName = useMemo(() => {
    return `${item.firstName} ${item.lastName}`;
  }, [item.firstName, item.lastName]);
 
  return <li>{displayName}</li>;
}

// ✅ Better approach: only memoize expensive operations
function ExpensiveList({ items, threshold }) {
  // This is appropriate for expensive filtering operations
  const filteredItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item =>
      complexExpensiveFilter(item, threshold)
    );
  }, [items, threshold]);
 
  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

Best Practices and Tips

  1. Start with useState and useEffect: Master these basic hooks before moving to more complex ones.
  2. Extract complex logic into custom hooks: If you find yourself repeating similar hook patterns, consider creating a custom hook.
  3. Use the ESLint plugin: The eslint-plugin-react-hooks will catch many common mistakes.
  4. Keep components focused: If a component uses too many hooks, it might be doing too much and should be split.
  5. Name custom hooks meaningfully: Start with “use” and choose a name that clearly indicates what the hook does.
  6. Test hooks in isolation: Use testing libraries like @testing-library/react-hooks to test custom hooks.
  7. Set realistic dependency arrays: Don’t just add dependencies to silence ESLint warnings. Understand why each dependency is needed.
  8. Consider useReducer for complex state: When state updates depend on previous state or multiple state variables depend on each other.
  9. Limit hook nesting: Avoid deeply nested custom hooks calling other custom hooks – it makes debugging difficult.
  10. Profile before optimizing: Use React DevTools Performance tab to identify actual performance issues before adding memoization.

Summary

React Hooks transform how we write React components by enabling functional components to use state, side effects, and other React features. The core built-in hooks (useState, useEffect, useContext, useReducer, useCallback, useMemo, useRef) provide the building blocks for creating clean, reusable, and efficient React applications.

Custom hooks allow developers to extract and share stateful logic between components, leading to more maintainable code. Following the rules of hooks and best practices ensures your components behave correctly and perform well.

As you gain experience with hooks, you’ll discover they simplify many complex patterns that were cumbersome with class components while encouraging better practices like co-locating related logic.

Resources

Scroll to Top