Getting Started with React Hooks: A Complete Guide NEW
Learn React Hooks from scratch with practical examples. This beginner-friendly guide covers useState, useEffect, useContext, and custom hooks.
Introduction
React Hooks revolutionized the way we write React components when they were introduced in React 16.8. They allow you to use state and other React features without writing class components, making your code more concise and easier to understand.
In this comprehensive guide, we'll explore the most commonly used React Hooks with practical examples. Whether you're new to React or looking to modernize your codebase, this tutorial will help you master Hooks.
What you'll learn:
- What are React Hooks and why they matter
- Understanding
useStatefor state management - Side effects with
useEffect - Context API with
useContext - Creating custom hooks
- Best practices and common pitfalls
What Are React Hooks?
Hooks are functions that let you "hook into" React features from function components. Before Hooks, you had to use class components to access state and lifecycle methods. Now, you can do everything with functions.
Key Benefits:
- ✅ Simpler, more readable code
- ✅ Better code reusability with custom hooks
- ✅ Easier to test
- ✅ No need for
thiskeyword - ✅ Better separation of concerns
useState: Managing Component State
The useState hook lets you add state to function components. It returns an array with two elements: the current state value and a function to update it.
Basic Syntax
import React, { useState } from 'react';
function Counter() {
// Declare state variable 'count' with initial value 0
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Multiple State Variables
You can use useState multiple times in a single component:
function UserProfile() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [email, setEmail] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
console.log({ name, age, email });
};
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
type="number"
value={age}
onChange={(e) => setAge(Number(e.target.value))}
placeholder="Age"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<button type="submit">Submit</button>
</form>
);
}
State with Objects and Arrays
When working with objects or arrays, always create a new reference:
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
// Create new array with spread operator
setTodos([...todos, { id: Date.now(), text, completed: false }]);
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
return (
<div>
{todos.map(todo => (
<div key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
</div>
))}
</div>
);
}
useEffect: Handling Side Effects
The useEffect hook lets you perform side effects in function components - things like data fetching, subscriptions, or manually changing the DOM.
Basic Usage
import React, { useState, useEffect } from 'react';
function UserData() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// This runs after every render
fetch('https://api.example.com/user/1')
.then(response => response.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, []); // Empty dependency array = run only once on mount
if (loading) return <p>Loading...</p>;
return <div>{user.name}</div>;
}
Dependency Array
The second argument to useEffect is the dependency array. It controls when the effect runs:
// Run on every render
useEffect(() => {
console.log('Runs on every render');
});
// Run only once (on mount)
useEffect(() => {
console.log('Runs once on mount');
}, []);
// Run when specific values change
useEffect(() => {
console.log('Runs when count changes');
}, [count]);
// Multiple dependencies
useEffect(() => {
console.log('Runs when count or name changes');
}, [count, name]);
Cleanup Functions
Return a cleanup function to avoid memory leaks:
useEffect(() => {
// Subscribe to something
const subscription = subscribeToData();
// Cleanup function
return () => {
subscription.unsubscribe();
};
}, []);
Practical Example: Document Title
function PageTitle() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Update title when count changes
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
useContext: Sharing Data Across Components
useContext makes it easy to share data between components without prop drilling.
Creating Context
import React, { createContext, useContext, useState } from 'react';
// Create context
const ThemeContext = createContext();
// Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook to use the context
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// Using the context
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
>
Toggle Theme (Current: {theme})
</button>
);
}
// App component
function App() {
return (
<ThemeProvider>
<ThemedButton />
</ThemeProvider>
);
}
Custom Hooks: Reusable Logic
Custom hooks let you extract component logic into reusable functions. They must start with "use".
Example: useFetch Hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
// Using the custom hook
function UserProfile() {
const { data, loading, error } = useFetch('https://api.example.com/user/1');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <div>{data.name}</div>;
}
Example: useLocalStorage Hook
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setStoredValue = (newValue) => {
try {
setValue(newValue);
window.localStorage.setItem(key, JSON.stringify(newValue));
} catch (error) {
console.error(error);
}
};
return [value, setStoredValue];
}
// Usage
function App() {
const [name, setName] = useLocalStorage('name', '');
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Your name (persisted in localStorage)"
/>
);
}
Best Practices and Common Pitfalls
✅ Do's
- Always call hooks at the top level - Don't call hooks inside loops, conditions, or nested functions
- Use descriptive names -
[user, setUser]is better than[data, setData] - Keep effects focused - One effect per concern
- Extract custom hooks - Reuse logic across components
- Specify dependencies correctly - Include all values used in the effect
❌ Don'ts
- Don't forget cleanup functions - Prevents memory leaks
- Don't mutate state directly - Always use setter functions
- Don't use stale closures - Be careful with async functions in effects
- Don't skip the dependency array - Leads to unexpected behavior
Common Mistakes
// ❌ Wrong: Mutating state directly
const [items, setItems] = useState([]);
items.push(newItem); // DON'T DO THIS
// ✅ Correct: Create new array
setItems([...items, newItem]);
// ❌ Wrong: Missing dependencies
useEffect(() => {
console.log(count); // count is used but not in dependencies
}, []);
// ✅ Correct: Include all dependencies
useEffect(() => {
console.log(count);
}, [count]);
Conclusion
React Hooks have transformed how we write React applications. They make code more readable, reusable, and easier to test. Start with useState and useEffect, get comfortable with useContext, and then create your own custom hooks to encapsulate reusable logic.
Key Takeaways:
- Hooks let you use React features in function components
useStatemanages component stateuseEffecthandles side effects and lifecycleuseContextshares data without prop drilling- Custom hooks extract reusable logic
- Always follow the Rules of Hooks
Further Resources
- Official React Hooks Documentation
- React Hooks Cheat Sheet
- UseHooks - Collection of Custom Hooks
- React Hooks Course on FreeCodeCamp
Ready to practice? Try building a simple todo app or weather app using Hooks. The best way to learn is by doing!
Questions or feedback? Drop a comment below or reach out to me on GitHub or LinkedIn.
Found this helpful? Share it with your friends and fellow developers!