Complete Guide to React Hooks: useState, useEffect, useContext & Custom Hooks

React Hooks revolutionized the way we write React components when introduced in React 16.8. They allow you to use state and other React features without writing class components. This comprehensive guide covers everything from basics to advanced patterns.Before Hooks, using state required converting components to classes. With Hooks, functional components can now manage state, handle side effects, and access context—making code simpler, more reusable, and easier to understand.

Why Learn Hooks?
Modern React development requires Hooks. They’re the recommended way to write React components. Mastering Hooks will significantly improve your React skills.

useState Hook Deep Dive

The useState Hook is the simplest and most commonly used Hook. It lets you add state to functional components with a clean, intuitive API.

Basic Syntax

const [state, setState] = useState(initialValue);

useState returns an array with two elements: the current state value and a function to update it.

Simple Counter Example

import { useState } from ‘react’;function Counter() {
const [count, setCount] = useState(0);return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}

Multiple State Variables

You can use multiple useState hooks in a single component:

function UserForm() {
const [name, setName] = useState();
const [email, setEmail] = useState();
const [age, setAge] = useState(0);return (
<form>
<input value={name} onChange={(e) => setName(e.target.value)}/>
<input value={email} onChange={(e) => setEmail(e.target.value)}/>
<input type=”number” value={age} onChange={(e) => setAge(e.target.value)}/>
</form>
);
}

Updating Objects and Arrays

When updating objects or arrays, create new objects/arrays rather than mutating:

// Correct: Create new object
const [user, setUser] = useState({ name: ‘John’, age: 30 });
setUser({ …user, age: 31 });// Correct: Create new array
const [items, setItems] = useState([]);
setItems([…items, newItem]);
⚠️ Common Mistake: Never mutate state directly. React relies on reference equality. Always create new objects/arrays when updating state.

useEffect Hook for Side Effects

useEffect lets you perform side effects in functional components: data fetching, subscriptions, manual DOM changes, and more.

Basic Syntax

useEffect(() => {
// Side effect code runs after renderreturn () => {
// Cleanup function (optional)
};
}, [dependencies]);

Data Fetching Example

function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);useEffect(() => {
const fetchUser = async () => {
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);if (loading) return <p>Loading…</p>;
return <div><p>{user?.name}</p></div>;
}

Dependency Array Explained

Dependency Array When Effect Runs Use Case
No array After every render Rarely needed, can cause issues
[] Only on mount Initialize data, setup subscriptions
[dependency] When dependency changes React to prop/state changes

Cleanup Functions

Return a cleanup function to remove subscriptions or cleanup timers:

useEffect(() => {
const handleResize = () => console.log(‘Resized’);
window.addEventListener(‘resize’, handleResize);return () => {
window.removeEventListener(‘resize’, handleResize);
};
}, []);
✅ Best Practice: Always cleanup subscriptions and listeners to prevent memory leaks.

useContext for Global State

useContext allows you to access context values without prop drilling through many levels of components.

Creating and Providing Context

import { createContext, useState } from ‘react’;export const ThemeContext = createContext();

export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(‘light’);

return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}

Consuming Context

import { useContext } from ‘react’;
import { ThemeContext } from ‘./ThemeProvider’;function Header() {
const { theme, setTheme } = useContext(ThemeContext);return (
<header style={{ background: theme === ‘dark’ ? ‘#333’ : ‘#fff’ }}>
<button onClick={() => setTheme(theme === ‘dark’ ? ‘light’ : ‘dark’)}>
Toggle Theme
</button>
</header>
);
}
When to Use Context: Use for global state like theme, language, or authentication. For complex state, consider Redux or Zustand.

Building Custom Hooks

Custom Hooks are JavaScript functions that use other Hooks. They let you extract logic and reuse it across components.

Rules for Custom Hooks

  • Must start with “use” (e.g., useFetch, useWindowSize)
  • Can only call other Hooks
  • Must follow the Rules of Hooks

useFetch Custom Hook

function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url);
const json = await res.json();
setData(json);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);return { data, loading, error };
}

Using Your Custom Hook

function PostsList() {
const { data: posts, loading } = useFetch(‘https://api.example.com/posts’);if (loading) return <p>Loading…</p>;
return (
<ul>
{posts?.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
);
}

Rules of Hooks

React enforces two important rules about Hooks through a linter plugin:

1. Only Call Hooks at the Top Level

Don’t call Hooks inside loops, conditions, or nested functions:

❌ Don’t

if (condition) {
const [state] = useState(0);
}

✅ Do

const [state] = useState(0);
if (condition) {
// use state here
}

2. Only Call Hooks from React Functions

Only call Hooks from React components or custom Hooks:

❌ Don’t

function helper() {
const [state] = useState(0);
}

✅ Do

function Component() {
const [state] = useState(0);
}

Best Practices

1. Separate Concerns with Multiple Effects

useEffect(() => {
// Fetch data
}, [dep1]);useEffect(() => {
// Setup event listener
}, [dep2]);

2. Handle Loading and Error States

const { data, loading, error } = useFetch(url);if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
return <DataDisplay data={data} />;

3. Extract Complex Logic into Custom Hooks

Keep components clean by extracting complex logic into reusable custom Hooks. This improves readability and maintainability.

4. Use Proper Keys in Lists

// Good: Unique, stable ID
{items.map(item => <Item key={item.id} item={item} />)}// Avoid: Index as key
{items.map((item, i) => <Item key={i} item={item} />)}
© 2025 JavaScript UX. Complete Guide to React Hooks.

Leave a Reply

Your email address will not be published. Required fields are marked *