React Hooks Deep Dive: useState, useEffect, and Custom Hooks
React Hooks Deep Dive: useState, useEffect, and Custom HooksMarch 15, 2024

React Hooks Deep Dive: useState, useEffect, and Custom Hooks

Master React Hooks with this comprehensive guide covering the most essential hooks and how to create powerful custom hooks. Learn common patterns, best practices, and performance optimization techniques.

Table of Contents

  1. Introduction to Hooks
  2. useState Hook
  3. useEffect Hook
  4. useContext Hook
  5. useReducer Hook
  6. Custom Hooks
  7. Performance Optimization
  8. Common Patterns
  9. Best Practices
  10. Troubleshooting

Introduction to Hooks {#introduction}

React Hooks were introduced in React 16.8 as a way to use state and other React features in functional components. They allow you to "hook into" React state and lifecycle methods without writing class components.

Why Hooks?

  • Simpler Code: Less boilerplate than class components
  • Better Logic Reuse: Custom hooks enable logic sharing between components
  • Easier Testing: Pure functions are easier to test
  • Better Performance: Hooks can help avoid unnecessary re-renders

Rules of Hooks

  1. Only call hooks at the top level - Don't call hooks inside loops, conditions, or nested functions
  2. Only call hooks from React functions - Call them from React functional components or custom hooks
// ❌ Wrong - calling hooks conditionally
function MyComponent({ condition }) {
  if (condition) {
    const [count, setCount] = useState(0); // This will break!
  }
  return <div>...</div>;
}

// ✅ Correct - hooks at top level
function MyComponent({ condition }) {
  const [count, setCount] = useState(0);
  
  if (condition) {
    // Use the state here instead
  }
  return <div>...</div>;
}

useState Hook {#usestate}

The useState hook lets you add state to functional components. It returns an array with the current state value and a function to update it.

Basic Usage

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>
  );
}

State with Objects

function UserProfile() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0
  });

  const updateName = (name) => {
    setUser(prevUser => ({
      ...prevUser,
      name: name
    }));
  };

  const updateUser = (updates) => {
    setUser(prevUser => ({
      ...prevUser,
      ...updates
    }));
  };

  return (
    <form>
      <input
        value={user.name}
        onChange={(e) => updateName(e.target.value)}
        placeholder="Name"
      />
      <input
        value={user.email}
        onChange={(e) => updateUser({ email: e.target.value })}
        placeholder="Email"
      />
    </form>
  );
}

State with Arrays

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [inputValue, setInputValue] = useState('');

  const addTodo = () => {
    if (inputValue.trim()) {
      setTodos(prevTodos => [
        ...prevTodos,
        {
          id: Date.now(),
          text: inputValue,
          completed: false
        }
      ]);
      setInputValue('');
    }
  };

  const toggleTodo = (id) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const deleteTodo = (id) => {
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
  };

  return (
    <div>
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && addTodo()}
      />
      <button onClick={addTodo}>Add Todo</button>
      
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <span
              style={{ 
                textDecoration: todo.completed ? 'line-through' : 'none' 
              }}
              onClick={() => toggleTodo(todo.id)}
            >
              {todo.text}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Lazy Initial State

When the initial state is expensive to compute, you can pass a function to useState:

function ExpensiveComponent() {
  // ❌ This runs on every render
  const [data, setData] = useState(expensiveComputation());

  // ✅ This only runs once
  const [data, setData] = useState(() => expensiveComputation());

  return <div>{data}</div>;
}

function expensiveComputation() {
  console.log('Computing...');
  // Simulate expensive computation
  return Array.from({ length: 1000 }, (_, i) => i);
}

useEffect Hook {#useeffect}

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

Basic Usage

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

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

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

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

Effect with Cleanup

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(prevSeconds => prevSeconds + 1);
    }, 1000);

    // Cleanup function (like componentWillUnmount)
    return () => {
      clearInterval(interval);
    };
  }, []); // Empty dependency array means this effect runs once

  return <div>Seconds: {seconds}</div>;
}

Conditional Effects

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!userId) return;

    setLoading(true);
    fetchUser(userId)
      .then(userData => {
        setUser(userData);
        setLoading(false);
      })
      .catch(error => {
        console.error('Error fetching user:', error);
        setLoading(false);
      });
  }, [userId]); // Effect runs when userId changes

  if (loading) return <div>Loading...</div>;
  if (!user) return <div>No user found</div>;

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

async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

Multiple Effects

function ChatRoom({ roomId, userId }) {
  const [messages, setMessages] = useState([]);
  const [onlineUsers, setOnlineUsers] = useState([]);

  // Effect for subscribing to messages
  useEffect(() => {
    const messageListener = (message) => {
      setMessages(prevMessages => [...prevMessages, message]);
    };

    ChatAPI.subscribeToMessages(roomId, messageListener);
    
    return () => {
      ChatAPI.unsubscribeFromMessages(roomId, messageListener);
    };
  }, [roomId]);

  // Effect for subscribing to online users
  useEffect(() => {
    const userListener = (users) => {
      setOnlineUsers(users);
    };

    ChatAPI.subscribeToOnlineUsers(roomId, userListener);
    
    return () => {
      ChatAPI.unsubscribeFromOnlineUsers(roomId, userListener);
    };
  }, [roomId]);

  // Effect for updating user status
  useEffect(() => {
    ChatAPI.updateUserStatus(userId, 'online');
    
    return () => {
      ChatAPI.updateUserStatus(userId, 'offline');
    };
  }, [userId]);

  return (
    <div>
      <div>Online Users: {onlineUsers.length}</div>
      <div>
        {messages.map(message => (
          <div key={message.id}>{message.text}</div>
        ))}
      </div>
    </div>
  );
}

useContext Hook {#usecontext}

The useContext hook provides a way to pass data through the component tree without manually passing props at every level.

Creating and Using Context

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

// Create context
const ThemeContext = createContext();
const UserContext = createContext();

// Theme Provider Component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

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

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

// User Provider Component
function UserProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = (userData) => {
    setUser(userData);
  };

  const logout = () => {
    setUser(null);
  };

  return (
    <UserContext.Provider value={{ user, login, logout }}>
      {children}
    </UserContext.Provider>
  );
}

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

function useUser() {
  const context = useContext(UserContext);
  if (!context) {
    throw new Error('useUser must be used within a UserProvider');
  }
  return context;
}

// Component using the context
function Header() {
  const { theme, toggleTheme } = useTheme();
  const { user, logout } = useUser();

  return (
    <header style={{ 
      backgroundColor: theme === 'light' ? 'white' : 'black',
      color: theme === 'light' ? 'black' : 'white'
    }}>
      <h1>My App</h1>
      <button onClick={toggleTheme}>
        Switch to {theme === 'light' ? 'dark' : 'light'} theme
      </button>
      {user ? (
        <div>
          <span>Welcome, {user.name}!</span>
          <button onClick={logout}>Logout</button>
        </div>
      ) : (
        <div>Please log in</div>
      )}
    </header>
  );
}

// App component
function App() {
  return (
    <ThemeProvider>
      <UserProvider>
        <Header />
        {/* Other components */}
      </UserProvider>
    </ThemeProvider>
  );
}

useReducer Hook {#usereducer}

The useReducer hook is an alternative to useState for managing complex state logic. It's similar to Redux reducers.

import React, { useReducer } from 'react';

// Reducer function
function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            id: Date.now(),
            text: action.payload,
            completed: false
          }
        ]
      };
    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 'SET_FILTER':
      return {
        ...state,
        filter: action.payload
      };
    default:
      return state;
  }
}

function TodoApp() {
  const initialState = {
    todos: [],
    filter: 'all' // 'all', 'active', 'completed'
  };

  const [state, dispatch] = useReducer(todoReducer, initialState);
  const [inputValue, setInputValue] = useState('');

  const addTodo = () => {
    if (inputValue.trim()) {
      dispatch({ type: 'ADD_TODO', payload: inputValue });
      setInputValue('');
    }
  };

  const filteredTodos = state.todos.filter(todo => {
    if (state.filter === 'active') return !todo.completed;
    if (state.filter === 'completed') return todo.completed;
    return true;
  });

  return (
    <div>
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && addTodo()}
      />
      <button onClick={addTodo}>Add Todo</button>
      
      <div>
        <button 
          onClick={() => dispatch({ type: 'SET_FILTER', payload: 'all' })}
          style={{ fontWeight: state.filter === 'all' ? 'bold' : 'normal' }}
        >
          All
        </button>
        <button 
          onClick={() => dispatch({ type: 'SET_FILTER', payload: 'active' })}
          style={{ fontWeight: state.filter === 'active' ? 'bold' : 'normal' }}
        >
          Active
        </button>
        <button 
          onClick={() => dispatch({ type: 'SET_FILTER', payload: 'completed' })}
          style={{ fontWeight: state.filter === 'completed' ? 'bold' : 'normal' }}
        >
          Completed
        </button>
      </div>

      <ul>
        {filteredTodos.map(todo => (
          <li key={todo.id}>
            <span
              style={{ 
                textDecoration: todo.completed ? 'line-through' : 'none',
                cursor: 'pointer'
              }}
              onClick={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
            >
              {todo.text}
            </span>
            <button 
              onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Custom Hooks {#custom-hooks}

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

useLocalStorage Hook

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  // Get from local storage then parse stored json or return initialValue
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  // Return a wrapped version of useState's setter function that persists the new value to localStorage
  const setValue = (value) => {
    try {
      // Allow value to be a function so we have the same API as useState
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  };

  return [storedValue, setValue];
}

// Usage
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [language, setLanguage] = useLocalStorage('language', 'en');

  return (
    <div>
      <label>
        Theme:
        <select value={theme} onChange={(e) => setTheme(e.target.value)}>
          <option value="light">Light</option>
          <option value="dark">Dark</option>
        </select>
      </label>
      
      <label>
        Language:
        <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>
      </label>
    </div>
  );
}

useFetch Hook

import { useState, useEffect } from 'react';

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);
        setError(null);
        
        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);
      } 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 UserList() {
  const { data: users, loading, error } = useFetch('/api/users');

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <ul>
      {users?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

useDebounce Hook

import { useState, useEffect } from 'react';

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

// Usage
function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500);
  const { data: results, loading } = useFetch(
    debouncedSearchTerm ? `/api/search?q=${debouncedSearchTerm}` : null
  );

  return (
    <div>
      <input
        type="text"
        placeholder="Search..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      
      {loading && <div>Searching...</div>}
      
      {results && (
        <ul>
          {results.map(result => (
            <li key={result.id}>{result.title}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

Performance Optimization {#performance}

useMemo Hook

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

function ExpensiveComponent({ items, filter }) {
  const [count, setCount] = useState(0);

  // Expensive computation that only runs when items or filter changes
  const filteredItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item => item.category === filter);
  }, [items, filter]);

  const expensiveValue = useMemo(() => {
    console.log('Computing expensive value...');
    return filteredItems.reduce((sum, item) => sum + item.value, 0);
  }, [filteredItems]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <p>Expensive Value: {expensiveValue}</p>
      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

useCallback Hook

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

const ChildComponent = memo(({ onClick, name }) => {
  console.log(`Rendering ${name}`);
  return <button onClick={onClick}>{name}</button>;
});

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

  // Without useCallback, this function is recreated on every render
  const handleClick1 = () => {
    console.log('Button 1 clicked');
  };

  // With useCallback, this function is only recreated when dependencies change
  const handleClick2 = useCallback(() => {
    console.log('Button 2 clicked', count);
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Other State: {otherState}</p>
      
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <button onClick={() => setOtherState(otherState + 1)}>Increment Other</button>
      
      {/* This child will re-render when parent re-renders due to new function reference */}
      <ChildComponent onClick={handleClick1} name="Button 1 (no useCallback)" />
      
      {/* This child will only re-render when count changes */}
      <ChildComponent onClick={handleClick2} name="Button 2 (with useCallback)" />
    </div>
  );
}

Common Patterns {#patterns}

Data Fetching Pattern

function useApi(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const execute = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      const response = await fetch(url);
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, [url]);

  useEffect(() => {
    execute();
  }, [execute]);

  return { data, loading, error, refetch: execute };
}

Form Handling Pattern

function useForm(initialValues, validationRules) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const setValue = (name, value) => {
    setValues(prev => ({ ...prev, [name]: value }));
    
    // Clear error when user starts typing
    if (errors[name]) {
      setErrors(prev => ({ ...prev, [name]: '' }));
    }
  };

  const setTouched = (name) => {
    setTouched(prev => ({ ...prev, [name]: true }));
  };

  const validate = () => {
    const newErrors = {};
    
    Object.keys(validationRules).forEach(field => {
      const rule = validationRules[field];
      const value = values[field];
      
      if (rule.required && !value) {
        newErrors[field] = `${field} is required`;
      } else if (rule.minLength && value.length < rule.minLength) {
        newErrors[field] = `${field} must be at least ${rule.minLength} characters`;
      } else if (rule.pattern && !rule.pattern.test(value)) {
        newErrors[field] = rule.message || `${field} is invalid`;
      }
    });
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = (onSubmit) => (e) => {
    e.preventDefault();
    
    // Mark all fields as touched
    const allTouched = Object.keys(initialValues).reduce((acc, key) => {
      acc[key] = true;
      return acc;
    }, {});
    setTouched(allTouched);
    
    if (validate()) {
      onSubmit(values);
    }
  };

  return {
    values,
    errors,
    touched,
    setValue,
    setTouched,
    handleSubmit,
    isValid: Object.keys(errors).length === 0
  };
}

// Usage
function ContactForm() {
  const { values, errors, touched, setValue, setTouched, handleSubmit } = useForm(
    { name: '', email: '', message: '' },
    {
      name: { required: true, minLength: 2 },
      email: { 
        required: true, 
        pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
        message: 'Please enter a valid email'
      },
      message: { required: true, minLength: 10 }
    }
  );

  const onSubmit = (formData) => {
    console.log('Submitting:', formData);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input
          type="text"
          placeholder="Name"
          value={values.name}
          onChange={(e) => setValue('name', e.target.value)}
          onBlur={() => setTouched('name')}
        />
        {touched.name && errors.name && <span style={{color: 'red'}}>{errors.name}</span>}
      </div>
      
      <div>
        <input
          type="email"
          placeholder="Email"
          value={values.email}
          onChange={(e) => setValue('email', e.target.value)}
          onBlur={() => setTouched('email')}
        />
        {touched.email && errors.email && <span style={{color: 'red'}}>{errors.email}</span>}
      </div>
      
      <div>
        <textarea
          placeholder="Message"
          value={values.message}
          onChange={(e) => setValue('message', e.target.value)}
          onBlur={() => setTouched('message')}
        />
        {touched.message && errors.message && <span style={{color: 'red'}}>{errors.message}</span>}
      </div>
      
      <button type="submit">Submit</button>
    </form>
  );
}

Best Practices {#best-practices}

1. Keep State Close to Where It's Used

// ❌ State too high up
function App() {
  const [userInput, setUserInput] = useState('');
  
  return (
    <div>
      <Header />
      <Sidebar />
      <SearchForm input={userInput} setInput={setUserInput} />
    </div>
  );
}

// ✅ State where it's needed
function SearchForm() {
  const [userInput, setUserInput] = useState('');
  
  return (
    <form>
      <input 
        value={userInput} 
        onChange={(e) => setUserInput(e.target.value)} 
      />
    </form>
  );
}

2. Use Multiple State Variables

// ❌ One big state object
function UserForm() {
  const [state, setState] = useState({
    name: '',
    email: '',
    loading: false,
    errors: {}
  });
  
  // Complex updates...
}

// ✅ Separate state variables
function UserForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [loading, setLoading] = useState(false);
  const [errors, setErrors] = useState({});
  
  // Simpler updates...
}

3. Optimize Dependencies

// ❌ Missing dependencies
useEffect(() => {
  fetchData(userId);
}, []); // Missing userId dependency

// ❌ Too many dependencies
useEffect(() => {
  if (user.id) {
    fetchUserData(user.id);
  }
}, [user]); // user object might change on every render

// ✅ Correct dependencies
useEffect(() => {
  if (user.id) {
    fetchUserData(user.id);
  }
}, [user.id]); // Only depend on user.id

4. Custom Hook Naming

// ✅ Good custom hook names
function useCounter(initialValue = 0) { /* ... */ }
function useLocalStorage(key, initialValue) { /* ... */ }
function useApi(url) { /* ... */ }
function useDebounce(value, delay) { /* ... */ }

// ❌ Bad naming
function counter() { /* ... */ } // Doesn't start with 'use'
function getData() { /* ... */ } // Not descriptive of hook purpose

Troubleshooting {#troubleshooting}

Common Issues and Solutions

Stale Closures

// ❌ Problem: Stale closure
function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(count + 1); // Always uses initial count value (0)
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return <div>{count}</div>;
}

// ✅ Solution: Use functional update
function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(prevCount => prevCount + 1); // Uses current count
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  return <div>{count}</div>;
}

Infinite Re-renders

// ❌ Problem: Object in dependency array
function UserProfile({ user }) {
  const [profile, setProfile] = useState(null);

  useEffect(() => {
    fetchProfile(user); // user object changes on every render
  }, [user]);
}

// ✅ Solution: Use specific properties
function UserProfile({ user }) {
  const [profile, setProfile] = useState(null);

  useEffect(() => {
    fetchProfile(user);
  }, [user.id]); // Only depend on user.id
}

Missing Cleanup

// ❌ Problem: No cleanup
function Component() {
  useEffect(() => {
    const subscription = api.subscribe(handleData);
    // Missing cleanup - memory leak!
  }, []);
}

// ✅ Solution: Proper cleanup
function Component() {
  useEffect(() => {
    const subscription = api.subscribe(handleData);
    
    return () => {
      subscription.unsubscribe();
    };
  }, []);
}

Conclusion

React Hooks provide a powerful and flexible way to manage state and side effects in functional components. Key takeaways:

  1. Start simple - Use useState and useEffect for most cases
  2. Extract logic - Create custom hooks for reusable logic
  3. Optimize wisely - Use useMemo and useCallback when you have performance issues
  4. Follow the rules - Always call hooks at the top level
  5. Think in hooks - Design your components around hook patterns

Remember that hooks are just functions, and like any tool, they should be used thoughtfully. Focus on writing clear, maintainable code, and optimize only when necessary.

Happy coding with React Hooks! 🎣