reactjs
/

React Hooks – Complete Introduction to Modern React (2026)

Last Sync: Today

On this page

16
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

reactjs

React Hooks – Complete Introduction to Modern React (2026)

What are React Hooks?

React Hooks are functions that let you use state and other React features in functional components. Introduced in React 16.8 (2019), hooks revolutionized how we write React components by eliminating the need for class components for stateful logic. Hooks allow you to reuse stateful logic across components without changing your component hierarchy.

Why Hooks?

  • Simplify component logic – Write cleaner, more readable components
  • Reuse stateful logic – Share logic across components without HOCs or render props
  • Better code organization – Group related logic together
  • Reduce bundle size – Functional components are smaller than classes
  • Easier to learn – No need to understand 'this', binding, or classes
  • Better performance – Less overhead than class components
  • Future-proof – Hooks are the future of React development

Rules of Hooks

JavaScriptRead-only
1
// ✅ CORRECT: Hooks called at top level
function Component() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    // Effect logic
  }, []);
  
  return <div>{count}</div>;
}

// ❌ WRONG: Hook called conditionally
function Component() {
  if (condition) {
    const [count, setCount] = useState(0); // DON'T DO THIS!
  }
}

// ❌ WRONG: Hook called after early return
function Component() {
  if (error) return null;
  const [count, setCount] = useState(0); // DON'T DO THIS!
}

// ❌ WRONG: Hook in nested function
function Component() {
  function handleClick() {
    const [count, setCount] = useState(0); // DON'T DO THIS!
  }
}

// ✅ CORRECT: Custom hook following rules
function useCustomHook() {
  const [data, setData] = useState(null);
  useEffect(() => {
    // Effect logic
  }, []);
  return data;
}

useState – Managing Component State

useState is the most basic hook for adding state to functional components. It returns an array with two elements: the current state value and a function to update it.

React JSXRead-only
1
import { useState } from 'react';

// Basic counter example
function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}

// Multiple state variables
function UserProfile() {
  const [name, setName] = useState('');
  const [age, setAge] = useState(0);
  const [email, setEmail] = useState('');
  
  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <input type="number" value={age} onChange={(e) => setAge(e.target.value)} />
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
    </div>
  );
}

// Using object state
function Form() {
  const [form, setForm] = useState({ username: '', password: '', remember: false });
  
  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setForm(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value
    }));
  };
  
  return (
    <form>
      <input name="username" value={form.username} onChange={handleChange} />
      <input name="password" type="password" value={form.password} onChange={handleChange} />
      <input name="remember" type="checkbox" checked={form.remember} onChange={handleChange} />
    </form>
  );
}

// Functional updates (when new state depends on previous)
function CounterWithDelay() {
  const [count, setCount] = useState(0);
  
  const incrementLater = () => {
    setTimeout(() => {
      setCount(prevCount => prevCount + 1); // ✅ Uses previous value
      // setCount(count + 1); // ❌ Would use stale value
    }, 1000);
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementLater}>Increment after 1 second</button>
    </div>
  );
}

// Lazy initialization (for expensive computations)
function ExpensiveComponent() {
  const [state, setState] = useState(() => {
    const initialValue = expensiveComputation();
    return initialValue;
  });
  
  return <div>{state}</div>;
}

// useState with arrays
function TodoList() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState('');
  
  const addTodo = () => {
    setTodos(prev => [...prev, { id: Date.now(), text: input, completed: false }]);
    setInput('');
  };
  
  const toggleTodo = (id) => {
    setTodos(prev => prev.map(todo => 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };
  
  const removeTodo = (id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  };
  
  return (
    <div>
      <input value={input} onChange={(e) => setInput(e.target.value)} />
      <button onClick={addTodo}>Add Todo</button>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
            <button onClick={() => toggleTodo(todo.id)}>Toggle</button>
            <button onClick={() => removeTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

export { Counter, UserProfile, Form, CounterWithDelay, TodoList };

useEffect – Handling Side Effects

useEffect lets you perform side effects in functional components. It replaces lifecycle methods (componentDidMount, componentDidUpdate, componentWillUnmount) from class components.

React JSXRead-only
1
import { useState, useEffect } from 'react';

// Basic effect - runs after every render
function BasicEffect() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  }); // No dependency array - runs after every render
  
  return <button onClick={() => setCount(count + 1)}>Click me</button>;
}

// Empty dependency array - runs only once (componentDidMount)
function DataFetcher() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetch('https://api.example.com/data')
      .then(res => res.json())
      .then(data => {
        setData(data);
        setLoading(false);
      });
  }, []); // Empty array = run once
  
  if (loading) return <div>Loading...</div>;
  return <div>{JSON.stringify(data)}</div>;
}

// With dependencies - runs when dependencies change
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    setLoading(true);
    fetch(`https://api.example.com/users/${userId}`)
      .then(res => res.json())
      .then(user => {
        setUser(user);
        setLoading(false);
      });
  }, [userId]); // Re-run when userId changes
  
  if (loading) return <div>Loading user...</div>;
  return <div>{user?.name}</div>;
}

// Cleanup effect (componentWillUnmount)
function Timer() {
  const [seconds, setSeconds] = useState(0);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);
    
    // Cleanup function - runs before component unmounts
    return () => {
      clearInterval(interval);
    };
  }, []); // Empty array = setup once, cleanup on unmount
  
  return <div>Timer: {seconds} seconds</div>;
}

// Effect with cleanup and dependencies
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();
    connection.on('message', (msg) => {
      setMessages(prev => [...prev, msg]);
    });
    
    // Cleanup on unmount or before re-run
    return () => {
      connection.disconnect();
    };
  }, [roomId]); // Re-run when roomId changes
  
  return <div>{messages.length} messages</div>;
}

// Multiple effects - each for different purposes
function ComplexComponent() {
  const [count, setCount] = useState(0);
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);
  
  // Effect 1: Update document title
  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);
  
  // Effect 2: Track window resize
  useEffect(() => {
    const handleResize = () => setWindowWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    
    return () => window.removeEventListener('resize', handleResize);
  }, []); // Empty array = run once
  
  // Effect 3: Log count changes
  useEffect(() => {
    console.log(`Count changed to ${count}`);
  }, [count]);
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Window width: {windowWidth}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

// Avoiding infinite loops
function AvoidInfiniteLoop() {
  const [data, setData] = useState([]);
  const [filter, setFilter] = useState('');
  
  // ❌ This would cause infinite loop
  // useEffect(() => {
  //   setData(fetchData(filter)); // Updates state -> re-run effect
  // }, [data, filter]); // data changes -> effect runs again
  
  // ✅ Correct way
  useEffect(() => {
    fetchData(filter).then(setData);
  }, [filter]); // Only re-run when filter changes
  
  return <div>{/* render data */}</div>;
}

// Conditional effects
function ConditionalEffect({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    if (!userId) return; // Skip if no userId
    
    let isMounted = true; // Flag to prevent state updates after unmount
    
    fetchUser(userId).then(user => {
      if (isMounted) {
        setUser(user);
      }
    });
    
    return () => {
      isMounted = false; // Cleanup
    };
  }, [userId]);
  
  return <div>{user?.name}</div>;
}

export { BasicEffect, DataFetcher, UserProfile, Timer, ChatRoom, ComplexComponent };

useContext – Managing Global State

useContext provides a way to pass data through the component tree without prop drilling. It works with React's Context API.

React JSXRead-only
1
import { createContext, useContext, useState } from 'react';

// 1. Create context
const ThemeContext = createContext();
const UserContext = createContext();
const LanguageContext = createContext();

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

// 3. Custom hook for consuming context
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// 4. Components consuming context
function ThemedButton() {
  const { theme, toggleTheme } = useTheme();
  
  const styles = {
    light: { backgroundColor: '#fff', color: '#000', border: '1px solid #ccc' },
    dark: { backgroundColor: '#333', color: '#fff', border: '1px solid #666' }
  };
  
  return (
    <button style={styles[theme]} onClick={toggleTheme}>
      Current theme: {theme}
    </button>
  );
}

function ThemedPanel() {
  const { theme } = useTheme();
  
  const styles = {
    light: { backgroundColor: '#f5f5f5', color: '#000', padding: '20px' },
    dark: { backgroundColor: '#222', color: '#fff', padding: '20px' }
  };
  
  return <div style={styles[theme]}>This panel uses the {theme} theme</div>;
}

// Multiple contexts
function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  );
}

function LanguageProvider({ children }) {
  const [language, setLanguage] = useState('en');
  
  const translations = {
    en: { greeting: 'Hello', button: 'Click me' },
    es: { greeting: 'Hola', button: 'Haz clic' },
    fr: { greeting: 'Bonjour', button: 'Cliquez' }
  };
  
  return (
    <LanguageContext.Provider value={{ language, setLanguage, t: translations[language] }}>
      {children}
    </LanguageContext.Provider>
  );
}

function useUser() {
  return useContext(UserContext);
}

function useLanguage() {
  return useContext(LanguageContext);
}

function Profile() {
  const { user } = useUser();
  const { t } = useLanguage();
  
  if (!user) return <div>Please log in</div>;
  
  return (
    <div>
      <p>{t.greeting}, {user.name}!</p>
    </div>
  );
}

function LanguageSelector() {
  const { language, setLanguage } = useLanguage();
  
  return (
    <select value={language} onChange={(e) => setLanguage(e.target.value)}>
      <option value="en">English</option>
      <option value="es">Español</option>
      <option value="fr">Français</option>
    </select>
  );
}

// App with multiple providers
function App() {
  return (
    <ThemeProvider>
      <UserProvider>
        <LanguageProvider>
          <div>
            <LanguageSelector />
            <ThemedButton />
            <ThemedPanel />
            <Profile />
          </div>
        </LanguageProvider>
      </UserProvider>
    </ThemeProvider>
  );
}

// Context with reducer pattern
const TodoContext = createContext();

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

function TodoProvider({ children }) {
  const [todos, dispatch] = useReducer(todoReducer, []);
  
  return (
    <TodoContext.Provider value={{ todos, dispatch }}>
      {children}
    </TodoContext.Provider>
  );
}

function useTodos() {
  return useContext(TodoContext);
}

function TodoList() {
  const { todos, dispatch } = useTodos();
  
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.text}
          </span>
          <button onClick={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}>
            Toggle
          </button>
          <button onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}>
            Delete
          </button>
        </li>
      ))}
    </ul>
  );
}

export { 
  ThemeProvider, useTheme, ThemedButton, ThemedPanel,
  UserProvider, LanguageProvider, useUser, useLanguage,
  Profile, LanguageSelector, TodoProvider, useTodos, TodoList,
  App
};

useReducer – Complex State Logic

useReducer is an alternative to useState for complex state logic that involves multiple sub-values or when the next state depends on the previous state.

React JSXRead-only
1
import { useReducer, useCallback } from 'react';

// Basic counter reducer
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 };
    case 'SET':
      return { count: action.payload };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });
  
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'SET', payload: 10 })}>Set to 10</button>
    </div>
  );
}

// Complex todo reducer
const todoReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, {
          id: Date.now(),
          text: action.payload,
          completed: false,
          createdAt: new Date()
        }]
      };
    
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      };
    
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload)
      };
    
    case 'EDIT_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload.id
            ? { ...todo, text: action.payload.text }
            : todo
        )
      };
    
    case 'CLEAR_COMPLETED':
      return {
        ...state,
        todos: state.todos.filter(todo => !todo.completed)
      };
    
    case 'SET_FILTER':
      return {
        ...state,
        filter: action.payload
      };
    
    default:
      return state;
  }
};

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, {
    todos: [],
    filter: 'all'
  });
  
  const [input, setInput] = useState('');
  
  const filteredTodos = state.todos.filter(todo => {
    if (state.filter === 'active') return !todo.completed;
    if (state.filter === 'completed') return todo.completed;
    return true;
  });
  
  const addTodo = () => {
    if (input.trim()) {
      dispatch({ type: 'ADD_TODO', payload: input });
      setInput('');
    }
  };
  
  return (
    <div>
      <input value={input} onChange={(e) => setInput(e.target.value)} />
      <button onClick={addTodo}>Add Todo</button>
      
      <div>
        <button onClick={() => dispatch({ type: 'SET_FILTER', payload: 'all' })}>All</button>
        <button onClick={() => dispatch({ type: 'SET_FILTER', payload: 'active' })}>Active</button>
        <button onClick={() => dispatch({ type: 'SET_FILTER', payload: 'completed' })}>Completed</button>
        <button onClick={() => dispatch({ type: 'CLEAR_COMPLETED' })}>Clear Completed</button>
      </div>
      
      <ul>
        {filteredTodos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
            <button onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}>
              Delete
            </button>
          </li>
        ))}
      </ul>
      
      <p>{state.todos.filter(t => !t.completed).length} items left</p>
    </div>
  );
}

// Form reducer example
const formReducer = (state, action) => {
  switch (action.type) {
    case 'CHANGE':
      return {
        ...state,
        [action.field]: action.value,
        errors: {
          ...state.errors,
          [action.field]: validateField(action.field, action.value)
        }
      };
    case 'TOUCH':
      return {
        ...state,
        touched: { ...state.touched, [action.field]: true }
      };
    case 'SUBMIT':
      return {
        ...state,
        isSubmitting: true
      };
    case 'SUCCESS':
      return {
        ...state,
        isSubmitting: false,
        submitted: true
      };
    case 'ERROR':
      return {
        ...state,
        isSubmitting: false,
        submitError: action.error
      };
    case 'RESET':
      return action.initialState;
    default:
      return state;
  }
};

function validateField(field, value) {
  switch (field) {
    case 'email':
      return !value ? 'Email is required' :
             !/\S+@\S+\.\S+/.test(value) ? 'Invalid email' : '';
    case 'password':
      return !value ? 'Password is required' :
             value.length < 6 ? 'Password must be at least 6 characters' : '';
    default:
      return '';
  }
}

function LoginForm() {
  const initialState = {
    email: '',
    password: '',
    errors: { email: '', password: '' },
    touched: {},
    isSubmitting: false,
    submitted: false,
    submitError: ''
  };
  
  const [state, dispatch] = useReducer(formReducer, initialState);
  
  const handleChange = (field, value) => {
    dispatch({ type: 'CHANGE', field, value });
  };
  
  const handleBlur = (field) => {
    dispatch({ type: 'TOUCH', field });
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    dispatch({ type: 'SUBMIT' });
    
    try {
      await login(state.email, state.password);
      dispatch({ type: 'SUCCESS' });
    } catch (error) {
      dispatch({ type: 'ERROR', error: error.message });
    }
  };
  
  if (state.submitted) {
    return <div>Login successful!</div>;
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="email"
          value={state.email}
          onChange={(e) => handleChange('email', e.target.value)}
          onBlur={() => handleBlur('email')}
          placeholder="Email"
        />
        {state.touched.email && state.errors.email && 
          <span style={{ color: 'red' }}>{state.errors.email}</span>}
      </div>
      
      <div>
        <input
          type="password"
          value={state.password}
          onChange={(e) => handleChange('password', e.target.value)}
          onBlur={() => handleBlur('password')}
          placeholder="Password"
        />
        {state.touched.password && state.errors.password && 
          <span style={{ color: 'red' }}>{state.errors.password}</span>}
      </div>
      
      {state.submitError && <div style={{ color: 'red' }}>{state.submitError}</div>}
      
      <button type="submit" disabled={state.isSubmitting}>
        {state.isSubmitting ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
}

// useState vs useReducer comparison
function CounterWithUseState() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

function CounterWithUseReducer() {
  const [state, dispatch] = useReducer(
    (state, action) => ({ count: state.count + action }),
    { count: 0 }
  );
  return <button onClick={() => dispatch(1)}>{state.count}</button>;
}

export { Counter, TodoApp, LoginForm, CounterWithUseState, CounterWithUseReducer };

useCallback & useMemo – Performance Optimization

React JSXRead-only
1
import { useState, useCallback, useMemo, memo } from 'react';

// useCallback - memoizes functions
function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');
  
  // Without useCallback - function recreated on every render
  const handleClickBad = () => {
    console.log('Button clicked');
  };
  
  // With useCallback - function is memoized
  const handleClickGood = useCallback(() => {
    console.log('Button clicked');
  }, []); // Empty deps = function never changes
  
  // With dependencies
  const handleIncrement = useCallback(() => {
    setCount(c => c + 1);
  }, []); // setCount is stable, so empty deps is fine
  
  const handleLogCount = useCallback(() => {
    console.log(`Current count: ${count}`);
  }, [count]); // Recreate when count changes
  
  return (
    <div>
      <button onClick={handleIncrement}>Count: {count}</button>
      <ChildComponent onClick={handleClickGood} /> {/* Won't re-render unnecessarily */}
      <input value={text} onChange={(e) => setText(e.target.value)} />
    </div>
  );
}

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

// useMemo - memoizes computed values
function ExpensiveComponent({ items, filterText }) {
  const [count, setCount] = useState(0);
  
  // Without useMemo - recalculates on every render
  const filteredItemsBad = items.filter(item => 
    item.name.toLowerCase().includes(filterText.toLowerCase())
  );
  
  // With useMemo - only recalculates when dependencies change
  const filteredItemsGood = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item => 
      item.name.toLowerCase().includes(filterText.toLowerCase())
    );
  }, [items, filterText]);
  
  // Another expensive calculation
  const statistics = useMemo(() => {
    console.log('Calculating statistics...');
    const total = items.reduce((sum, item) => sum + item.value, 0);
    const average = total / items.length;
    const max = Math.max(...items.map(item => item.value));
    const min = Math.min(...items.map(item => item.value));
    return { total, average, max, min };
  }, [items]);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Re-render count: {count}</button>
      <div>
        <h3>Statistics</h3>
        <p>Total: {statistics.total}</p>
        <p>Average: {statistics.average}</p>
        <p>Max: {statistics.max}</p>
        <p>Min: {statistics.min}</p>
      </div>
      <ul>
        {filteredItemsGood.map(item => (
          <li key={item.id}>{item.name}: {item.value}</li>
        ))}
      </ul>
    </div>
  );
}

// Real-world example: Search with debouncing
function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  
  // Memoize search function
  const searchAPI = useCallback(async (searchQuery) => {
    if (!searchQuery) return [];
    setLoading(true);
    const response = await fetch(`/api/search?q=${searchQuery}`);
    const data = await response.json();
    setLoading(false);
    return data;
  }, []);
  
  // Debounced search
  const debouncedSearch = useMemo(() => {
    let timeoutId;
    return (value) => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(async () => {
        const results = await searchAPI(value);
        setResults(results);
      }, 300);
    };
  }, [searchAPI]);
  
  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value);
    debouncedSearch(value);
  };
  
  return (
    <div>
      <input value={query} onChange={handleChange} placeholder="Search..." />
      {loading && <div>Loading...</div>}
      <ul>
        {results.map(result => (
          <li key={result.id}>{result.title}</li>
        ))}
      </ul>
    </div>
  );
}

// When to use useCallback vs useMemo
function ComparisonExample() {
  const [data, setData] = useState([]);
  
  // useCallback: memoizes the FUNCTION itself
  const processData = useCallback((item) => {
    return item * 2;
  }, []);
  
  // useMemo: memoizes the RESULT of the function
  const processedData = useMemo(() => {
    return data.map(processData);
  }, [data, processData]);
  
  // Don't over-optimize!
  // ❌ Unnecessary useMemo
  const simpleValue = useMemo(() => 42, []); // Just use const simpleValue = 42;
  
  // ❌ Unnecessary useCallback
  const simpleFunction = useCallback(() => {
    console.log('hello');
  }, []); // Just define the function normally
  
  // ✅ Use when:
  // 1. Function is passed to memoized child components
  // 2. Function is a dependency of useEffect
  // 3. Function is expensive to create (rare)
  
  return <div>{/* ... */}</div>;
}

export { ParentComponent, ExpensiveComponent, SearchComponent };

useRef – Accessing DOM and Persistent Values

React JSXRead-only
1
import { useRef, useState, useEffect } from 'react';

// Accessing DOM elements
function TextInputWithFocus() {
  const inputRef = useRef(null);
  
  const focusInput = () => {
    inputRef.current.focus();
  };
  
  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus Input</button>
    </div>
  );
}

// Measuring DOM elements
function MeasureExample() {
  const [height, setHeight] = useState(0);
  const divRef = useRef(null);
  
  useEffect(() => {
    if (divRef.current) {
      setHeight(divRef.current.clientHeight);
    }
  }, []); // Run once after mount
  
  return (
    <div>
      <div ref={divRef} style={{ padding: '20px', background: '#f0f0f0' }}>
        <h3>Measurable content</h3>
        <p>This div's height is being measured</p>
      </div>
      <p>Div height: {height}px</p>
    </div>
  );
}

// Storing mutable values (doesn't trigger re-render)
function TimerWithRef() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null);
  
  const startTimer = () => {
    if (intervalRef.current) return;
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
  };
  
  const stopTimer = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
  };
  
  useEffect(() => {
    return () => stopTimer(); // Cleanup on unmount
  }, []);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={startTimer}>Start</button>
      <button onClick={stopTimer}>Stop</button>
    </div>
  );
}

// Keeping previous values
function usePrevious(value) {
  const ref = useRef();
  
  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref.current;
}

function CounterWithPrevious() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  
  return (
    <div>
      <p>Now: {count}, before: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

// Storing instance variables (like this in class components)
function FormWithRefs() {
  const [formData, setFormData] = useState({ name: '', email: '' });
  const submitCountRef = useRef(0);
  const startTimeRef = useRef(Date.now());
  
  const handleSubmit = (e) => {
    e.preventDefault();
    submitCountRef.current++;
    console.log(`Submit count: ${submitCountRef.current}`);
    console.log(`Time on page: ${Date.now() - startTimeRef.current}ms`);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        value={formData.name}
        onChange={(e) => setFormData({ ...formData, name: e.target.value })}
      />
      <input
        value={formData.email}
        onChange={(e) => setFormData({ ...formData, email: e.target.value })}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

// Combining refs (forwarding refs)
const FancyInput = React.forwardRef((props, ref) => {
  const inputRef = useRef();
  
  // Expose multiple methods via ref
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    blur: () => inputRef.current.blur(),
    setValue: (value) => { inputRef.current.value = value; },
    getValue: () => inputRef.current.value
  }));
  
  return <input ref={inputRef} {...props} />;
});

function ParentWithFancyInput() {
  const fancyInputRef = useRef();
  
  return (
    <div>
      <FancyInput ref={fancyInputRef} placeholder="Type here..." />
      <button onClick={() => fancyInputRef.current.focus()}>Focus</button>
      <button onClick={() => fancyInputRef.current.setValue('Hello!')}>Set Value</button>
      <button onClick={() => alert(fancyInputRef.current.getValue())}>Get Value</button>
    </div>
  );
}

// useRef vs useState comparison
function Comparison() {
  const [stateCount, setStateCount] = useState(0);
  const refCount = useRef(0);
  
  const incrementState = () => {
    setStateCount(stateCount + 1); // Triggers re-render
  };
  
  const incrementRef = () => {
    refCount.current++; // Does NOT trigger re-render
    console.log(`Ref value: ${refCount.current}`);
  };
  
  return (
    <div>
      <p>State: {stateCount} (re-renders on change)</p>
      <p>Ref: {refCount.current} (no re-render)</p>
      <button onClick={incrementState}>Update State</button>
      <button onClick={incrementRef}>Update Ref</button>
    </div>
  );
}

export { 
  TextInputWithFocus, MeasureExample, TimerWithRef, 
  CounterWithPrevious, FormWithRefs, ParentWithFancyInput,
  Comparison
};

Custom Hooks – Reusing Logic

React JSXRead-only
1
import { useState, useEffect, useCallback, useRef } from 'react';

// Hook for fetching data
function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const abortController = new AbortController();
    
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url, {
          ...options,
          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);
        }
      } finally {
        setLoading(false);
      }
    };
    
    fetchData();
    
    return () => abortController.abort();
  }, [url, JSON.stringify(options)]);
  
  return { data, loading, error };
}

// Usage
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
  
  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>{user?.name}</div>;
}

// Hook for local storage
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.log(error);
      return initialValue;
    }
  });
  
  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.log(error);
    }
  };
  
  return [storedValue, setValue];
}

// Usage
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Current theme: {theme}
    </button>
  );
}

// Hook for window size
function useWindowSize() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });
  
  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    
    window.addEventListener('resize', handleResize);
    handleResize(); // Call immediately
    
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  
  return windowSize;
}

// Hook for media query
function useMediaQuery(query) {
  const [matches, setMatches] = useState(false);
  
  useEffect(() => {
    const media = window.matchMedia(query);
    if (media.matches !== matches) {
      setMatches(media.matches);
    }
    
    const listener = (e) => setMatches(e.matches);
    media.addEventListener('change', listener);
    
    return () => media.removeEventListener('change', listener);
  }, [query, matches]);
  
  return matches;
}

// Hook for click outside
function useClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) {
        return;
      }
      handler(event);
    };
    
    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);
    
    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
    };
  }, [ref, handler]);
}

// Hook for debouncing
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => clearTimeout(handler);
  }, [value, delay]);
  
  return debouncedValue;
}

// Hook for previous value
function usePrevious(value) {
  const ref = useRef();
  
  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref.current;
}

// Hook for toggle
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = useCallback(() => setValue(v => !v), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);
  
  return { value, toggle, setTrue, setFalse };
}

// Hook for counter
function useCounter(initialValue = 0, step = 1) {
  const [count, setCount] = useState(initialValue);
  
  const increment = useCallback(() => setCount(c => c + step), [step]);
  const decrement = useCallback(() => setCount(c => c - step), [step]);
  const reset = useCallback(() => setCount(initialValue), [initialValue]);
  const set = useCallback((value) => setCount(value), []);
  
  return { count, increment, decrement, reset, set };
}

// Hook for form handling
function useForm(initialValues, validate) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  
  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    const newValue = type === 'checkbox' ? checked : value;
    
    setValues(prev => ({ ...prev, [name]: newValue }));
    
    if (validate) {
      const error = validate(name, newValue);
      setErrors(prev => ({ ...prev, [name]: error }));
    }
  };
  
  const handleBlur = (e) => {
    const { name } = e.target;
    setTouched(prev => ({ ...prev, [name]: true }));
  };
  
  const handleSubmit = (callback) => (e) => {
    e.preventDefault();
    
    // Validate all fields
    if (validate) {
      const newErrors = {};
      Object.keys(values).forEach(key => {
        const error = validate(key, values[key]);
        if (error) newErrors[key] = error;
      });
      
      setErrors(newErrors);
      setTouched(Object.keys(values).reduce((acc, key) => ({ ...acc, [key]: true }), {}));
      
      if (Object.keys(newErrors).length === 0) {
        callback(values);
      }
    } else {
      callback(values);
    }
  };
  
  const reset = () => {
    setValues(initialValues);
    setErrors({});
    setTouched({});
  };
  
  return {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    handleSubmit,
    reset
  };
}

// Complete example using multiple custom hooks
function SearchWithHooks() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500);
  const { data: results, loading } = useFetch(
    debouncedSearchTerm ? `/api/search?q=${debouncedSearchTerm}` : null
  );
  const isMobile = useMediaQuery('(max-width: 768px)');
  
  return (
    <div>
      <input
        type="text"
        placeholder="Search..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        style={{ width: isMobile ? '100%' : '300px' }}
      />
      {loading && <div>Searching...</div>}
      {results && (
        <ul>
          {results.map(result => (
            <li key={result.id}>{result.title}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

export {
  useFetch, useLocalStorage, useWindowSize, useMediaQuery,
  useClickOutside, useDebounce, usePrevious, useToggle,
  useCounter, useForm, SearchWithHooks
};

Additional Built-in Hooks

React JSXRead-only
1
import { 
  useImperativeHandle, 
  useLayoutEffect, 
  useDebugValue,
  useId,
  useTransition,
  useDeferredValue,
  useSyncExternalStore
} from 'react';

// useId - Generate unique IDs for accessibility
function FormFields() {
  const id = useId();
  const emailId = `${id}-email`;
  const passwordId = `${id}-password`;
  
  return (
    <div>
      <label htmlFor={emailId}>Email:</label>
      <input id={emailId} type="email" />
      
      <label htmlFor={passwordId}>Password:</label>
      <input id={passwordId} type="password" />
    </div>
  );
}

// useTransition - Mark updates as non-urgent
function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();
  
  const handleTabChange = (newTab) => {
    startTransition(() => {
      setTab(newTab);
    });
  };
  
  return (
    <div>
      <button onClick={() => handleTabChange('home')}>Home</button>
      <button onClick={() => handleTabChange('profile')}>Profile</button>
      {isPending && <div>Loading...</div>}
      <SlowComponent tab={tab} />
    </div>
  );
}

// useDeferredValue - Defer updating a value
function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;
  
  return (
    <div style={{ opacity: isStale ? 0.5 : 1 }}>
      <ExpensiveSearchResults query={deferredQuery} />
    </div>
  );
}

// useLayoutEffect - Runs synchronously after DOM mutations
function MeasureElement() {
  const [height, setHeight] = useState(0);
  const ref = useRef();
  
  useLayoutEffect(() => {
    // Runs before browser paints
    setHeight(ref.current.clientHeight);
  }, []);
  
  return (
    <div ref={ref}>
      <p>Height: {height}px</p>
    </div>
  );
}

// useDebugValue - Label custom hooks in React DevTools
function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
  
  useDebugValue(isOnline ? 'Online' : 'Offline');
  useDebugValue(friendID, id => `Friend ${id}`);
  
  // ... hook logic
  return isOnline;
}

// useSyncExternalStore - Subscribe to external stores
function useOnlineStatus() {
  const isOnline = useSyncExternalStore(
    (callback) => {
      window.addEventListener('online', callback);
      window.addEventListener('offline', callback);
      return () => {
        window.removeEventListener('online', callback);
        window.removeEventListener('offline', callback);
      };
    },
    () => navigator.onLine,
    () => true // Server snapshot
  );
  
  return isOnline;
}

export { FormFields, TabContainer, SearchResults, MeasureElement, useFriendStatus, useOnlineStatus };

Hooks Best Practices

  • Follow the Rules of Hooks – Only call hooks at the top level, only call hooks from React functions
  • Use ESLint Plugin – eslint-plugin-react-hooks helps enforce rules and best practices
  • Keep hooks focused – Each hook should have a single responsibility
  • Extract custom hooks – Reuse logic by creating custom hooks instead of duplicating code
  • Name custom hooks with 'use' prefix – This helps identify hooks and enables linting rules
  • Minimize useEffect dependencies – Only include variables that the effect actually uses
  • Clean up effects – Always return cleanup functions from useEffect to prevent memory leaks
  • Avoid infinite loops – Be careful with state updates in useEffect that cause re-runs
  • Use functional updates – When new state depends on previous state, use the functional form
  • Memoize expensive operations – Use useMemo and useCallback appropriately
  • Don't over-optimize – Not everything needs memoization; measure performance first
  • Keep components pure – Avoid side effects in render phase
  • Use useReducer for complex state – When state logic is complex, useReducer is more maintainable
  • Lift state when needed – Share state by lifting it to common ancestors
  • Use Context for global state – For truly global state (theme, user), useContext is great
  • Test hooks – Use React Hooks Testing Library to test custom hooks
  • Document custom hooks – Add JSDoc comments to explain parameters and return values
  • Version hooks – When sharing hooks, use semantic versioning
  • Avoid large dependency arrays – Break effects into smaller, focused effects
  • Use useRef for mutable values – When you need to persist values without causing re-renders

Common Hook Mistakes

  • ❌ Calling hooks conditionally – Hooks must be called in the same order every render
  • ❌ Missing dependencies in useEffect – Always include all variables used in the effect
  • ❌ Creating infinite loops – Updating state that's a dependency of the same effect
  • ❌ Not cleaning up effects – Forgetting to clear intervals, subscriptions, or event listeners
  • ❌ Overusing useState – Using multiple useState calls when an object would be cleaner
  • ❌ Directly mutating state – Always use the state setter function
  • ❌ Stale closures – Using state values that are outdated in callbacks or effects
  • ❌ Not using functional updates – When new state depends on previous state
  • ❌ Premature optimization – Using useMemo/useCallback everywhere unnecessarily
  • ❌ Using useRef for state that should trigger re-renders – useRef changes don't cause re-renders
  • ❌ Setting state in useEffect without conditions – Can cause unnecessary re-renders
  • ❌ Not handling loading/error states – Always consider async operations
  • ❌ Prop drilling with hooks – Using hooks to pass data through many levels instead of Context
  • ❌ Creating new objects in dependencies – Object literals break dependency comparison
  • ❌ Calling hooks in event handlers – Hooks can only be called during render
  • ❌ Ignoring abort controllers – Forgetting to cancel fetch requests on unmount
  • ❌ Using useEffect for derived state – Computed values don't need useEffect
  • ❌ Not extracting custom hooks – Duplicating hook logic across components

Hooks Comparison Table

HookPurposeUse CaseClass Equivalent
useStateLocal state managementSimple values, counters, togglesthis.state & this.setState
useEffectSide effectsData fetching, subscriptions, DOM updatescomponentDidMount, componentDidUpdate, componentWillUnmount
useContextGlobal stateTheme, user, language preferencesContext.Consumer
useReducerComplex state logicForms, todo lists, state machinesthis.state with complex updates
useCallbackMemoize functionsPreventing unnecessary re-rendersClass methods with binding
useMemoMemoize valuesExpensive calculationsManual memoization
useRefMutable referencesDOM access, instance variablescreateRef, instance properties
useImperativeHandleCustom ref methodsExposing specific methodsForwarding refs
useLayoutEffectSynchronous effectsDOM measurements, mutationscomponentDidMount/Update (sync)
useDebugValueDevTools labelingCustom hook debuggingN/A
useIdUnique IDsAccessibility, form labelsManual ID generation
useTransitionNon-urgent updatesDeferred UI updatesN/A
useDeferredValueDefer value updatesOptimistic UI, searchN/A
useSyncExternalStoreExternal store subscriptionThird-party storesN/A

Migration from Class to Hooks

React JSXRead-only
1
// Class component
class ClassCounter extends React.Component {
  state = { count: 0 };
  
  componentDidMount() {
    console.log('Mounted');
  }
  
  componentDidUpdate(prevProps, prevState) {
    if (prevState.count !== this.state.count) {
      console.log('Count changed');
    }
  }
  
  componentWillUnmount() {
    console.log('Unmounted');
  }
  
  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };
  
  render() {
    return <button onClick={this.increment}>{this.state.count}</button>;
  }
}

// Same functionality with hooks
function FunctionalCounter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    console.log('Mounted');
    return () => console.log('Unmounted');
  }, []);
  
  useEffect(() => {
    console.log('Count changed');
  }, [count]);
  
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

// Class with multiple lifecycle methods
class ClassDataFetcher extends React.Component {
  state = { data: null, loading: true, error: null };
  
  componentDidMount() {
    this.fetchData();
  }
  
  componentDidUpdate(prevProps) {
    if (prevProps.userId !== this.props.userId) {
      this.fetchData();
    }
  }
  
  fetchData = async () => {
    try {
      this.setState({ loading: true });
      const response = await fetch(`/api/users/${this.props.userId}`);
      const data = await response.json();
      this.setState({ data, loading: false });
    } catch (error) {
      this.setState({ error, loading: false });
    }
  };
  
  render() {
    // ... render logic
  }
}

// Same with hooks
function FunctionalDataFetcher({ userId }) {
  const [state, setState] = useState({
    data: null,
    loading: true,
    error: null
  });
  
  useEffect(() => {
    let isMounted = true;
    
    const fetchData = async () => {
      try {
        setState(prev => ({ ...prev, loading: true }));
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        if (isMounted) {
          setState({ data, loading: false, error: null });
        }
      } catch (error) {
        if (isMounted) {
          setState({ data: null, loading: false, error });
        }
      }
    };
    
    fetchData();
    
    return () => { isMounted = false; };
  }, [userId]);
  
  // ... render logic
}

export { ClassCounter, FunctionalCounter, ClassDataFetcher, FunctionalDataFetcher };

Conclusion

React Hooks have revolutionized how we write React applications by enabling functional components to have state and lifecycle features. They provide a more direct API to React concepts you already know: props, state, context, refs, and lifecycle. Hooks make code more reusable, composable, and easier to understand. Start with useState and useEffect, then gradually incorporate other hooks as needed. Remember to follow the Rules of Hooks and extract custom hooks when you find yourself duplicating logic. With hooks, you can build modern, performant React applications with cleaner and more maintainable code.

Try it yourself

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

// Counter component using useState
function Counter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);
  
  return (
    <div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px', margin: '10px' }}>
      <h3>useState Example</h3>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + step)}>+{step}</button>
      <button onClick={() => setCount(count - step)}>-{step}</button>
      <button onClick={() => setCount(0)}>Reset</button>
      <div style={{ marginTop: '10px' }}>
        <label>Step: </label>
        <input 
          type="number" 
          value={step} 
          onChange={(e) => setStep(Number(e.target.value))}
          style={{ marginLeft: '10px', padding: '5px' }}
        />
      </div>
    </div>
  );
}

// Data fetching with useEffect
function DataFetcher() {
  const [userId, setUserId] = useState(1);
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true);
      try {
        const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
        const data = await response.json();
        setUser(data);
      } catch (error) {
        console.error('Error fetching user:', error);
      } finally {
        setLoading(false);
      }
    };
    
    fetchUser();
  }, [userId]);
  
  return (
    <div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px', margin: '10px' }}>
      <h3>useEffect Example - Data Fetching</h3>
      <div>
        <button onClick={() => setUserId(1)}>User 1</button>
        <button onClick={() => setUserId(2)}>User 2</button>
        <button onClick={() => setUserId(3)}>User 3</button>
      </div>
      {loading && <p>Loading...</p>}
      {user && (
        <div style={{ marginTop: '10px' }}>
          <h4>{user.name}</h4>
          <p>Email: {user.email}</p>
          <p>Phone: {user.phone}</p>
        </div>
      )}
    </div>
  );
}

// Performance optimization with useMemo and useCallback
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React Hooks', completed: false },
    { id: 2, text: 'Build a project', completed: false },
    { id: 3, text: 'Master React', completed: false }
  ]);
  const [filter, setFilter] = useState('all');
  const [newTodo, setNewTodo] = useState('');
  
  // Memoize filtered todos
  const filteredTodos = useMemo(() => {
    console.log('Filtering todos...');
    switch(filter) {
      case 'active':
        return todos.filter(todo => !todo.completed);
      case 'completed':
        return todos.filter(todo => todo.completed);
      default:
        return todos;
    }
  }, [todos, filter]);
  
  // Memoize callbacks
  const addTodo = useCallback(() => {
    if (newTodo.trim()) {
      setTodos(prev => [...prev, {
        id: Date.now(),
        text: newTodo,
        completed: false
      }]);
      setNewTodo('');
    }
  }, [newTodo]);
  
  const toggleTodo = useCallback((id) => {
    setTodos(prev => prev.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  }, []);
  
  const deleteTodo = useCallback((id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []);
  
  const stats = useMemo(() => {
    const total = todos.length;
    const completed = todos.filter(t => t.completed).length;
    const active = total - completed;
    return { total, completed, active };
  }, [todos]);
  
  return (
    <div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px', margin: '10px' }}>
      <h3>useMemo & useCallback Example - Todo List</h3>
      
      <div style={{ marginBottom: '10px' }}>
        <input
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="Add new todo..."
          style={{ padding: '5px', marginRight: '10px' }}
        />
        <button onClick={addTodo}>Add Todo</button>
      </div>
      
      <div style={{ marginBottom: '10px' }}>
        <button onClick={() => setFilter('all')}>All</button>
        <button onClick={() => setFilter('active')}>Active</button>
        <button onClick={() => setFilter('completed')}>Completed</button>
      </div>
      
      <div style={{ marginBottom: '10px' }}>
        <p>Stats: Total: {stats.total} | Active: {stats.active} | Completed: {stats.completed}</p>
      </div>
      
      <ul>
        {filteredTodos.map(todo => (
          <li key={todo.id} style={{ marginBottom: '5px' }}>
            <span
              style={{
                textDecoration: todo.completed ? 'line-through' : 'none',
                cursor: 'pointer',
                marginRight: '10px'
              }}
              onClick={() => toggleTodo(todo.id)}
            >
              {todo.text}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
      
      {filteredTodos.length === 0 && <p>No todos found</p>}
    </div>
  );
}

// Custom hook example
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });
  
  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  
  return size;
}

function ResponsiveComponent() {
  const { width, height } = useWindowSize();
  
  return (
    <div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px', margin: '10px' }}>
      <h3>Custom Hook Example - useWindowSize</h3>
      <p>Window width: {width}px</p>
      <p>Window height: {height}px</p>
      <p>Breakpoint: {width < 768 ? 'Mobile' : width < 1024 ? 'Tablet' : 'Desktop'}</p>
    </div>
  );
}

// Main App
function App() {
  return (
    <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
      <h1>React Hooks Interactive Demo</h1>
      <p>Explore different React Hooks with these interactive examples:</p>
      
      <Counter />
      <DataFetcher />
      <TodoList />
      <ResponsiveComponent />
      
      <div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px', margin: '10px', background: '#f9f9f9' }}>
        <h3>Key Takeaways:</h3>
        <ul>
          <li>✅ useState manages local state</li>
          <li>✅ useEffect handles side effects</li>
          <li>✅ useMemo memoizes expensive calculations</li>
          <li>✅ useCallback memoizes functions</li>
          <li>✅ Custom hooks reuse logic across components</li>
        </ul>
      </div>
    </div>
  );
}

export default App;

Test Your Knowledge

Q1
of 10

What is the correct way to declare state using useState?

A
const state = useState(0);
B
const [state, setState] = useState(0);
C
this.state = useState(0);
D
useState(0);
Q2
of 10

Which hook is used for side effects like data fetching?

A
useState
B
useContext
C
useEffect
D
useReducer
Q3
of 10

How do you prevent useEffect from running on every render?

A
Return false from useEffect
B
Use an empty dependency array []
C
Call useEffect only once
D
Use useMemo instead
Q4
of 10

What hook would you use to memoize an expensive calculation?

A
useCallback
B
useMemo
C
useEffect
D
useRef
Q5
of 10

Which hook allows you to access DOM elements directly?

A
useDom
B
useElement
C
useRef
D
useLayoutEffect
Q6
of 10

What are the Rules of Hooks?

A
Only call hooks from React functions and at the top level
B
Hooks can be called conditionally
C
Hooks can only be used in class components
D
Hooks must return a value
Q7
of 10

When should you use useReducer instead of useState?

A
For all state management
B
For complex state logic with multiple sub-values
C
Only for forms
D
Never, useState is always better
Q8
of 10

What is the difference between useEffect and useLayoutEffect?

A
No difference, they're identical
B
useEffect runs after paint, useLayoutEffect runs before paint
C
useLayoutEffect runs after paint, useEffect runs before paint
D
useEffect is for class components only
Q9
of 10

What hook would you use to access previous state or props?

A
usePrevious
B
useRef
C
useState
D
useCallback
Q10
of 10

What prefix should custom hooks start with?

A
hook
B
custom
C
use
D
react

Frequently Asked Questions

What are React Hooks and why were they introduced?

React Hooks are functions that let you use state and other React features in functional components. They were introduced to solve problems like wrapper hell, complex components, and confusing classes. Hooks make it easier to reuse stateful logic, organize code by related functionality, and learn React without mastering classes.

What are the Rules of Hooks?

Two main rules: 1) Only call hooks at the top level of your component or custom hook (not inside loops, conditions, or nested functions). 2) Only call hooks from React function components or custom hooks (not from regular JavaScript functions). The ESLint plugin eslint-plugin-react-hooks helps enforce these rules.

What's the difference between useEffect and useLayoutEffect?

useEffect runs asynchronously after the browser paints, so it doesn't block visual updates. useLayoutEffect runs synchronously after DOM mutations but before the browser paints, which is useful for measuring DOM elements but can block rendering. Prefer useEffect unless you need to measure or mutate DOM before painting.

When should I use useReducer instead of useState?

Use useReducer when you have complex state logic with multiple sub-values, when the next state depends on the previous state, or when you're managing state objects that have multiple transitions. useReducer also helps keep related state logic together and is easier to test.

How do I prevent infinite loops with useEffect?

Infinite loops happen when useEffect updates state that's in its dependency array. To prevent them: 1) Ensure dependency arrays include only needed variables, 2) Use functional updates when new state depends on previous state, 3) Move non-dependencies outside the effect, 4) Use useCallback for functions passed as dependencies.

What's the difference between useCallback and useMemo?

useCallback memoizes the function itself, returning the same function instance across renders unless dependencies change. useMemo memoizes the result of calling a function, returning the same computed value across renders unless dependencies change. Use useCallback for functions passed to memoized children, useMemo for expensive calculations.

When should I use useRef instead of useState?

Use useRef when you need to store mutable values that shouldn't cause re-renders when changed, like interval IDs, previous values, or DOM references. Use useState when changes should trigger re-renders and update the UI. Changing useRef.current doesn't cause re-renders.

How do I create a custom hook?

Create a function that starts with 'use' and calls other hooks inside it. Custom hooks let you extract component logic into reusable functions. They can accept parameters and return any values. Example: function useWindowSize() { const [size, setSize] = useState({width: window.innerWidth}); useEffect(() => { const handler = () => setSize({width: window.innerWidth}); window.addEventListener('resize', handler); return () => window.removeEventListener('resize', handler); }, []); return size; }

Previous

react forms

Next

react use state

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.