About us Guides Projects Contacts
Админка
please wait

React Hooks transformed how we write React components, enabling state and lifecycle features in functional components. Understanding hooks deeply—beyond useState and useEffect—unlocks powerful patterns for building maintainable React applications. This guide covers mastering React Hooks from a senior developer's perspective.

Why Hooks Matter

Hooks revolutionized React development:

  1. Simpler Components: No more class boilerplate
  2. Logic Reuse: Custom hooks share stateful logic
  3. Composition: Combine small hooks into complex behavior
  4. Testability: Hooks are easier to test than lifecycle methods
  5. TypeScript: Better type inference than class components

Core Hooks

useState: Component State

import { useState } from 'react';
function Counter() {
// Basic usage
const [count, setCount] = useState(0);
// Lazy initialization (expensive computation)
const [data, setData] = useState(() => {
return computeExpensiveInitialValue();
});
// Object state
const [user, setUser] = useState({ name: '', email: '' });
// Update with callback (access previous value)
const increment = () => {
setCount(prev => prev + 1);
};
// Update object (spread to maintain other fields)
const updateName = (name) => {
setUser(prev => ({ ...prev, name }));
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
<input
value={user.name}
onChange={(e) => updateName(e.target.value)}
/>
</div>
);
}

useEffect: Side Effects

import { useEffect, useState } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// Run on every render (rarely needed)
useEffect(() => {
console.log('Component rendered');
});
// Run once on mount
useEffect(() => {
console.log('Component mounted');
// Cleanup on unmount
return () => {
console.log('Component unmounted');
};
}, []);
// Run when dependency changes
useEffect(() => {
let cancelled = false;
async function fetchUser() {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
// Prevent state update if the component is unmounted
if (!cancelled) {
setUser(data);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchUser();
// Cleanup: cancel pending request
return () => {
cancelled = true;
};
}, [userId]); // Re-run when userId changes
if (loading) return <div>Loading...</div>;
return <div>{user?.name}</div>;
}

useContext: Shared State

import { createContext, useContext, useState } from 'react';
// Create context
const ThemeContext = createContext(null);
// Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook for consuming context
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// Usage
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
>
Toggle Theme
</button>
);
}

useReducer: Complex State

import { useReducer } from 'react';
// Reducer function
function todoReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, { id: Date.now(), text: action.text, done: false }];
case 'TOGGLE':
return state.map(todo =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
);
case 'DELETE':
return state.filter(todo => todo.id !== action.id);
case 'CLEAR_COMPLETED':
return state.filter(todo => !todo.done);
default:
throw new Error(`Unknown action: ${action.type}`);
}
}
function TodoList() {
const [todos, dispatch] = useReducer(todoReducer, []);
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
dispatch({ type: 'ADD', text });
setText('');
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => dispatch({ type: 'TOGGLE', id: todo.id })}
/>
<span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'DELETE', id: todo.id })}>
Delete
</button>
</li>
))}
</ul>
<button onClick={() => dispatch({ type: 'CLEAR_COMPLETED' })}>
Clear Completed
</button>
</div>
);
}

Performance Hooks

useMemo: Memoize Values

import { useMemo, useState } from 'react';
function ExpensiveList({ items, filter }) {
// Only recalculate when items or filter change
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
// Memoize complex objects for child props
const sortedItems = useMemo(() => {
return [...filteredItems].sort((a, b) => a.name.localeCompare(b.name));
}, [filteredItems]);
return (
<ul>
{sortedItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}

useCallback: Memoize Functions

import { useCallback, useState, memo } from 'react';
// Memoized child component
const Button = memo(function Button({ onClick, label }) {
console.log(`Rendering ${label}`);
return <button onClick={onClick}>{label}</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// Without useCallback, this creates a new function every render
// const handleClick = () => setCount(c => c + 1);
// With useCallback, the function reference stays stable
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
// If the callback needs current state, include it in dependencies
const handleReset = useCallback(() => {
setCount(0);
setName('');
}, []);
return (
<div>
<p>Count: {count}</p>
<input value={name} onChange={(e) => setName(e.target.value)} />
{/*Button won't re-render when name changes*/}
<Button onClick={handleClick} label="Increment" />
<Button onClick={handleReset} label="Reset" />
</div>
);
}

useRef: Mutable Reference

import { useRef, useEffect, useState } from 'react';
function FocusInput() {
const inputRef = useRef(null);
// Focus on mount
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
}
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
const stop = () => {
clearInterval(intervalRef.current);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={stop}>Stop</button>
</div>
);
}
// Track previous value
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function Component({ value }) {
const prevValue = usePrevious(value);
return (
<p>
Current: {value}, Previous: {prevValue}
</p>
);
}

Custom Hooks

Data Fetching Hook

import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const result = await response.json();
if (!cancelled) {
setData(result);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true;
};
}, [url]);
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>
);
}

Local Storage Hook

import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Initialize from localStorage or use the initial value
const [value, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// Sync to localStorage when the value changes
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
}, [key, value]);
return [value, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [fontSize, setFontSize] = useLocalStorage('fontSize', 16);
return (
<div>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<input
type="range"
min="12"
max="24"
value={fontSize}
onChange={(e) => setFontSize(Number(e.target.value))}
/>
</div>
);
}

Debounce Hook

import { useState, useEffect } from 'react';
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// Usage: Search with debounce
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearch = useDebounce(searchTerm, 500);
const { data: results } = useFetch(
debouncedSearch ? `/api/search?q=${debouncedSearch}` : null
);
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<ul>
{results?.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}

Window Size Hook

import { useState, useEffect } from 'react';
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// Usage
function ResponsiveComponent() {
const { width } = useWindowSize();
return (
<div>
{width < 768 ? <MobileView /> : <DesktopView />}
</div>
);
}

Rules of Hooks

  1. Only call at top level: Never in loops, conditions, or nested functions
  2. Only call in React functions: Components or custom hooks
  3. Name custom hooks with "use": Enables lint rule checking
// BAD
function Component({ condition }) {
if (condition) {
const [value, setValue] = useState(0); // Error!
}
}
// GOOD
function Component({ condition }) {
const [value, setValue] = useState(0);
if (condition) {
// Use value here
}
}

Key Takeaways

  1. useState for simple state: Objects need spread for updates
  2. useEffect with cleanup: Prevent memory leaks and race conditions
  3. useCallback/useMemo carefully: Premature optimization is costly
  4. Custom hooks for reuse: Extract common patterns
  5. useReducer for complex state: Better than multiple useState
  6. Dependency arrays matter: Missing deps cause bugs

Hooks are the foundation of modern React—master them, and you'll write cleaner, more maintainable components.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive