React Hooks Tutorial: Complete Guide from Basics to Advanced (2025)
React Hooks revolutionized how we write React components when they were introduced in React 16.8. If you're still writing class components or struggling to understand hooks, this comprehensive guide will transform you from a beginner to a React Hooks expert with practical examples, best practices, and real-world use cases you can implement today.
📚 Related Learning Resources:
What are React Hooks?
React Hooks are special functions that let you "hook into" React features like state and lifecycle methods from functional components. Before hooks, you needed class components to use state and lifecycle methods. Now, you can do everything with functional components, making your code cleaner, more reusable, and easier to test.
Think of hooks as superpowers for your functional components. They allow you to:
- Add state to functional components without classes
- Share stateful logic between components easily
- Split complex components into smaller functions
- Use React features without learning complex class syntax
- Write more readable and maintainable code
Why React Hooks are Important: Benefits and Advantages
Developer Experience Benefits:
- Simpler Code: No more confusing 'this' keyword and binding methods
- Better Code Reuse: Share logic between components with custom hooks
- Easier Testing: Pure functions are simpler to test than classes
- Modern Standard: 90% of new React code uses hooks
- Better Performance: Functional components with hooks are faster than classes
How React Hooks Work: Core Concepts
The Rules of Hooks:
- Only Call Hooks at the Top Level: Never call hooks inside loops, conditions, or nested functions
- Only Call Hooks from React Functions: Call hooks from functional components or custom hooks
JAVASCRIPT
// ❌ WRONG - Hook inside condition
function MyComponent() {
if (someCondition) {
const [state, setState] = useState(0); // Error!
}
}
// ✅ CORRECT - Hook at top level
function MyComponent() {
const [state, setState] = useState(0);
if (someCondition) {
// Use state here
}
}
useState Hook: Managing Component State
The useState hook is the most fundamental hook. It adds state to functional components.
Basic Usage:
REACT COMPONENT
import React, { useState } from 'react';
function Counter() {
// Declare state variable
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Multiple State Variables:
REACT FORM
function UserForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
const handleSubmit = (e) => {
e.preventDefault();
console.log({ name, email, age });
};
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<button type="submit">Submit</button>
</form>
);
}
useEffect Hook: Handling Side Effects
The useEffect hook lets you perform side effects in functional components. It replaces componentDidMount, componentDidUpdate, and componentWillUnmount.
Basic useEffect:
REACT USEEFFECT
import React, { useState, useEffect } from 'react';
function DocumentTitle() {
const [count, setCount] = useState(0);
// Runs after every render
useEffect(() => {
document.title = `Count: ${count}`;
});
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
useEffect with Dependency Array:
DATA FETCHING
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// Only runs when userId changes
useEffect(() => {
setLoading(true);
fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]); // Dependency array
if (loading) return <p>Loading...</p>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
Cleanup Function:
TIMER WITH CLEANUP
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// Cleanup function
return () => {
clearInterval(interval);
};
}, []); // Empty array = runs once
return <p>Seconds: {seconds}</p>;
}
useContext Hook: Sharing Data Across Components
The useContext hook provides a way to share data across the component tree without prop drilling.
CONTEXT EXAMPLE
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(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Consumer component
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme}>
Toggle Theme (Current: {theme})
</button>
);
}
useReducer Hook: Managing Complex State Logic
The useReducer hook is an alternative to useState for managing complex state logic. It's similar to Redux reducers.
USEREDUCER EXAMPLE
import React, { useReducer } from 'react';
// Reducer function
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
<button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
</div>
);
}
useCallback and useMemo: Performance Optimization
These hooks help optimize performance by preventing unnecessary re-renders and recalculations.
useCallback - Memoize Functions:
USECALLBACK EXAMPLE
import React, { useState, useCallback } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);
// Function is only recreated when dependencies change
const increment = useCallback(() => {
setCount(c => c + 1);
}, []); // Empty deps = never recreated
return (
<div>
<p>Count: {count}</p>
<ChildComponent onIncrement={increment} />
<button onClick={() => setOther(other + 1)}>
Other: {other}
</button>
</div>
);
}
useMemo - Memoize Expensive Calculations:
USEMEMO EXAMPLE
import React, { useState, useMemo } from 'react';
function ExpensiveComponent({ items }) {
const [filter, setFilter] = useState('');
// Expensive calculation only runs when dependencies change
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter items"
/>
<ul>
{filteredItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
useRef Hook: Accessing DOM and Persisting Values
USEREF EXAMPLE
import React, { useRef, useEffect } from 'react';
function FocusInput() {
const inputRef = useRef(null);
useEffect(() => {
// Focus input on mount
inputRef.current.focus();
}, []);
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={() => inputRef.current.focus()}>
Focus Input
</button>
</div>
);
}
Custom Hooks: Creating Reusable Logic
Custom hooks let you extract component logic into reusable functions. They're one of the most powerful features of React Hooks.
useLocalStorage Hook:
CUSTOM HOOK - LOCALSTORAGE
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// Usage
function App() {
const [name, setName] = useLocalStorage('name', 'Guest');
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
);
}
useFetch Hook:
CUSTOM HOOK - DATA FETCHING
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
// Usage
function UserList() {
const { data, loading, error } = useFetch('https://api.example.com/users');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Real-World React Hooks Project: Authentication System
COMPLETE AUTH SYSTEM
// AuthContext.js
import React, { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check if user is logged in on mount
const token = localStorage.getItem('token');
if (token) {
verifyToken(token)
.then(userData => setUser(userData))
.catch(() => localStorage.removeItem('token'))
.finally(() => setLoading(false));
} else {
setLoading(false);
}
}, []);
const login = async (email, password) => {
const { user, token } = await loginAPI(email, password);
localStorage.setItem('token', token);
setUser(user);
};
const logout = () => {
localStorage.removeItem('token');
setUser(null);
};
return (
<AuthContext.Provider value={{ user, loading, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
return useContext(AuthContext);
}
React Hooks vs Class Components
Before Hooks (Class Component):
CLASS COMPONENT
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.increment = this.increment.bind(this);
}
componentDidMount() {
document.title = `Count: ${this.state.count}`;
}
componentDidUpdate() {
document.title = `Count: ${this.state.count}`;
}
increment() {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
With Hooks (Functional Component):
FUNCTIONAL COMPONENT WITH HOOKS
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
Comparison Table:
| Feature |
Class Components |
Hooks |
| Code Length |
More verbose |
More concise |
| Learning Curve |
Complex (this, binding) |
Easier to learn |
| Code Reuse |
HOCs, Render Props |
Custom Hooks |
| Performance |
Good |
Better |
| Bundle Size |
Larger |
Smaller |
| Testing |
More complex |
Easier |
| Future Support |
Maintained but legacy |
Actively developed |
Common React Hooks Mistakes and Best Practices
Common Mistakes to Avoid:
❌ Breaking Rules of Hooks: Always call hooks at the top level
WRONG VS CORRECT
// ❌ WRONG
function MyComponent({ condition }) {
if (condition) {
const [state, setState] = useState(0); // Error!
}
}
// ✅ CORRECT
function MyComponent({ condition }) {
const [state, setState] = useState(0);
if (condition) {
// Use state here
}
}
❌ Missing Dependencies in useEffect: Always include all dependencies
DEPENDENCY ARRAY ISSUE
// ❌ WRONG
function MyComponent({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // Missing userId!
}
// ✅ CORRECT
function MyComponent({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // Include userId
}
Best Practices:
✅ Write Good Hook Patterns:
- Name custom hooks with 'use' prefix
- Keep hooks small and focused
- Extract reusable logic into custom hooks
- Always cleanup side effects
- Use TypeScript for better type safety
Advanced Hooks: useImperativeHandle, useLayoutEffect, useDebugValue
useImperativeHandle - Expose Methods to Parent Components:
USEIMPERATIVEHANDLE
import React, { useRef, useImperativeHandle, forwardRef } from 'react';
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
clear: () => {
inputRef.current.value = '';
}
}));
return <input ref={inputRef} {...props} />;
});
// Parent component
function Parent() {
const inputRef = useRef();
return (
<div>
<FancyInput ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>Focus</button>
<button onClick={() => inputRef.current.clear()}>Clear</button>
</div>
);
}
useLayoutEffect - Synchronous DOM Updates:
USELAYOUTEFFECT
import React, { useState, useLayoutEffect, useRef } from 'react';
function Tooltip() {
const [show, setShow] = useState(false);
const [position, setPosition] = useState({ x: 0, y: 0 });
const buttonRef = useRef();
// Runs synchronously after DOM mutations
useLayoutEffect(() => {
if (show) {
const rect = buttonRef.current.getBoundingClientRect();
setPosition({
x: rect.left + rect.width / 2,
y: rect.top - 10
});
}
}, [show]);
return (
<>
<button
ref={buttonRef}
onMouseEnter={() => setShow(true)}
onMouseLeave={() => setShow(false)}
>
Hover me
</button>
{show && (
<div
style={{
position: 'absolute',
left: position.x,
top: position.y
}}
>
Tooltip
</div>
)}
</>
);
}
Testing React Hooks
Testing hooks is crucial for maintaining reliable applications. Here's how to test hooks effectively:
TESTING HOOKS
import { renderHook, act } from '@testing-library/react-hooks';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('should increment counter', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('should decrement counter', () => {
const { result } = renderHook(() => useCounter(5));
expect(result.current.count).toBe(5);
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
});
Hook Performance Optimization Tips
- Use React.memo for Component Memoization: Prevent unnecessary re-renders of child components
- Implement useCallback for Event Handlers: Memoize functions passed as props
- Apply useMemo for Expensive Calculations: Cache computed values
- Lazy Initial State: Use function form for expensive initial state calculations
- Split State Appropriately: Don't combine unrelated state variables
- Use useReducer for Complex State: Better than multiple useState calls
PERFORMANCE OPTIMIZATION
// Lazy initial state
const [state, setState] = useState(() => {
// Expensive computation runs only once
return computeExpensiveInitialState();
});
// Memoized component with React.memo
const ExpensiveChild = React.memo(({ data, onClick }) => {
console.log('Child rendered');
return <div onClick={onClick}>{data}</div>;
});
// Parent using useCallback
function Parent() {
const [count, setCount] = useState(0);
const [data, setData] = useState('Hello');
// Memoized callback
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // Empty deps = stable reference
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ExpensiveChild data={data} onClick={handleClick} />
</div>
);
}
React 18+ Hooks: Latest Features
React 18 introduced new hooks that enhance performance and user experience:
useTransition - Mark Updates as Non-Urgent:
USETRANSITION
import { useState, useTransition } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (e) => {
const value = e.target.value;
setQuery(value); // Urgent update
startTransition(() => {
// Non-urgent update
const filtered = searchLargeDataset(value);
setResults(filtered);
});
};
return (
<div>
<input value={query} onChange={handleSearch} />
{isPending && <span>Loading...</span>}
<ResultsList results={results} />
</div>
);
}
useDeferredValue - Defer Updates:
USEDEFERREDVALUE
import { useState, useDeferredValue, useMemo } from 'react';
function SearchList() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
const list = useMemo(() =>
generateLargeList(deferredText),
[deferredText]
);
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
<div style={{ opacity: text !== deferredText ? 0.5 : 1 }}>
{list}
</div>
</>
);
}
FAQs About React Hooks
Q1: Can I use hooks in class components?
No, hooks only work in functional components. However, you can gradually migrate to hooks by creating new components as functional components while keeping existing class components.
Q2: Do hooks replace Redux or other state management?
Not necessarily. For simple state management, useContext and useReducer can replace Redux. However, for large applications with complex state logic, Redux still has advantages.
Q3: How do I test components with hooks?
Use React Testing Library which provides utilities like renderHook for testing custom hooks.
Q4: Why am I getting infinite loops with useEffect?
This usually happens when you forget to include a dependency array, or when you're updating state that's in the dependency array without proper checks.
Q5: Should I use one useState or multiple?
Use multiple useState calls when values are independent and change separately. Use one useState with an object when values are closely related.
Q6: What's the difference between useEffect and useLayoutEffect?
useEffect runs asynchronously after render and paint, while useLayoutEffect runs synchronously after render but before paint.
Q7: Can I create my own hooks?
Yes! Custom hooks are one of the most powerful features. Extract any logic that uses hooks into a custom hook by creating a function that starts with "use".
Conclusion
React Hooks have revolutionized how we write React applications, making code more readable, maintainable, and reusable. Throughout this comprehensive guide, we've covered everything from basic hooks like useState and useEffect to advanced patterns like custom hooks, performance optimization, and real-world applications.
The key to mastering React Hooks is practice. Start by converting simple class components to functional components, then gradually explore advanced hooks like useReducer and useContext. Create custom hooks to share logic across your application, and always follow the rules of hooks to avoid common pitfalls.
Ready to level up your React skills? Start refactoring your components with hooks today, build custom hooks for your common patterns, and explore the React ecosystem!
🚀 Next Steps in Your Learning Journey:
Additional Resources
Quick Reference: Essential React Hooks
| Hook |
Purpose |
Basic Usage |
useState |
Add state to components |
const [state, setState] = useState(initial) |
useEffect |
Handle side effects |
useEffect(() => { }, [deps]) |
useContext |
Access context values |
const value = useContext(Context) |
useReducer |
Complex state logic |
const [state, dispatch] = useReducer(reducer, init) |
useCallback |
Memoize functions |
const fn = useCallback(() => { }, [deps]) |
useMemo |
Memoize values |
const value = useMemo(() => calc(), [deps]) |
useRef |
Access DOM/persist values |
const ref = useRef(initial) |
Tags: React Hooks, React Tutorial, JavaScript, Web Development, Frontend Development, useState, useEffect, Custom Hooks, React 18, Modern React, React 2025
Last Updated: November 2025
Author: KnowledgeMarkG Team