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