Back to the blog

React Hooks: Complete Guide from useState to Custom Hooks

Deep dive into React Hooks: useState, useEffect, useContext, useMemo, useCallback, useRef and creating custom hooks.

January 5, 2026
14 min read
351 views
MOLOTILO

MOLOTILO DIGITAL

React Hooks: Complete Guide from useState to Custom Hooks

Introduction to React Hooks

React Hooks appeared in version 16.8 and changed the way we write components. Hooks let you use state and other React features without classes. Let’s look at all the main hooks and patterns for using them.

useState — managing state

useState is the basic hook for a component’s local state:

import { useState } from 'react';

// Simple state
function Counter() {
  const [count, setCount] = useState(0);

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

// State with an object
interface User {
  name: string;
  email: string;
  age: number;
}

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

  // Updating a single field
  const updateField = (field: keyof User, value: string | number) => {
    setUser(prev => ({ ...prev, [field]: value }));
  };

  return (
    <form>
      <input
        value={user.name}
        onChange={e => updateField('name', e.target.value)}
      />
    </form>
  );
}

// Lazy initialization (for heavy computations)
function ExpensiveComponent() {
  const [data, setData] = useState(() => {
    // Runs only on the first render
    return computeExpensiveValue();
  });
}

useEffect — side effects

useEffect performs side effects: API requests, subscriptions, DOM manipulations:

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    // A flag to cancel on unmount
    let cancelled = false;

    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();

        if (!cancelled) {
          setUser(data);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err as Error);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    fetchUser();

    // Cleanup function
    return () => {
      cancelled = true;
    };
  }, [userId]); // Dependency — re-run when userId changes

  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  return <UserCard user={user} />;
}

// Subscribing to events
function WindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    function handleResize() {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    }

    handleResize(); // Initial value
    window.addEventListener('resize', handleResize);

    return () => window.removeEventListener('resize', handleResize);
  }, []); // Empty array — only on mount

  return <div>{size.width} x {size.height}</div>;
}

useContext — global state

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

// Creating a context
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

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

// Provider
function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

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

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

// A custom hook to use the context
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// Usage
function ThemeToggle() {
  const { theme, toggleTheme } = useTheme();

  return (
    <button onClick={toggleTheme}>
      Current: {theme}
    </button>
  );
}

useMemo and useCallback — optimization

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

function ProductList({ products, filter }: Props) {
  // useMemo — memoizing computations
  const filteredProducts = useMemo(() => {
    console.log('Filtering products...');
    return products.filter(p =>
      p.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [products, filter]); // Recomputes only when dependencies change

  // useCallback — memoizing functions
  const handleClick = useCallback((id: string) => {
    console.log('Clicked:', id);
  }, []); // The function is not recreated

  return (
    <ul>
      {filteredProducts.map(product => (
        <ProductItem
          key={product.id}
          product={product}
          onClick={handleClick}
        />
      ))}
    </ul>
  );
}

// When to use:
// useMemo — heavy computations, complex filtering
// useCallback — passing functions to memoized child components

useRef — refs and mutable values

import { useRef, useEffect } from 'react';

// A ref to a DOM element
function TextInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} />;
}

// Storing the previous value
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>();

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

  return ref.current;
}

// Storing a mutable value without a re-render
function Timer() {
  const intervalRef = useRef<NodeJS.Timeout>();

  const start = () => {
    intervalRef.current = setInterval(() => {
      console.log('tick');
    }, 1000);
  };

  const stop = () => {
    clearInterval(intervalRef.current);
  };

  return (
    <>
      <button onClick={start}>Start</button>
      <button onClick={stop}>Stop</button>
    </>
  );
}

Custom Hooks — reusing logic

// useLocalStorage
function useLocalStorage<T>(key: string, initialValue: T) {
  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 | ((val: T) => T)) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value;
    setStoredValue(valueToStore);
    window.localStorage.setItem(key, JSON.stringify(valueToStore));
  };

  return [storedValue, setValue] as const;
}

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

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

    fetch(url, { signal: controller.signal })
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));

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

  return { data, loading, error };
}

// useDebounce
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;
}

// Usage
function SearchComponent() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);
  const { data, loading } = useFetch(`/api/search?q=${debouncedQuery}`);

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      {loading ? <Spinner /> : <Results data={data} />}
    </div>
  );
}

Conclusion

React Hooks are a powerful tool for managing state and side effects. Start with useState and useEffect, then learn optimization with useMemo/useCallback. Create custom hooks to reuse logic.

The rules of hooks: call hooks only at the top level of a component, not inside conditions or loops.

Enjoyed the article?

Subscribe to our blog so you don’t miss new posts