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>
);
}
Related articles
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.



