Przejdź do treści
Guides

React Hooks - A Practical Guide from Basics to Advanced Patterns

Published on:
·3 min read·Author: MDS Software Solutions Group

React Hooks Practical

poradniki

React Hooks - A Practical Guide from Scratch

React Hooks revolutionized how we build components in React. Since their introduction in version 16.8, hooks have become the preferred approach to managing state and side effects, effectively replacing class components. In this guide, we will cover all essential hooks, demonstrate practical patterns with TypeScript, and introduce the latest additions from React 19.

What Are React Hooks?#

Hooks are functions that let you "hook into" React's internal mechanisms from functional components. They allow you to use state, side effects, context, and many other features without writing class components.

// Before hooks - class component
class Counter extends React.Component<{}, { count: number }> {
  state = { count: 0 };

  render() {
    return (
      <button onClick={() => this.setState({ count: this.state.count + 1 })}>
        Clicks: {this.state.count}
      </button>
    );
  }
}

// With hooks - functional component
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicks: {count}
    </button>
  );
}

useState - Managing Local State#

useState is the most fundamental hook. It adds state to a functional component. It accepts an initial value and returns a pair: the current state value and a function to update it.

import { useState } from 'react';

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

function RegistrationForm() {
  const [formData, setFormData] = useState<FormData>({
    name: '',
    email: '',
    age: 0,
  });
  const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleChange = (field: keyof FormData, value: string | number) => {
    setFormData(prev => ({ ...prev, [field]: value }));
    // Clear error when value changes
    setErrors(prev => ({ ...prev, [field]: undefined }));
  };

  const validate = (): boolean => {
    const newErrors: Partial<Record<keyof FormData, string>> = {};

    if (!formData.name.trim()) {
      newErrors.name = 'Name is required';
    }
    if (!formData.email.includes('@')) {
      newErrors.email = 'Invalid email address';
    }
    if (formData.age < 18) {
      newErrors.age = 'You must be at least 18 years old';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!validate()) return;

    setIsSubmitting(true);
    try {
      await submitForm(formData);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={formData.name}
        onChange={e => handleChange('name', e.target.value)}
        placeholder="Name"
      />
      {errors.name && <span className="error">{errors.name}</span>}

      <input
        value={formData.email}
        onChange={e => handleChange('email', e.target.value)}
        placeholder="Email"
      />
      {errors.email && <span className="error">{errors.email}</span>}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Register'}
      </button>
    </form>
  );
}

Lazy State Initialization#

When the initial value requires expensive computation, pass a function instead of a value:

// Expensive initialization - runs on EVERY render
const [items, setItems] = useState(parseExpensiveData(rawData));

// Lazy initialization - runs ONLY once
const [items, setItems] = useState(() => parseExpensiveData(rawData));

Updating State Based on Previous Value#

Always use the functional form when the new value depends on the previous one:

// Potential issue with batched updates
const handleDoubleIncrement = () => {
  setCount(count + 1); // count = 0, sets to 1
  setCount(count + 1); // count is still 0, sets to 1
};

// Correct approach
const handleDoubleIncrement = () => {
  setCount(prev => prev + 1); // 0 -> 1
  setCount(prev => prev + 1); // 1 -> 2
};

useEffect - Side Effects#

useEffect allows you to synchronize your component with external systems: APIs, subscriptions, timers, or the DOM.

import { useState, useEffect } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
}

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const controller = new AbortController();

    async function fetchUser() {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(`/api/users/${userId}`, {
          signal: controller.signal,
        });

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }

        const data: User = await response.json();
        setUser(data);
      } catch (err) {
        if (err instanceof Error && err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchUser();

    // Cleanup - cancel request on unmount or userId change
    return () => controller.abort();
  }, [userId]);

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

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

Common useEffect Patterns#

// Run once after mounting
useEffect(() => {
  initializeAnalytics();
}, []);

// Respond to dependency changes
useEffect(() => {
  document.title = `You have ${count} new messages`;
}, [count]);

// Subscription with cleanup
useEffect(() => {
  const ws = new WebSocket('wss://api.example.com/feed');

  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    setMessages(prev => [...prev, data]);
  };

  return () => ws.close();
}, []);

// Event listener
useEffect(() => {
  const handleResize = () => {
    setWindowWidth(window.innerWidth);
  };

  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

useContext - Global State Without Prop Drilling#

useContext eliminates the problem of passing props through many layers of components.

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

interface Theme {
  mode: 'light' | 'dark';
  primaryColor: string;
}

interface ThemeContextType {
  theme: Theme;
  toggleMode: () => void;
  setPrimaryColor: (color: string) => void;
}

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

// Custom hook with validation
function useTheme(): ThemeContextType {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>({
    mode: 'light',
    primaryColor: '#3b82f6',
  });

  const toggleMode = () => {
    setTheme(prev => ({
      ...prev,
      mode: prev.mode === 'light' ? 'dark' : 'light',
    }));
  };

  const setPrimaryColor = (color: string) => {
    setTheme(prev => ({ ...prev, primaryColor: color }));
  };

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

// Component consuming the context
function ThemeToggle() {
  const { theme, toggleMode } = useTheme();

  return (
    <button onClick={toggleMode}>
      Current theme: {theme.mode === 'light' ? 'Light' : 'Dark'}
    </button>
  );
}

useReducer - Advanced State Management#

useReducer is a better choice than useState when your state logic is complex or involves multiple related values.

import { useReducer } from 'react';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartState {
  items: CartItem[];
  discount: number;
  isCheckingOut: boolean;
}

type CartAction =
  | { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
  | { type: 'REMOVE_ITEM'; payload: { id: string } }
  | { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
  | { type: 'APPLY_DISCOUNT'; payload: { discount: number } }
  | { type: 'CHECKOUT_START' }
  | { type: 'CHECKOUT_COMPLETE' }
  | { type: 'CLEAR_CART' };

function cartReducer(state: CartState, action: CartAction): CartState {
  switch (action.type) {
    case 'ADD_ITEM': {
      const existingItem = state.items.find(item => item.id === action.payload.id);

      if (existingItem) {
        return {
          ...state,
          items: state.items.map(item =>
            item.id === action.payload.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          ),
        };
      }

      return {
        ...state,
        items: [...state.items, { ...action.payload, quantity: 1 }],
      };
    }

    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload.id),
      };

    case 'UPDATE_QUANTITY':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: Math.max(0, action.payload.quantity) }
            : item
        ).filter(item => item.quantity > 0),
      };

    case 'APPLY_DISCOUNT':
      return { ...state, discount: action.payload.discount };

    case 'CHECKOUT_START':
      return { ...state, isCheckingOut: true };

    case 'CHECKOUT_COMPLETE':
      return { items: [], discount: 0, isCheckingOut: false };

    case 'CLEAR_CART':
      return { items: [], discount: 0, isCheckingOut: false };

    default:
      return state;
  }
}

function ShoppingCart() {
  const [cart, dispatch] = useReducer(cartReducer, {
    items: [],
    discount: 0,
    isCheckingOut: false,
  });

  const total = cart.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );
  const discountedTotal = total * (1 - cart.discount);

  return (
    <div>
      {cart.items.map(item => (
        <div key={item.id}>
          <span>{item.name} - ${item.price} x {item.quantity}</span>
          <button onClick={() => dispatch({
            type: 'UPDATE_QUANTITY',
            payload: { id: item.id, quantity: item.quantity + 1 }
          })}>+</button>
          <button onClick={() => dispatch({
            type: 'UPDATE_QUANTITY',
            payload: { id: item.id, quantity: item.quantity - 1 }
          })}>-</button>
          <button onClick={() => dispatch({
            type: 'REMOVE_ITEM',
            payload: { id: item.id }
          })}>Remove</button>
        </div>
      ))}
      <p>Total: ${discountedTotal.toFixed(2)}</p>
    </div>
  );
}

useMemo and useCallback - Performance Optimization#

These hooks help avoid unnecessary computations and re-renders.

useMemo - Memoizing Values#

import { useMemo, useState } from 'react';

interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
  rating: number;
}

function ProductList({ products }: { products: Product[] }) {
  const [sortBy, setSortBy] = useState<'price' | 'rating'>('price');
  const [filterCategory, setFilterCategory] = useState<string>('all');
  const [searchQuery, setSearchQuery] = useState('');

  // Memoize expensive filtering and sorting
  const filteredAndSorted = useMemo(() => {
    let result = products;

    // Filter by category
    if (filterCategory !== 'all') {
      result = result.filter(p => p.category === filterCategory);
    }

    // Filter by search query
    if (searchQuery) {
      const query = searchQuery.toLowerCase();
      result = result.filter(p =>
        p.name.toLowerCase().includes(query)
      );
    }

    // Sort
    return [...result].sort((a, b) =>
      sortBy === 'price' ? a.price - b.price : b.rating - a.rating
    );
  }, [products, sortBy, filterCategory, searchQuery]);

  // Memoize statistics
  const stats = useMemo(() => ({
    count: filteredAndSorted.length,
    avgPrice: filteredAndSorted.reduce((s, p) => s + p.price, 0) / filteredAndSorted.length || 0,
    avgRating: filteredAndSorted.reduce((s, p) => s + p.rating, 0) / filteredAndSorted.length || 0,
  }), [filteredAndSorted]);

  return (
    <div>
      <input
        value={searchQuery}
        onChange={e => setSearchQuery(e.target.value)}
        placeholder="Search products..."
      />
      <p>Found: {stats.count} | Average price: ${stats.avgPrice.toFixed(2)}</p>
      {filteredAndSorted.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

useCallback - Memoizing Functions#

import { useCallback, memo } from 'react';

// Child component with React.memo
const TodoItem = memo(function TodoItem({
  todo,
  onToggle,
  onDelete,
}: {
  todo: { id: string; text: string; done: boolean };
  onToggle: (id: string) => void;
  onDelete: (id: string) => void;
}) {
  console.log(`Render TodoItem: ${todo.text}`);

  return (
    <li>
      <input
        type="checkbox"
        checked={todo.done}
        onChange={() => onToggle(todo.id)}
      />
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </li>
  );
});

function TodoList() {
  const [todos, setTodos] = useState<{ id: string; text: string; done: boolean }[]>([]);
  const [input, setInput] = useState('');

  // Without useCallback - new reference on every render
  // = every TodoItem re-renders
  const handleToggle = useCallback((id: string) => {
    setTodos(prev =>
      prev.map(todo =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    );
  }, []);

  const handleDelete = useCallback((id: string) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []);

  return (
    <div>
      <input value={input} onChange={e => setInput(e.target.value)} />
      <ul>
        {todos.map(todo => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={handleToggle}
            onDelete={handleDelete}
          />
        ))}
      </ul>
    </div>
  );
}

useRef - References and Mutable Values#

useRef holds a mutable value that does not cause a re-render when changed. It is ideal for storing DOM element references and persistent values between renders.

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

function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null);
  const renderCount = useRef(0);
  const previousValue = useRef<string>('');
  const [value, setValue] = useState('');

  // Render counter (does not cause additional renders)
  renderCount.current += 1;

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

  useEffect(() => {
    previousValue.current = value;
  }, [value]);

  return (
    <div>
      <input
        ref={inputRef}
        value={value}
        onChange={e => setValue(e.target.value)}
        placeholder="This field auto-focuses"
      />
      <p>Render count: {renderCount.current}</p>
      <p>Previous value: {previousValue.current}</p>
    </div>
  );
}

useRef with Timers#

function Stopwatch() {
  const [time, setTime] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);

  const start = () => {
    if (isRunning) return;
    setIsRunning(true);
    intervalRef.current = setInterval(() => {
      setTime(prev => prev + 10);
    }, 10);
  };

  const stop = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
    setIsRunning(false);
  };

  const reset = () => {
    stop();
    setTime(0);
  };

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

  const formatTime = (ms: number) => {
    const minutes = Math.floor(ms / 60000);
    const seconds = Math.floor((ms % 60000) / 1000);
    const centiseconds = Math.floor((ms % 1000) / 10);
    return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
  };

  return (
    <div>
      <span>{formatTime(time)}</span>
      <button onClick={start} disabled={isRunning}>Start</button>
      <button onClick={stop} disabled={!isRunning}>Stop</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Custom Hooks - Building Your Own#

Custom hooks are a powerful mechanism for extracting and reusing stateful logic across components.

// useLocalStorage - persistent state in localStorage
function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value: T | ((prev: T) => T)) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value;
    setStoredValue(valueToStore);
    window.localStorage.setItem(key, JSON.stringify(valueToStore));
  };

  return [storedValue, setValue];
}

// useDebounce - delayed value
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// useFetch - generic data fetching hook
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const controller = new AbortController();

    async function fetchData() {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const json: T = await response.json();
        setData(json);
      } catch (err) {
        if (err instanceof Error && err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchData();
    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

// Usage
function SearchPage() {
  const [query, setQuery] = useLocalStorage('searchQuery', '');
  const debouncedQuery = useDebounce(query, 300);
  const { data, loading, error } = useFetch<Product[]>(
    `/api/search?q=${encodeURIComponent(debouncedQuery)}`
  );

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error}</p>}
      {data?.map(product => <ProductCard key={product.id} product={product} />)}
    </div>
  );
}

Rules of Hooks#

React requires you to follow two fundamental rules:

1. Only Call Hooks at the Top Level#

// Never use hooks inside conditions, loops, or nested functions

function BadComponent({ isLoggedIn }: { isLoggedIn: boolean }) {
  // NEVER do this
  if (isLoggedIn) {
    const [user, setUser] = useState(null); // Bad practice
  }

  // Correct - hook always at the top level
  const [user, setUser] = useState(null);

  // Condition inside or after the hook
  useEffect(() => {
    if (isLoggedIn) {
      fetchUser().then(setUser);
    }
  }, [isLoggedIn]);
}

2. Only Call Hooks from React Components or Custom Hooks#

// Do not call hooks in regular JavaScript functions
function regularFunction() {
  const [state, setState] = useState(0); // Error
}

// Correct - custom hook (name starts with "use")
function useCounter(initial: number = 0) {
  const [count, setCount] = useState(initial);
  const increment = () => setCount(prev => prev + 1);
  const decrement = () => setCount(prev => prev - 1);
  return { count, increment, decrement };
}

New Hooks in React 19#

React 19 introduces new hooks that significantly simplify common patterns.

useFormStatus#

Tracks form submission state without passing props:

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending, data, method } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Submitting...' : 'Submit Form'}
    </button>
  );
}

function ContactForm() {
  async function handleSubmit(formData: FormData) {
    'use server';
    const name = formData.get('name');
    const email = formData.get('email');
    await saveContact({ name, email });
  }

  return (
    <form action={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <SubmitButton />
    </form>
  );
}

useOptimistic#

Enables optimistic UI updates - showing the expected result before the server confirms the operation:

import { useOptimistic, useState } from 'react';

interface Message {
  id: string;
  text: string;
  sending?: boolean;
}

function Chat() {
  const [messages, setMessages] = useState<Message[]>([]);

  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state: Message[], newMessage: Message) => [
      ...state,
      { ...newMessage, sending: true },
    ]
  );

  async function sendMessage(formData: FormData) {
    const text = formData.get('message') as string;
    const optimisticMsg: Message = {
      id: crypto.randomUUID(),
      text,
      sending: true,
    };

    addOptimisticMessage(optimisticMsg);

    const savedMessage = await saveMessageToServer(text);
    setMessages(prev => [...prev, savedMessage]);
  }

  return (
    <div>
      {optimisticMessages.map(msg => (
        <div key={msg.id} style={{ opacity: msg.sending ? 0.6 : 1 }}>
          {msg.text}
          {msg.sending && <span> (sending...)</span>}
        </div>
      ))}
      <form action={sendMessage}>
        <input name="message" placeholder="Type a message..." />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

use - A New Way to Read Resources#

The use hook lets you read values from Promises and Context in ways no other hook can - even conditionally:

import { use, Suspense } from 'react';

// Reading a Promise
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise);

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

// Conditional context reading
function StatusMessage({ isAdmin }: { isAdmin: boolean }) {
  if (isAdmin) {
    const adminContext = use(AdminContext);
    return <p>Admin panel: {adminContext.dashboardUrl}</p>;
  }
  return <p>User view</p>;
}

// Usage with Suspense
function App() {
  const userPromise = fetchUser(1);

  return (
    <Suspense fallback={<div>Loading profile...</div>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

Performance Patterns#

Avoiding Unnecessary Re-renders#

import { memo, useMemo, useCallback } from 'react';

// 1. Component splitting - isolate state
function SearchPage() {
  return (
    <div>
      <SearchInput /> {/* Search state lives here */}
      <ExpensiveStaticContent /> {/* Does not re-render */}
    </div>
  );
}

// 2. Composition pattern - children do not re-render
function Layout({ children }: { children: ReactNode }) {
  const [sidebarOpen, setSidebarOpen] = useState(false);

  return (
    <div>
      <Sidebar open={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
      <main>{children}</main> {/* children do not re-render */}
    </div>
  );
}

// 3. Context memoization
function OptimizedProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const value = useMemo(
    () => ({ user, setUser }),
    [user]
  );

  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
}

Common Mistakes#

1. Missing Dependencies in useEffect#

// Problem - stale closure
function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // Always reads count = 0
    }, 1000);
    return () => clearInterval(id);
  }, []); // Missing count in dependencies

  // Solution - use the functional form
  useEffect(() => {
    const id = setInterval(() => {
      setCount(prev => prev + 1); // Always current value
    }, 1000);
    return () => clearInterval(id);
  }, []);
}

2. Infinite Loop in useEffect#

// Problem - object as dependency
function UserList() {
  const [users, setUsers] = useState<User[]>([]);

  const filters = { role: 'admin', active: true }; // New object on every render

  useEffect(() => {
    fetchUsers(filters).then(setUsers); // Infinite loop
  }, [filters]); // filters is always a new reference

  // Solution - useMemo
  const filters = useMemo(() => ({ role: 'admin', active: true }), []);

  useEffect(() => {
    fetchUsers(filters).then(setUsers);
  }, [filters]);
}

3. Overusing useEffect#

// Anti-pattern - useEffect for data transformation
function FilteredList({ items, filter }: Props) {
  const [filtered, setFiltered] = useState(items);

  useEffect(() => {
    setFiltered(items.filter(item => item.category === filter));
  }, [items, filter]);

  // Correct - compute during rendering
  const filtered = useMemo(
    () => items.filter(item => item.category === filter),
    [items, filter]
  );
}

Summary#

React Hooks are the foundation of modern React:

  • useState - local component state
  • useEffect - synchronization with external systems
  • useContext - global state without prop drilling
  • useReducer - complex state logic
  • useMemo / useCallback - performance optimization
  • useRef - DOM references and mutable values
  • Custom hooks - reusable stateful logic
  • React 19 - useFormStatus, useOptimistic, use

The key is understanding when and why to use each hook, rather than applying them blindly. Remember the rules of hooks and measure performance before optimizing.

Need Help?#

At MDS Software Solutions Group, we help with:

  • Building modern React applications with TypeScript
  • Migrating from class components to hooks
  • Frontend application performance optimization
  • Implementing best practices and architectural patterns
  • Code reviews and quality audits

Contact us to discuss your project!

Author
MDS Software Solutions Group

Team of programming experts specializing in modern web technologies.

React Hooks - A Practical Guide from Basics to Advanced Patterns | MDS Software Solutions Group | MDS Software Solutions Group