Sign In Create Account
React Hooks

React Hooks

Comprehensive guide to using React hooks effectively, including built-in hooks, custom hooks, and performance optimization.

useState and useEffect Patterns

Master the most fundamental hooks with proper patterns for state management and side effects.

  • • Initialize state with proper types and default values
  • • Use functional updates when new state depends on previous state
  • • Optimize useEffect with proper dependency arrays
  • • Clean up side effects to prevent memory leaks

useState and useEffect Best Practices

// ✅ Good useState patterns
interface User {
  id: string;
  name: string;
  email: string;
}

interface FormData {
  name: string;
  email: string;
}

const UserProfile: React.FC<{ userId: string }> = ({ userId }) => {
  // Initialize with proper types and null for async data
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  // Form state with initial values
  const [formData, setFormData] = useState<FormData>({
    name: '',
    email: ''
  });

  // ✅ Functional state update when depending on previous state
  const [count, setCount] = useState(0);
  const increment = () => setCount(prev => prev + 1);
  
  // ✅ Proper useEffect with dependencies and cleanup
  useEffect(() => {
    let cancelled = false;
    
    const fetchUser = async () => {
      try {
        setIsLoading(true);
        setError(null);
        
        const userData = await getUserById(userId);
        
        // Check if component is still mounted
        if (!cancelled) {
          setUser(userData);
          setFormData({
            name: userData.name,
            email: userData.email
          });
        }
      } catch (err) {
        if (!cancelled) {
          setError(err instanceof Error ? err.message : 'Unknown error');
        }
      } finally {
        if (!cancelled) {
          setIsLoading(false);
        }
      }
    };

    fetchUser();

    // Cleanup function to prevent state updates on unmounted component
    return () => {
      cancelled = true;
    };
  }, [userId]); // Only re-run when userId changes

  // ✅ Effect for setting up subscriptions with cleanup
  useEffect(() => {
    if (!user) return;

    const subscription = subscribeToUserUpdates(user.id, (updatedUser) => {
      setUser(updatedUser);
    });

    return () => {
      subscription.unsubscribe();
    };
  }, [user?.id]);

  // ✅ Update form field with proper state management
  const handleInputChange = (field: keyof FormData) => (
    event: React.ChangeEvent<HTMLInputElement>
  ) => {
    setFormData(prev => ({
      ...prev,
      [field]: event.target.value
    }));
  };

  if (isLoading) return <LoadingSpinner />;
  if (error) return <ErrorMessage message={error} />;
  if (!user) return <EmptyState />;

  return (
    <form>
      <input 
        value={formData.name}
        onChange={handleInputChange('name')}
        placeholder="Name"
      />
      <input 
        value={formData.email}
        onChange={handleInputChange('email')}
        placeholder="Email"
      />
    </form>
  );
};

// ❌ Avoid - common useState/useEffect mistakes
const BadComponent = ({ userId }: { userId: string }) => {
  const [user, setUser] = useState(); // No type annotation
  const [data, setData] = useState({}); // Generic object

  // ❌ Missing dependency array - runs on every render
  useEffect(() => {
    fetchUser();
  });

  // ❌ Missing cleanup - can cause memory leaks
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Timer running');
    }, 1000);
    // Missing: return () => clearInterval(timer);
  }, []);

  // ❌ Direct state mutation
  const handleUpdate = () => {
    user.name = 'New Name'; // Mutating state directly
    setUser(user); // Won't trigger re-render
  };

  return <div>{user?.name}</div>;
};

Performance Optimization with useMemo and useCallback

Use performance hooks strategically to prevent unnecessary re-renders and expensive calculations.

Performance Optimization Patterns

// ✅ useMemo for expensive calculations
interface ProductListProps {
  products: Product[];
  searchTerm: string;
  sortBy: 'name' | 'price' | 'rating';
  filterBy: string[];
}

const ProductList: React.FC<ProductListProps> = ({
  products,
  searchTerm,
  sortBy,
  filterBy
}) => {
  // ✅ Memoize expensive filtering and sorting
  const filteredAndSortedProducts = useMemo(() => {
    let result = products.filter(product => 
      product.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
      (filterBy.length === 0 || filterBy.includes(product.category))
    );

    return result.sort((a, b) => {
      switch (sortBy) {
        case 'name':
          return a.name.localeCompare(b.name);
        case 'price':
          return a.price - b.price;
        case 'rating':
          return b.rating - a.rating;
        default:
          return 0;
      }
    });
  }, [products, searchTerm, sortBy, filterBy]);

  // ✅ Memoize calculated statistics
  const productStats = useMemo(() => ({
    totalProducts: filteredAndSortedProducts.length,
    averagePrice: filteredAndSortedProducts.reduce((sum, p) => sum + p.price, 0) / 
                  filteredAndSortedProducts.length,
    averageRating: filteredAndSortedProducts.reduce((sum, p) => sum + p.rating, 0) / 
                   filteredAndSortedProducts.length
  }), [filteredAndSortedProducts]);

  // ✅ useCallback to prevent child re-renders
  const handleProductClick = useCallback((productId: string) => {
    // Navigate to product details or trigger action
    router.push(`/products/${productId}`);
  }, [router]);

  const handleAddToCart = useCallback((product: Product) => {
    addToCart(product);
    showNotification(`${product.name} added to cart`);
  }, [addToCart, showNotification]);

  // ✅ Memoized event handlers with dependencies
  const handleSearch = useCallback(
    debounce((term: string) => {
      setSearchTerm(term);
      analytics.track('product_search', { term });
    }, 300),
    [setSearchTerm, analytics]
  );

  return (
    <div>
      <ProductStats stats={productStats} />
      <SearchInput onSearch={handleSearch} />
      
      <div className="product-grid">
        {filteredAndSortedProducts.map(product => (
          <ProductCard
            key={product.id}
            product={product}
            onClick={handleProductClick}
            onAddToCart={handleAddToCart}
          />
        ))}
      </div>
    </div>
  );
};

// ✅ Optimized child component with memo
interface ProductCardProps {
  product: Product;
  onClick: (productId: string) => void;
  onAddToCart: (product: Product) => void;
}

const ProductCard = memo<ProductCardProps>(({ product, onClick, onAddToCart }) => {
  const handleClick = () => onClick(product.id);
  const handleAddToCart = () => onAddToCart(product);

  return (
    <div className="product-card" onClick={handleClick}>
      <img src={product.imageUrl} alt={product.name} />
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button 
        onClick={(e) => {
          e.stopPropagation();
          handleAddToCart();
        }}
      >
        Add to Cart
      </button>
    </div>
  );
});

// ❌ Avoid - overusing performance hooks
const OverOptimizedComponent = ({ items }: { items: string[] }) => {
  // ❌ Unnecessary memoization of simple operations
  const itemCount = useMemo(() => items.length, [items]);
  
  // ❌ Memoizing primitive values
  const title = useMemo(() => 'My List', []);
  
  // ❌ useCallback without proper dependencies
  const handleClick = useCallback(() => {
    console.log(items.length); // Uses items but not in dependency array
  }, []);

  return <div>{title}: {itemCount} items</div>;
};

Custom Hooks for Reusable Logic

Extract common logic into custom hooks to promote reusability and cleaner components.

Custom Hook Patterns

// Custom hook for API data fetching
interface UseApiOptions<T> {
  initialData?: T;
  enabled?: boolean;
}

interface UseApiReturn<T> {
  data: T | null;
  isLoading: boolean;
  error: string | null;
  refetch: () => Promise<void>;
}

function useApi<T>(
  url: string,
  options: UseApiOptions<T> = {}
) {
  const { initialData = null, enabled = true } = options;

  const [data, setData] = useState<T | null>(initialData);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchData = useCallback(async () => {
    try {
      setIsLoading(true);
      setError(null);

      const response = await fetch(url);
      if (!response.ok) {
        throw new Error('HTTP error: ' + response.status);
      }

      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
    } finally {
      setIsLoading(false);
    }
  }, [url]);

  useEffect(() => {
    if (enabled) {
      fetchData();
    }
  }, [fetchData, enabled]);

  return { data, isLoading, error, refetch: fetchData };
}

Advanced Hook Patterns

Leverage advanced patterns like useReducer for complex state and useRef for imperative operations.

Advanced Hook Patterns

// ✅ useReducer for complex state logic
interface TodoState {
  todos: Todo[];
  filter: 'all' | 'active' | 'completed';
  isLoading: boolean;
}

type TodoAction = 
  | { type: 'ADD_TODO'; payload: { text: string } }
  | { type: 'TOGGLE_TODO'; payload: { id: string } }
  | { type: 'DELETE_TODO'; payload: { id: string } }
  | { type: 'SET_FILTER'; payload: { filter: TodoState['filter'] } }
  | { type: 'SET_LOADING'; payload: { isLoading: boolean } }
  | { type: 'LOAD_TODOS'; payload: { todos: Todo[] } };

const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            id: crypto.randomUUID(),
            text: action.payload.text,
            completed: false,
            createdAt: new Date()
          }
        ]
      };
      
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload.id
            ? { ...todo, completed: !todo.completed }
            : todo
        )
      };
      
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload.id)
      };
      
    case 'SET_FILTER':
      return {
        ...state,
        filter: action.payload.filter
      };
      
    case 'SET_LOADING':
      return {
        ...state,
        isLoading: action.payload.isLoading
      };
      
    case 'LOAD_TODOS':
      return {
        ...state,
        todos: action.payload.todos,
        isLoading: false
      };
      
    default:
      return state;
  }
};

const TodoApp = () => {
  const [state, dispatch] = useReducer(todoReducer, {
    todos: [],
    filter: 'all',
    isLoading: true
  });

  const filteredTodos = useMemo(() => {
    switch (state.filter) {
      case 'active':
        return state.todos.filter(todo => !todo.completed);
      case 'completed':
        return state.todos.filter(todo => todo.completed);
      default:
        return state.todos;
    }
  }, [state.todos, state.filter]);

  const addTodo = useCallback((text: string) => {
    dispatch({ type: 'ADD_TODO', payload: { text } });
  }, []);

  const toggleTodo = useCallback((id: string) => {
    dispatch({ type: 'TOGGLE_TODO', payload: { id } });
  }, []);

  return (
    <div>
      <AddTodoForm onAdd={addTodo} />
      <TodoFilters 
        currentFilter={state.filter} 
        onFilterChange={(filter) => dispatch({ type: 'SET_FILTER', payload: { filter } })}
      />
      <TodoList todos={filteredTodos} onToggle={toggleTodo} />
    </div>
  );
};

// ✅ useRef for imperative operations and DOM access
const FocusableInput = () => {
  const inputRef = useRef<HTMLInputElement>(null);
  const [value, setValue] = useState('');

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

  const handleClear = () => {
    setValue('');
    inputRef.current?.focus();
  };

  return (
    <div>
      <input
        ref={inputRef}
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder="Type something..."
      />
      <button onClick={handleClear}>Clear</button>
    </div>
  );
};

// ✅ useRef for storing mutable values
const Timer = () => {
  const [time, setTime] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  const intervalRef = useRef<NodeJS.Timeout | null>(null);

  const startTimer = useCallback(() => {
    if (intervalRef.current) return;
    
    setIsRunning(true);
    intervalRef.current = setInterval(() => {
      setTime(prev => prev + 1);
    }, 1000);
  }, []);

  const stopTimer = useCallback(() => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
    setIsRunning(false);
  }, []);

  const resetTimer = useCallback(() => {
    stopTimer();
    setTime(0);
  }, [stopTimer]);

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);

  return (
    <div>
      <div>Time: {time}s</div>
      <button onClick={startTimer} disabled={isRunning}>
        Start
      </button>
      <button onClick={stopTimer} disabled={!isRunning}>
        Stop
      </button>
      <button onClick={resetTimer}>Reset</button>
    </div>
  );
};

// ✅ useImperativeHandle for exposing component methods
interface ModalHandle {
  open: () => void;
  close: () => void;
}

interface ModalProps {
  children: React.ReactNode;
  title?: string;
}

const Modal = forwardRef<ModalHandle, ModalProps>(({ children, title }, ref) => {
  const [isOpen, setIsOpen] = useState(false);

  useImperativeHandle(ref, () => ({
    open: () => setIsOpen(true),
    close: () => setIsOpen(false)
  }));

  if (!isOpen) return null;

  return (
    <div className="modal-overlay">
      <div className="modal">
        {title && <h2>{title}</h2>}
        {children}
        <button onClick={() => setIsOpen(false)}>Close</button>
      </div>
    </div>
  );
});

// Usage with imperative handle
const App = () => {
  const modalRef = useRef<ModalHandle>(null);

  return (
    <div>
      <button onClick={() => modalRef.current?.open()}>
        Open Modal
      </button>
      
      <Modal ref={modalRef} title="Confirmation">
        <p>Are you sure you want to proceed?</p>
        <button onClick={() => modalRef.current?.close()}>
          Confirm
        </button>
      </Modal>
    </div>
  );
};