Essential React Hooks

Learn how to use essential React hooks to build faster and more efficient web applications.

What are Hooks?

Hooks are a new addition in React 16.8 that allow you to use state and other React features without writing a class. They solve problems like:

  • Reusing stateful logic between components
  • Complex components becoming hard to understand
  • Confusing class concepts for developers

Basic Hooks

useState

The useState hook lets you add state to functional components.

import { useState } from 'react';

const Counter = () => {
  // Declare a state variable called "count" with initial value 0
  const [count, setCount] = useState<number>(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
};

Key points:

  • Returns the current state and a function to update it
  • The initial state is set only on the first render
  • State updates trigger a re-render
  • You can use multiple useState hooks in a single component

useEffect

The useEffect hook lets you perform side effects in function components.

import { useState, useEffect } from 'react';

const UserProfile = ({ userId }) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState<boolean>(true);

  // Effect for fetching user data
  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true);
      try {
        const response = await fetch(`/api/users/${userId}`);
        const userData = await response.json();
        setUser(userData);
      } catch (error) {
        console.error('Failed to fetch user:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, [userId]); // Only re-run if userId changes

  // Effect for updating document title
  useEffect(() => {
    document.title = user ? `${user.name}'s Profile` : 'Loading...';
  }, [user]); // Only re-run if user changes

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      <h1>{user?.name}</h1>
      <p>{user?.email}</p>
    </div>
  );
};

Key points:

  • Runs after every render by default
  • Accepts a dependency array to control when it runs
  • Can return a cleanup function
  • Replaces componentDidMount, componentDidUpdate, and componentWillUnmount

useContext

The useContext hook lets you subscribe to React context without introducing nesting.

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

// Create a context
interface ThemeContextType {
  theme: string;
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

// Context provider component
const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState<string>('light');

  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

// Component that uses the context
const ThemedButton = () => {
  const context = useContext(ThemeContext);
  
  if (!context) {
    throw new Error('ThemedButton must be used within a ThemeProvider');
  }
  
  const { theme, toggleTheme } = context;

  return (
    <button 
      onClick={toggleTheme}
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#000' : '#fff'
      }}
    >
      Toggle Theme
    </button>
  );
};

Key points:

  • Accepts a context object and returns the current context value
  • Component re-renders when the context value changes
  • Still need a Context.Provider up in the component tree

useReducer

The useReducer hook is an alternative to useState for managing complex state logic.

import { useReducer } from 'react';

// Define the state type
interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

// Define action types
type TodoAction =
  | { type: 'ADD_TODO'; text: string }
  | { type: 'TOGGLE_TODO'; id: number }
  | { type: 'DELETE_TODO'; id: number };

// Reducer function
const todoReducer = (state: Todo[], action: TodoAction): Todo[] => {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          id: Date.now(),
          text: action.text,
          completed: false
        }
      ];
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.id
          ? { ...todo, completed: !todo.completed }
          : todo
      );
    case 'DELETE_TODO':
      return state.filter(todo => todo.id !== action.id);
    default:
      return state;
  }
};

const TodoApp = () => {
  const [todos, dispatch] = useReducer(todoReducer, []);
  const [text, setText] = useState<string>('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (text.trim()) {
      dispatch({ type: 'ADD_TODO', text });
      setText('');
    }
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          value={text}
          onChange={e => setText(e.target.value)}
          placeholder="Add a todo"
        />
        <button type="submit">Add</button>
      </form>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <span
              style={{
                textDecoration: todo.completed ? 'line-through' : 'none'
              }}
              onClick={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })}
            >
              {todo.text}
            </span>
            <button onClick={() => dispatch({ type: 'DELETE_TODO', id: todo.id })}>
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
};

Key points:

  • Better for complex state logic with multiple sub-values
  • Similar pattern to Redux
  • Predictable state updates

Predictable state updates

useCallback

The useCallback hook returns a memoized callback function.

import { useState, useCallback } from 'react';

const ChildComponent = ({ onClick, value }) => {
  console.log('Child rendered');
  return <button onClick={() => onClick(value)}>Click me</button>;
};

// Memoize the child component to prevent unnecessary re-renders
const MemoizedChild = React.memo(ChildComponent);

const ParentComponent = () => {
  const [count, setCount] = useState<number>(0);
  const [value, setValue] = useState<number>(1);

  // Without useCallback, this function would be recreated on every render
  // causing MemoizedChild to re-render even when its props haven't changed
  const increment = useCallback((amount: number) => {
    setCount(prevCount => prevCount + amount);
  }, []); // Empty dependency array means this function is created once

  return (
    <div>
      <p>Count: {count}</p>
      <MemoizedChild onClick={increment} value={value} />
      <button onClick={() => setValue(v => v + 1)}>Change value ({value})</button>
    </div>
  );
};

Key points:

  • Returns a memoized version of the callback
  • Only changes if dependencies change
  • Useful when passing callbacks to optimized child components

useMemo

The useMemo hook returns a memoized value.

import { useState, useMemo } from 'react';

const ExpensiveCalculationComponent = ({ number }) => {
  // This expensive calculation will only re-run when 'number' changes
  const calculatedValue = useMemo(() => {
    console.log('Calculating expensive value...');
    let result = 0;
    for (let i = 0; i < 1000000000; i++) {
      result += number;
    }
    return result;
  }, [number]); // Only recalculate when number changes

  return <div>Calculated value: {calculatedValue}</div>;
};

const App = () => {
  const [number, setNumber] = useState<number>(1);
  const [isDark, setIsDark] = useState<boolean>(false);

  return (
    <div style={{ background: isDark ? 'black' : 'white', color: isDark ? 'white' : 'black' }}>
      <input
        type="number"
        value={number}
        onChange={e => setNumber(parseInt(e.target.value))}
      />
      <ExpensiveCalculationComponent number={number} />
      <button onClick={() => setIsDark(prev => !prev)}>Toggle Theme</button>
    </div>
  );
};

Key points:

  • Returns a memoized value
  • Only recalculates when dependencies change
  • Useful for expensive calculations

useRef

The useRef hook returns a mutable ref object.

import { useRef, useEffect } from 'react';

const FocusInput = () => {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    // Focus the input element on component mount
    if (inputRef.current) {
      inputRef.current.focus();
    }
  }, []);

  const handleClick = () => {
    // Focus the input element on button click
    if (inputRef.current) {
      inputRef.current.focus();
    }
  };

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

// Another use case: storing previous values
const usePrevious = <T,>(value: T): T | undefined => {
  const ref = useRef<T>();
  
  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref.current;
};

const CounterWithPrevious = () => {
  const [count, setCount] = useState<number>(0);
  const prevCount = usePrevious<number>(count);

  return (
    <div>
      <p>Current: {count}, Previous: {prevCount}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
};

Key points:

  • Returns a mutable ref object with a .current property
  • Changing .current doesn't cause a re-render
  • Useful for accessing DOM elements and storing mutable values