Essential React Hooks
Learn how to use essential React hooks to build faster and more efficient web applications.
What are Hooks?
Hooks are a new addition in React 16.8 that allow you to use state and other React features without writing a class. They solve problems like:
- Reusing stateful logic between components
- Complex components becoming hard to understand
- Confusing class concepts for developers
Basic Hooks
useState
The useState hook lets you add state to functional components.
import { useState } from 'react';
const Counter = () => {
// Declare a state variable called "count" with initial value 0
const [count, setCount] = useState<number>(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
};Key points:
- Returns the current state and a function to update it
- The initial state is set only on the first render
- State updates trigger a re-render
- You can use multiple useState hooks in a single component
useEffect
The useEffect hook lets you perform side effects in function components.
import { useState, useEffect } from 'react';
const UserProfile = ({ userId }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(true);
// Effect for fetching user data
useEffect(() => {
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // Only re-run if userId changes
// Effect for updating document title
useEffect(() => {
document.title = user ? `${user.name}'s Profile` : 'Loading...';
}, [user]); // Only re-run if user changes
if (loading) return <div>Loading...</div>;
return (
<div>
<h1>{user?.name}</h1>
<p>{user?.email}</p>
</div>
);
};Key points:
- Runs after every render by default
- Accepts a dependency array to control when it runs
- Can return a cleanup function
- Replaces componentDidMount, componentDidUpdate, and componentWillUnmount
useContext
The useContext hook lets you subscribe to React context without introducing nesting.
import { createContext, useContext, useState } from 'react';
// Create a context
interface ThemeContextType {
theme: string;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// Context provider component
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState<string>('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
// Component that uses the context
const ThemedButton = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('ThemedButton must be used within a ThemeProvider');
}
const { theme, toggleTheme } = context;
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff'
}}
>
Toggle Theme
</button>
);
};Key points:
- Accepts a context object and returns the current context value
- Component re-renders when the context value changes
- Still need a Context.Provider up in the component tree
useReducer
The useReducer hook is an alternative to useState for managing complex state logic.
import { useReducer } from 'react';
// Define the state type
interface Todo {
id: number;
text: string;
completed: boolean;
}
// Define action types
type TodoAction =
| { type: 'ADD_TODO'; text: string }
| { type: 'TOGGLE_TODO'; id: number }
| { type: 'DELETE_TODO'; id: number };
// Reducer function
const todoReducer = (state: Todo[], action: TodoAction): Todo[] => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: Date.now(),
text: action.text,
completed: false
}
];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
);
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
};
const TodoApp = () => {
const [todos, dispatch] = useReducer(todoReducer, []);
const [text, setText] = useState<string>('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (text.trim()) {
dispatch({ type: 'ADD_TODO', text });
setText('');
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={e => setText(e.target.value)}
placeholder="Add a todo"
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}
onClick={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })}
>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'DELETE_TODO', id: todo.id })}>
Delete
</button>
</li>
))}
</ul>
</div>
);
};Key points:
- Better for complex state logic with multiple sub-values
- Similar pattern to Redux
- Predictable state updates
Predictable state updates
useCallback
The useCallback hook returns a memoized callback function.
import { useState, useCallback } from 'react';
const ChildComponent = ({ onClick, value }) => {
console.log('Child rendered');
return <button onClick={() => onClick(value)}>Click me</button>;
};
// Memoize the child component to prevent unnecessary re-renders
const MemoizedChild = React.memo(ChildComponent);
const ParentComponent = () => {
const [count, setCount] = useState<number>(0);
const [value, setValue] = useState<number>(1);
// Without useCallback, this function would be recreated on every render
// causing MemoizedChild to re-render even when its props haven't changed
const increment = useCallback((amount: number) => {
setCount(prevCount => prevCount + amount);
}, []); // Empty dependency array means this function is created once
return (
<div>
<p>Count: {count}</p>
<MemoizedChild onClick={increment} value={value} />
<button onClick={() => setValue(v => v + 1)}>Change value ({value})</button>
</div>
);
};Key points:
- Returns a memoized version of the callback
- Only changes if dependencies change
- Useful when passing callbacks to optimized child components
useMemo
The useMemo hook returns a memoized value.
import { useState, useMemo } from 'react';
const ExpensiveCalculationComponent = ({ number }) => {
// This expensive calculation will only re-run when 'number' changes
const calculatedValue = useMemo(() => {
console.log('Calculating expensive value...');
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += number;
}
return result;
}, [number]); // Only recalculate when number changes
return <div>Calculated value: {calculatedValue}</div>;
};
const App = () => {
const [number, setNumber] = useState<number>(1);
const [isDark, setIsDark] = useState<boolean>(false);
return (
<div style={{ background: isDark ? 'black' : 'white', color: isDark ? 'white' : 'black' }}>
<input
type="number"
value={number}
onChange={e => setNumber(parseInt(e.target.value))}
/>
<ExpensiveCalculationComponent number={number} />
<button onClick={() => setIsDark(prev => !prev)}>Toggle Theme</button>
</div>
);
};Key points:
- Returns a memoized value
- Only recalculates when dependencies change
- Useful for expensive calculations
useRef
The useRef hook returns a mutable ref object.
import { useRef, useEffect } from 'react';
const FocusInput = () => {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// Focus the input element on component mount
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
const handleClick = () => {
// Focus the input element on button click
if (inputRef.current) {
inputRef.current.focus();
}
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>Focus Input</button>
</div>
);
};
// Another use case: storing previous values
const usePrevious = <T,>(value: T): T | undefined => {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
};
const CounterWithPrevious = () => {
const [count, setCount] = useState<number>(0);
const prevCount = usePrevious<number>(count);
return (
<div>
<p>Current: {count}, Previous: {prevCount}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
};Key points:
- Returns a mutable ref object with a .current property
- Changing .current doesn't cause a re-render
- Useful for accessing DOM elements and storing mutable values
Building APIs with Next.js
Learn how to build APIs with Next.js, including setting up your project, understanding the App Router and Route Handlers, handling multiple HTTP methods, implementing dynamic routing, creating reusable middleware logic, and deciding when to spin up a dedicated API layer.
Fix "Internal NoFallbackError" in Next.js App Router
Practical steps to resolve NoFallbackError caused by static params, notFound/redirect handling, Node fallbacks, and API responses.