React hooks - useState, useEffect, and custom hooks
Alex Chang
Feb 2026
1 tab
import React, { useState, useEffect, useCallback, useMemo, useRef, useContext } from 'react';
// 1. useState - managing component state
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// Update functions can use previous state
const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter name"
/>
</div>
);
}
// 2. useEffect - side effects
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Runs after render
console.log('Component mounted or userId changed');
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);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchUser();
// Cleanup function
return () => {
cancelled = true;
console.log('Cleanup on unmount or before next effect');
};
}, [userId]); // Re-run when userId changes
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return null;
return <div>{user.name}</div>;
}
// 3. useCallback - memoize functions
function TodoList({ todos, onToggle }) {
const [filter, setFilter] = useState('all');
// Memoize callback to prevent child re-renders
const handleToggle = useCallback((id) => {
onToggle(id);
}, [onToggle]);
const filteredTodos = useMemo(() => {
if (filter === 'active') return todos.filter(t => !t.completed);
if (filter === 'completed') return todos.filter(t => t.completed);
return todos;
}, [todos, filter]);
return (
<div>
<div>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('active')}>Active</button>
<button onClick={() => setFilter('completed')}>Completed</button>
</div>
{filteredTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
))}
</div>
);
}
const TodoItem = React.memo(({ todo, onToggle }) => {
console.log('TodoItem rendered:', todo.id);
return (
<div onClick={() => onToggle(todo.id)}>
{todo.text}
</div>
);
});
// 4. useRef - mutable references
function TextInputWithFocus() {
const inputRef = useRef(null);
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
});
const focusInput = () => {
inputRef.current?.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
<p>Render count: {renderCount.current}</p>
</div>
);
}
// 5. useContext - accessing context
const ThemeContext = React.createContext('light');
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button className={`btn-${theme}`}>
Themed Button
</button>
);
}
// 6. Custom hooks - reusable logic
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// Usage
function App() {
const [name, setName] = useLocalStorage('name', 'Anonymous');
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
);
}
// 7. Custom hook - API data fetching
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() {
try {
setLoading(true);
const response = await fetch(url);
const json = await response.json();
if (!cancelled) {
setData(json);
setError(null);
}
} catch (err) {
if (!cancelled) {
setError(err);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
// Usage
function Users() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error!</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// 8. Custom hook - window size
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>
);
}
1 file · javascript
Explain with highlit
React Hooks enable state and lifecycle features in function components. I use useState to add stateful values that persist between renders. The useEffect hook handles side effects like data fetching, subscriptions, and DOM manipulation. Dependencies array controls when effects run - empty [] means run once on mount. Custom hooks extract reusable logic by composing built-in hooks. The useCallback hook memoizes functions, while useMemo memoizes values. The useRef hook creates mutable references that persist across renders. The useContext hook accesses context without prop drilling. Hooks must follow rules: call at top level, only in function components.