React Hooks And React Custom Hooks
Since their introduction in React 16.8, Hooks have revolutionized the way we write React components, enabling functional components to maintain internal state, manage side effects, and share logic without resorting to higher‑order components or render props. This article provides an exhaustive overview of all the standard React Hooks and showcases practical custom Hook examples that you can adapt for your own projects. Whether you’re just getting started or looking to deepen your understanding, this guide has you covered.
Table of Contents
- Why Hooks?
- Rules of Hooks
- Overview of Built‑In Hooks
- useState
- useEffect
- useContext
- useReducer
- useCallback
- useMemo
- useRef
- useLayoutEffect
- useImperativeHandle
- useDebugValue
- Advanced Hooks Patterns
- Building Custom Hooks
- Fetching Data: useFetch
- Form Handling: useFormFields
- Window Size: useWindowSize
- Previous Value: usePrevious
- Local Storage Sync: useLocalStorage
- Combining Hooks and Context
- Performance Considerations
- Testing Hooks
- Best Practices and Pitfalls
- Conclusion
Why Hooks?
Before Hooks, React developers often relied on class components to access lifecycle methods and manage state. This approach led to verbose code, complex this binding issues, and tangled logic when sharing stateful behavior. Hooks solve these problems by enabling:
- Stateful logic in functional components
- Reusable abstractions without HOCs or render props
- Cleaner separation of concerns
- Simpler component hierarchies
Rules of Hooks
- Only call Hooks at the top level. Don’t call Hooks inside loops, conditions, or nested functions.
- Only call Hooks from React functions. Call them from React functional components or custom Hooks, not regular JavaScript functions.
These rules ensure consistent Hook invocation order across renders.
Overview of Built‑In Hooks
useState
The most basic Hook, useState, lets you add state to functional components.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Current count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}- Signature: const [state, setState] = useState(initialValue)
- The initial state can be a value or a lazy initializer function.
useEffect
Handles side effects—data fetching, subscriptions, manually changing the DOM, etc.
import { useEffect, useState } from 'react';
function DataFetcher({ url }) {
const [data, setData] = useState(null);
useEffect(() => {
let isMounted = true;
fetch(url)
.then(res => res.json())
.then(json => {
if (isMounted) setData(json);
});
return () => {
isMounted = false;
};
}, [url]);
if (!data) return <div>Loading...</div>;
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}- Dependencies array controls when the effect runs.
- Cleanup function helps avoid memory leaks.
useContext
Access context values without <Context.Consumer>.
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button style={{ background: theme.background, color: theme.foreground }}>Click me</button>;
}- Eliminates prop‑drilling for deeply nested components.
useReducer
An alternative to useState for complex state logic.
import { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>−</button>
</div>
);
}- Great for managing complex state transitions.
useCallback
Returns a memoized version of a callback, useful for passing stable references to child components.
import { useCallback, useState } from 'react';
function ExpensiveChild({ onAction }) {
// ...expensive rendering
}
function Parent() {
const [count, setCount] = useState(0);
const handleAction = useCallback(() => {
// ...do something
}, [/* dependencies */]);
return (
<>
<ExpensiveChild onAction={handleAction} />
<button onClick={() => setCount(count + 1)}>Increment</button>
</>
);
}- Prevents unnecessary re‑renders.
useMemo
Memoizes an expensive computed value.
import { useMemo, useState } from 'react';
function Fibonacci({ n }) {
const fibN = useMemo(() => {
function fib(num) {
return num <= 1 ? num : fib(num - 1) + fib(num - 2);
}
return fib(n);
}, [n]);
return <div>Fibonacci({n}) = {fibN}</div>;
}- Only recalculates when dependencies change.
useRef
Stores a mutable value that persists across renders, and can hold a DOM reference.
import { useRef } from 'react';
function TextInputWithFocusButton() {
const inputRef = useRef(null);
return (
<>
<input ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Focus Input</button>
</>
);
}- ref.current can be any value (not just DOM nodes).
useLayoutEffect
Similar to useEffect, but fires synchronously after all DOM mutations. Use sparingly—for reading layout and synchronously re‑rendering.
useImperativeHandle
Customize the instance value exposed to parent components when using ref with forwardRef.
import { forwardRef, useImperativeHandle, useRef } from 'react';
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} />;
});useDebugValue
Display a label for custom Hooks in React DevTools.
import { useDebugValue, useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useDebugValue(isOnline ? 'Online' : 'Offline');
useEffect(() => {
// subscribe to friend status
}, [friendID]);
return isOnline;
}Advanced Hooks Patterns
- Custom dependency hooks (e.g., useUpdateEffect that skips initial mount)
- Hook composition: combine multiple built‑in Hooks into a single abstraction
- Dynamic Hooks: change dependencies or logic at runtime
Building Custom Hooks
Custom Hooks let you extract reusable stateful logic. They are just JavaScript functions whose names start with “use” and may call other Hooks.
1. Fetching Data: useFetch
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 mounted = true;
setLoading(true);
fetch(url)
.then(res => {
if (!res.ok) throw new Error('Network error');
return res.json();
})
.then(json => {
if (mounted) {
setData(json);
setLoading(false);
}
})
.catch(err => {
if (mounted) {
setError(err);
setLoading(false);
}
});
return () => (mounted = false);
}, [url]);
return { data, loading, error };
}Usage:
function UsersList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <div>Loading…</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}2. Form Handling: useFormFields
import { useState } from 'react';
function useFormFields(initialState) {
const [fields, setFields] = useState(initialState);
function handleChange(e) {
setFields({
...fields,
[e.target.name]: e.target.value
});
}
return [fields, handleChange];
}Usage:
function SignupForm() {
const [fields, handleChange] = useFormFields({ email: '', password: '' });
return (
<form>
<input name="email" value={fields.email} onChange={handleChange} />
<input name="password" type="password" value={fields.password} onChange={handleChange} />
<button type="submit">Sign Up</button>
</form>
);
}3. Window Size: useWindowSize
import { useState, useEffect } from 'react';
function useWindowSize() {
const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight });
useEffect(() => {
function onResize() {
setSize({ width: window.innerWidth, height: window.innerHeight });
}
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return size;
}Usage:
function DisplayWindowSize() {
const { width, height } = useWindowSize();
return <div>Width: {width}, Height: {height}</div>;
}4. Previous Value: usePrevious
import { useRef, useEffect } from 'react';
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}Usage:
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
Now: {count}, Before: {prevCount}
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}5. Local Storage Sync: useLocalStorage
import { useState } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (e) {
console.warn(e);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = typeof value === 'function' ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (e) {
console.warn(e);
}
};
return [storedValue, setValue];
}Usage:
function RememberMeToggle() {
const [remember, setRemember] = useLocalStorage('rememberMe', false);
return (
<label>
<input type="checkbox" checked={remember} onChange={e => setRemember(e.target.checked)} />
Remember Me
</label>
);
}Combining Hooks and Context
You can build more powerful abstractions by combining useContext with custom Hooks to encapsulate context logic:
// AuthContext.js
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (u) => setUser(u);
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
return useContext(AuthContext);
}Performance Considerations
- Avoid over‑memoization.
- Prefer useMemo for expensive calculations.
- Use React.memo for pure child components.
- Batch state updates where possible.
Testing Hooks
- React Testing Library: renderHook from @testing-library/react-hooks
- Jest: mock timers for useEffect cleanup/testing timeouts
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});Best Practices and Pitfalls
- Keep dependencies arrays accurate.
- Never conditionally call Hooks.
- Extract complex logic into custom Hooks.
- Document your custom Hooks with JSDoc or comments.
Conclusion
React Hooks have dramatically simplified how we build React applications, allowing for cleaner, more modular, and testable code. By mastering both built‑in Hooks and crafting your own custom Hooks, you’ll be able to share and reuse complex logic across your components with ease. Dive in, experiment with the examples above, and watch your development experience transform.