React Hooks Explained for Beginners: A Practical Guide
Introduction
If you’re learning React, you’ve probably heard about Hooks. Introduced in React 16.8, Hooks revolutionized how we write React components by allowing us to use state and other React features without writing classes. In this guide, I’ll explain React Hooks in a beginner-friendly way with practical examples you can start using in your projects right away.
What Are React Hooks and Why Should You Use Them?
React Hooks are functions that let you “hook into” React state and lifecycle features from function components. Before Hooks, you had to use class components for these features.
Benefits of using Hooks:
- Write cleaner, more concise code
- Reuse stateful logic between components
- Organize related code together (instead of splitting it across lifecycle methods)
- Avoid the confusion of
this
keyword in JavaScript classes
The Most Important Hooks
Let’s explore the most commonly used Hooks with practical examples.
useState: Managing Component State
The useState
Hook lets you add state to function components.
Basic Example
import React, { useState } from 'react';
function Counter() {
// Declare a state variable called "count" with initial value of 0
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Breaking this down:
- We import the
useState
Hook from React - Inside our component, we call
useState(0)
to create a state variable (count
) with an initial value of 0 useState
returns an array with two items: the current state value and a function to update it- We use array destructuring to assign names to these items:
[count, setCount]
- When the button is clicked, we call
setCount
with the new value
Managing Multiple State Values
You can call useState
multiple times in a component:
function UserForm() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
return (
<form>
<input
type="text"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
placeholder="First name"
/>
<input
type="text"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
placeholder="Last name"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
</form>
);
}
Using Objects with useState
For related state values, you can use an object:
function UserForm() {
const [user, setUser] = useState({
firstName: '',
lastName: '',
email: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setUser({
...user, // Important: spread the existing user properties
[name]: value // Update only the changed field
});
};
return (
<form>
<input
type="text"
name="firstName"
value={user.firstName}
onChange={handleChange}
placeholder="First name"
/>
<input
type="text"
name="lastName"
value={user.lastName}
onChange={handleChange}
placeholder="Last name"
/>
<input
type="email"
name="email"
value={user.email}
onChange={handleChange}
placeholder="Email"
/>
</form>
);
}
Note: When updating an object with useState
, you must create a new object that includes all existing properties (using the spread operator ...
) plus your changes. React won’t automatically merge objects like it does with this.setState()
in class components.
useEffect: Performing Side Effects
The useEffect
Hook lets you perform side effects in function components. Side effects include data fetching, subscriptions, manual DOM manipulations, and more.
Basic Example
import React, { useState, useEffect } from 'react';
function DocumentTitleUpdater() {
const [count, setCount] = useState(0);
// This runs after every render
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
In this example, useEffect
updates the document title after every render, reflecting the current count.
Controlling When Effects Run
You can control when effects run by providing a dependency array:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// This effect runs when the component mounts
// and whenever userId changes
setLoading(true);
fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(data => {
setUser(data);
setLoading(false);
})
.catch(error => {
console.error("Error fetching user:", error);
setLoading(false);
});
}, [userId]); // Only re-run if userId changes
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
The dependency array [userId]
tells React to only re-run the effect when userId
changes.
Cleaning Up Effects
Some effects need cleanup, like subscriptions or timers:
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
// Return a cleanup function
return () => {
clearInterval(intervalId);
};
}, []); // Empty array means "run only on mount and unmount"
return <div>Seconds: {seconds}</div>;
}
The cleanup function runs when the component unmounts or before the effect runs again.
useContext: Accessing Context
The useContext
Hook provides a way to pass data through the component tree without manually passing props down at every level.
Setting Up Context
import React, { createContext, useState, useContext } from 'react';
// Create a context
const ThemeContext = createContext();
// Create a provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
// The value prop contains what we want to expose to consumers
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
Using Context with useContext
function ThemedButton() {
// Get values from context
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff',
border: '1px solid',
padding: '8px 16px',
}}
>
Toggle Theme
</button>
);
}
// Using the provider in your app
function App() {
return (
<ThemeProvider>
<div className="app">
<h1>Context Example</h1>
<ThemedButton />
</div>
</ThemeProvider>
);
}
This allows ThemedButton
to access the theme context without passing props through intermediate components.
Creating Custom Hooks
One of the most powerful features of Hooks is the ability to create your own custom Hooks to extract and reuse stateful logic.
Example: useLocalStorage
Let’s create a custom Hook that syncs state with localStorage:
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Initialize state with value from localStorage or 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;
}
});
// Update localStorage when state changes
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
Using the custom Hook:
function SavedNotes() {
const [notes, setNotes] = useLocalStorage('notes', []);
const [currentNote, setCurrentNote] = useLocalStorage('currentNote', '');
const addNote = () => {
if (currentNote.trim()) {
setNotes([...notes, currentNote]);
setCurrentNote('');
}
};
return (
<div>
<h2>Saved Notes</h2>
<textarea
value={currentNote}
onChange={(e) => setCurrentNote(e.target.value)}
placeholder="Write a note..."
/>
<button onClick={addNote}>Save Note</button>
<ul>
{notes.map((note, index) => (
<li key={index}>{note}</li>
))}
</ul>
</div>
);
}
This custom Hook encapsulates all the logic for reading from and writing to localStorage, making it reusable across components.
Example: useFetch
Here’s another custom Hook for data fetching:
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortController = new AbortController();
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url, { signal: abortController.signal });
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const result = await response.json();
setData(result);
setError(null);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
setData(null);
}
} finally {
setLoading(false);
}
}
fetchData();
// Cleanup: abort fetch if component unmounts or url changes
return () => abortController.abort();
}, [url]);
return { data, loading, error };
}
Using the custom Hook:
function UserList() {
const { data, loading, error } = useFetch('https://api.example.com/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!data) return <div>No data</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Common Hooks Mistakes and How to Avoid Them
Mistake 1: Missing Dependencies in useEffect
// ❌ Incorrect: missing dependency
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
fetchResults(query).then(data => setResults(data));
}, []); // Missing 'query' in dependencies
// ...
}
// ✅ Correct: include all dependencies
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
fetchResults(query).then(data => setResults(data));
}, [query]); // 'query' included in dependencies
// ...
}
Mistake 2: Creating Functions Inside useEffect
// ❌ Inefficient: creates a new function on every render
function UserProfile({ userId }) {
useEffect(() => {
// This function is recreated on every render
const fetchUser = async () => {
const response = await fetch(`/api/users/${userId}`);
// ...
};
fetchUser();
}, [userId]);
// ...
}
// ✅ Better: use useCallback for functions needed in effects
function UserProfile({ userId }) {
const fetchUser = useCallback(async () => {
const response = await fetch(`/api/users/${userId}`);
// ...
}, [userId]);
useEffect(() => {
fetchUser();
}, [fetchUser]); // fetchUser is stable between renders if userId doesn't change
// ...
}
Mistake 3: Not Using Functional Updates
// ❌ Potential issue: uses stale state
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
// This might use a stale value of count
setCount(count + 1);
};
// ...
}
// ✅ Better: use functional updates
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
// This always uses the latest state
setCount(prevCount => prevCount + 1);
};
// ...
}
Rules of Hooks
To use Hooks correctly, follow these two rules:
- Only call Hooks at the top level - Don’t call Hooks inside loops, conditions, or nested functions.
// ❌ Wrong: Hook inside a condition
function Component() {
if (someCondition) {
useEffect(() => {
// This breaks the rules
});
}
}
// ✅ Correct: Condition inside the Hook
function Component() {
useEffect(() => {
if (someCondition) {
// This is fine
}
});
}
- Only call Hooks from React function components or custom Hooks - Don’t call Hooks from regular JavaScript functions.
Conclusion
React Hooks provide a more intuitive way to work with state and side effects in function components. By understanding useState
, useEffect
, and useContext
, you have the foundation to build powerful React applications. Custom Hooks allow you to extract and reuse logic, making your code more modular and maintainable.
Remember these key points:
- Use
useState
to add state to function components - Use
useEffect
for side effects like data fetching and DOM manipulation - Use
useContext
to consume context without prop drilling - Create custom Hooks to reuse stateful logic across components
- Follow the Rules of Hooks for correct behavior
With these fundamentals, you’re well on your way to mastering React Hooks and building more efficient React applications.