How to Use React Hooks

How to Use React Hooks React Hooks revolutionized the way developers write stateful and side-effect-driven components in React. Introduced in React 16.8, Hooks allow functional components to manage state, lifecycle events, and side effects without the need for class components. This shift not only simplifies code structure but also enhances reusability, readability, and maintainability across larg

Oct 30, 2025 - 13:10
Oct 30, 2025 - 13:10
 0

How to Use React Hooks

React Hooks revolutionized the way developers write stateful and side-effect-driven components in React. Introduced in React 16.8, Hooks allow functional components to manage state, lifecycle events, and side effects without the need for class components. This shift not only simplifies code structure but also enhances reusability, readability, and maintainability across large-scale applications. Before Hooks, developers relied on class components to handle state and lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount. These classes often led to verbose, nested, and hard-to-reuse code. With Hooks, you can now extract and share logic between components using simple functionsmaking your React applications more modular and intuitive.

The adoption of Hooks has become industry standard. Major frameworks and libraries now assume Hooks as the default pattern, and new React documentation prioritizes functional components with Hooks over class-based approaches. Understanding how to use React Hooks is no longer optionalits essential for any modern React developer. Whether youre building a small landing page or a complex enterprise dashboard, mastering Hooks will empower you to write cleaner, more efficient, and more testable code.

This guide provides a comprehensive, step-by-step walkthrough of how to use React Hooks effectively. Youll learn the core HooksuseState, useEffect, useContext, useReducer, and morealong with advanced patterns, best practices, and real-world examples. By the end of this tutorial, youll have the confidence to implement Hooks in any project and avoid common pitfalls that hinder performance and scalability.

Step-by-Step Guide

1. Setting Up Your React Environment

Before diving into Hooks, ensure your development environment is properly configured. React Hooks require React 16.8 or higher. If youre starting a new project, use Create React App (CRA) or Vite for a quick setup.

To create a new React project with CRA, open your terminal and run:

npx create-react-app my-hook-app

Once the installation completes, navigate into the project directory:

cd my-hook-app

Start the development server:

npm start

If youre upgrading an existing project, verify your React version by checking your package.json file. Ensure react and react-dom are at version 16.8.0 or higher. If not, update them:

npm install react@latest react-dom@latest

Modern bundlers like Vite or Next.js also support Hooks out of the box. Vite offers faster build times and is ideal for new projects:

npm create vite@latest my-hook-app -- --template react

Once your environment is ready, youre set to begin using Hooks.

2. Using useState: Managing Local State

The useState Hook is the most commonly used Hook. It allows functional components to manage local statesomething previously only possible in class components.

Heres a basic example of useState:

import React, { useState } from 'react';

function Counter() {

const [count, setCount] = useState(0);

return (

<div>

<p>You clicked {count} times</p>

<button onClick={() => setCount(count + 1)}>Click me</button>

</div>

);

}

export default Counter;

In this example:

  • useState(0) initializes the state variable count with a value of 0.
  • [count, setCount] is an array destructuring assignment. The first element is the current state value; the second is the function to update it.
  • setCount(count + 1) updates the state. React re-renders the component with the new value.

You can use useState for any data type: strings, booleans, objects, or arrays.

Example with an object:

function UserForm() {

const [user, setUser] = useState({ name: '', email: '' });

const handleInputChange = (e) => {

const { name, value } = e.target;

setUser(prevUser => ({

...prevUser,

[name]: value

}));

};

return (

<form>

<input

type="text"

name="name"

value={user.name}

onChange={handleInputChange}

placeholder="Name"

/>

<input

type="email"

name="email"

value={user.email}

onChange={handleInputChange}

placeholder="Email"

/>

</form>

);

}

Always use the updater function form (prev => ...) when the new state depends on the previous state. This avoids race conditions in asynchronous operations.

3. Using useEffect: Handling Side Effects

useEffect replaces lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount. It runs after every render by default, but you can control when it executes using a dependency array.

Basic useEffect example:

import React, { useState, useEffect } from 'react';

function DataFetcher() {

const [data, setData] = useState(null);

useEffect(() => {

fetch('https://jsonplaceholder.typicode.com/posts/1')

.then(response => response.json())

.then(data => setData(data));

}, []); // Empty dependency array = run once after initial render

return (

<div>

{data ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>Loading...</p>}

</div>

);

}

The empty dependency array [] ensures the effect runs only oncesimilar to componentDidMount.

To run the effect on every render, omit the dependency array:

useEffect(() => {

document.title = You clicked ${count} times;

}); // Runs after every render

To run the effect only when specific values change, list them in the dependency array:

useEffect(() => {

fetch(/api/user/${userId})

.then(res => res.json())

.then(setUser);

}, [userId]); // Runs only when userId changes

Always clean up side effects like subscriptions or timers using a return function:

useEffect(() => {

const timer = setInterval(() => {

console.log('Tick');

}, 1000);

return () => {

clearInterval(timer); // Cleanup on unmount

};

}, []);

Without cleanup, you risk memory leaks and unintended behavior, especially in components that mount and unmount frequently.

4. Using useContext: Accessing Global State

useContext lets you consume values from a React Context without wrapping components in a Context.Consumer. Its ideal for avoiding prop drillingpassing data through multiple layers of components.

First, create a context:

// ThemeContext.js

import React from 'react';

const ThemeContext = React.createContext({

theme: 'light',

toggleTheme: () => {}

});

export default ThemeContext;

Then, wrap your app with a Provider:

// App.js

import React, { useState } from 'react';

import ThemeContext from './ThemeContext';

import Header from './Header';

function App() {

const [theme, setTheme] = useState('light');

const toggleTheme = () => {

setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));

};

const value = { theme, toggleTheme };

return (

<ThemeContext.Provider value={value}>

<Header />

</ThemeContext.Provider>

);

}

export default App;

Now, any child component can consume the context using useContext:

// Header.js

import React, { useContext } from 'react';

import ThemeContext from './ThemeContext';

function Header() {

const { theme, toggleTheme } = useContext(ThemeContext);

return ( <header style={{ background: theme === 'dark' ? '

333' : '#fff', color: theme === 'dark' ? '#fff' : '#333' }}>

<h1>My App</h1>

<button onClick={toggleTheme}>Toggle Theme</button>

</header>

);

}

export default Header;

useContext is powerful but should be used judiciously. Overusing context for every piece of state can lead to unnecessary re-renders. Use it for truly global data like themes, user authentication, or language preferences.

5. Using useReducer: Managing Complex State Logic

When state logic becomes complexespecially when it involves multiple sub-values or next state depends on previous stateuseReducer is a better alternative to useState.

useReducer accepts a reducer function and an initial state, returning the current state and a dispatch function.

Example: Managing a shopping cart

// cartReducer.js

export const cartReducer = (state, action) => {

switch (action.type) {

case 'ADD_ITEM':

return {

...state,

items: [...state.items, action.payload],

total: state.total + action.payload.price

};

case 'REMOVE_ITEM':

const itemToRemove = state.items.find(item => item.id === action.payload);

return {

...state,

items: state.items.filter(item => item.id !== action.payload),

total: state.total - itemToRemove.price

};

case 'CLEAR_CART':

return {

items: [],

total: 0

};

default:

return state;

}

};

Now use it in a component:

import React, { useReducer } from 'react';

import { cartReducer } from './cartReducer';

function ShoppingCart() {

const [state, dispatch] = useReducer(cartReducer, {

items: [],

total: 0

});

const addToCart = (product) => {

dispatch({ type: 'ADD_ITEM', payload: product });

};

const removeFromCart = (id) => {

dispatch({ type: 'REMOVE_ITEM', payload: id });

};

return (

<div>

<h2>Cart: {state.items.length} items (${state.total})</h2>

<button onClick={() => addToCart({ id: 1, name: 'Book', price: 25 })}>Add Book</button>

<button onClick={() => removeFromCart(1)}>Remove Book</button>

<ul>

{state.items.map(item => (

<li key={item.id}>{item.name} - ${item.price}</li>

))}

</ul>

</div>

);

}

useReducer makes state transitions predictable and testable. Its especially useful for forms, multi-step wizards, or any state with complex update logic.

6. Custom Hooks: Reusing Logic Across Components

One of the most powerful features of Hooks is the ability to create custom Hooks. These are JavaScript functions that start with use and encapsulate reusable logic.

Example: A custom Hook for fetching data

// useFetch.js

import { useState, useEffect } from 'react';

function useFetch(url) {

const [data, setData] = useState(null);

const [loading, setLoading] = useState(true);

const [error, setError] = useState(null);

useEffect(() => {

fetch(url)

.then(res => {

if (!res.ok) throw new Error('Network response was not ok');

return res.json();

})

.then(setData)

.catch(setError)

.finally(() => setLoading(false));

}, [url]);

return { data, loading, error };

}

export default useFetch;

Now use it in any component:

// UserProfile.js

import React from 'react';

import useFetch from './useFetch';

function UserProfile({ userId }) {

const { data: user, loading, error } = useFetch(https://jsonplaceholder.typicode.com/users/${userId});

if (loading) return <p>Loading user...</p>;

if (error) return <p>Error: {error.message}</p>;

return (

<div>

<h2>{user.name}</h2>

<p>Email: {user.email}</p>

</div>

);

}

Custom Hooks promote DRY (Dont Repeat Yourself) principles. They can manage state, side effects, subscriptions, or even animationsall without tying logic to a specific component.

7. Other Essential Hooks

React provides several other built-in Hooks for specialized use cases:

useCallback

useCallback memoizes a function to prevent unnecessary re-creations on every render. This improves performance when passing callbacks to optimized child components.

const memoizedCallback = useCallback(

() => {

doSomething(a, b);

},

[a, b]

);

Use it when you pass a function as a prop to a memoized child component using React.memo.

useMemo

useMemo memoizes the result of an expensive computation. Only re-computes when dependencies change.

const expensiveValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

Dont overuse useMemo. Only memoize if the computation is costly and the result doesnt change often.

useRef

useRef creates a mutable object that persists across renders. Its commonly used to access DOM elements or store mutable values that dont trigger re-renders.

function TextInputWithFocusButton() {

const inputEl = useRef(null);

const onButtonClick = () => {

inputEl.current.focus();

};

return (

<div>

<input ref={inputEl} type="text" />

<button onClick={onButtonClick}>Focus the input</button>

</div>

);

}

useRef is also useful for storing timers, intervals, or previous values:

const prevCountRef = useRef();

useEffect(() => {

prevCountRef.current = count;

});

Best Practices

1. Always Follow the Rules of Hooks

React enforces two strict rules for Hooks:

  1. Only call Hooks at the top level. Dont call them inside loops, conditions, or nested functions.
  2. Only call Hooks from React functional components or custom Hooks. Dont call them from regular JavaScript functions.

Violating these rules breaks the internal contract React uses to track state and side effects. The React team provides a linter plugin to catch these errors automatically.

Install the ESLint plugin:

npm install eslint-plugin-react-hooks --save-dev

Add to your .eslintrc:

{

"plugins": ["react-hooks"],

"rules": {

"react-hooks/rules-of-hooks": "error",

"react-hooks/exhaustive-deps": "warn"

}

}

2. Avoid Unnecessary Re-renders

Every time state changes, React re-renders the component and its children. This can lead to performance bottlenecks in large applications.

Use React.memo to prevent re-renders of functional components when props havent changed:

const MyComponent = React.memo(({ data }) => {

// Component logic

});

Combine React.memo with useCallback to avoid passing new function references on every render:

const handleClick = useCallback(() => {

// handler logic

}, [dependency]);

Then pass handleClick to the memoized component.

3. Keep Custom Hooks Focused

Custom Hooks should have a single responsibility. Avoid creating mega-hooks that do too many things. Instead, compose smaller hooks:

// Good: focused hooks

function useLocalStorage(key, initialValue) { ... }

function useApi(url) { ... }

function useWindowSize() { ... }

// Compose them

function useUserData() {

const [user, setUser] = useLocalStorage('user', null);

const { data, loading } = useApi('/api/user');

return { user, setUser, data, loading };

}

4. Clean Up Side Effects

Always return a cleanup function from useEffect when you create subscriptions, timers, event listeners, or WebSocket connections.

Failure to clean up leads to memory leaks and unexpected behavior, especially in development mode with Reacts Strict Mode, which intentionally double-invokes effects to detect issues.

5. Use TypeScript for Type Safety

TypeScript enhances the safety and maintainability of Hooks-based code. Define types for state, actions, and custom Hooks.

interface User {

id: number;

name: string;

email: string;

}

function useUser(id: number): { user: User | null; loading: boolean; error: string | null } {

const [user, setUser] = useState<User | null>(null);

const [loading, setLoading] = useState(true);

const [error, setError] = useState<string | null>(null);

useEffect(() => {

fetch(/api/users/${id})

.then(res => res.json())

.then(setUser)

.catch(setError)

.finally(() => setLoading(false));

}, [id]);

return { user, loading, error };

}

TypeScript catches errors at compile time, reducing runtime bugs and improving developer experience.

6. Prefer useState Over useReducer for Simple State

While useReducer is powerful, it adds boilerplate. Use useState for simple state like toggles, counters, or form inputs. Reserve useReducer for complex state logic with multiple sub-values or actions.

Tools and Resources

1. React DevTools

Install the React DevTools browser extension (available for Chrome and Firefox). It allows you to inspect component trees, view state and props, and track Hook updates in real time. You can even modify state and see changes instantly.

2. ESLint Plugin for React Hooks

As mentioned earlier, the eslint-plugin-react-hooks plugin enforces the Rules of Hooks and warns about missing dependencies in useEffect. Integrate it into your CI pipeline to catch issues early.

3. React Query (TanStack Query)

For data fetching, consider using React Query (formerly React Query). It handles caching, background updates, pagination, and error retry logic out of the box, reducing the need to manually write useFetch hooks.

Install:

npm install @tanstack/react-query

Use:

import { useQuery } from '@tanstack/react-query';

function UserProfile({ userId }) {

const { data, isLoading, error } = useQuery(['user', userId], () =>

fetch(/api/users/${userId}).then(res => res.json())

);

if (isLoading) return <p>Loading...</p>;

if (error) return <p>Error: {error.message}</p>;

return <div>{data.name}</div>;

}

4. Redux Toolkit

For global state management beyond Context, Redux Toolkit simplifies Redux usage with Hooks. It provides createSlice, createAsyncThunk, and useSelector/useDispatch Hooks.

Install:

npm install @reduxjs/toolkit react-redux

5. CodeSandbox and StackBlitz

Use online sandboxes like CodeSandbox or StackBlitz to experiment with Hooks without local setup. They offer live previews, dependency management, and easy sharing.

6. Official React Documentation

Always refer to the official React documentation. Its regularly updated, well-structured, and includes interactive examples.

7. React Hooks Cheatsheet

Bookmark the React Hooks Cheatsheet by the React team. Its a quick reference for when to use each Hook and how to avoid common mistakes.

Real Examples

Example 1: Real-Time Search with Debouncing

Search bars often trigger API calls on every keystroke. This can overload servers. Use useEffect with useCallback and setTimeout to debounce input.

import React, { useState, useEffect, useCallback } from 'react';

function SearchBox() {

const [query, setQuery] = useState('');

const [results, setResults] = useState([]);

const fetchResults = useCallback(async (q) => {

if (!q) {

setResults([]);

return;

}

const response = await fetch(/api/search?q=${encodeURIComponent(q)});

const data = await response.json();

setResults(data);

}, []);

useEffect(() => {

const handler = setTimeout(() => {

fetchResults(query);

}, 500); // 500ms debounce

return () => clearTimeout(handler); // Cleanup on change

}, [query, fetchResults]);

return (

<div>

<input

type="text"

value={query}

onChange={(e) => setQuery(e.target.value)}

placeholder="Search..."

/>

<ul>

{results.map((item) => (

<li key={item.id}>{item.name}</li>

))}

</ul>

</div>

);

}

Example 2: Form Validation with Custom Hook

// useValidation.js

import { useState } from 'react';

function useValidation(initialState, validators) {

const [values, setValues] = useState(initialState);

const [errors, setErrors] = useState({});

const handleChange = (e) => {

const { name, value } = e.target;

setValues(prev => ({ ...prev, [name]: value }));

// Validate on change

if (validators[name]) {

const error = validators[name](value);

setErrors(prev => ({ ...prev, [name]: error }));

}

};

const isValid = Object.keys(errors).every(key => !errors[key]);

return { values, errors, handleChange, isValid };

}

// Usage

function ContactForm() {

const validators = {

email: (value) => !value.includes('@') ? 'Invalid email' : '',

name: (value) => value.length < 2 ? 'Name must be at least 2 characters' : ''

};

const { values, errors, handleChange, isValid } = useValidation(

{ name: '', email: '' },

validators

);

const handleSubmit = (e) => {

e.preventDefault();

if (isValid) {

console.log('Submitted:', values);

}

};

return (

<form onSubmit={handleSubmit}>

<input

name="name"

value={values.name}

onChange={handleChange}

placeholder="Name"

/>

{errors.name && <span style={{ color: 'red' }}>{errors.name}</span>}

<br />

<input

name="email"

value={values.email}

onChange={handleChange}

placeholder="Email"

/>

{errors.email && <span style={{ color: 'red' }}>{errors.email}</span>}

<br />

<button type="submit" disabled={!isValid}>Submit</button>

</form>

);

}

Example 3: Dark Mode Toggle with Persistence

// useDarkMode.js

import { useState, useEffect } from 'react';

function useDarkMode() {

const [isDarkMode, setIsDarkMode] = useState(() => {

const saved = localStorage.getItem('darkMode');

return saved ? JSON.parse(saved) : window.matchMedia('(prefers-color-scheme: dark)').matches;

});

useEffect(() => {

localStorage.setItem('darkMode', JSON.stringify(isDarkMode));

if (isDarkMode) {

document.documentElement.classList.add('dark');

} else {

document.documentElement.classList.remove('dark');

}

}, [isDarkMode]);

return [isDarkMode, setIsDarkMode];

}

// Usage

function App() {

const [isDarkMode, setIsDarkMode] = useDarkMode();

return (

<div>

<button onClick={() => setIsDarkMode(!isDarkMode)}>

Toggle {isDarkMode ? 'Light' : 'Dark'} Mode

</button>

<h1>Welcome to My App</h1>

</div>

);

}

FAQs

Can I use Hooks in class components?

No. Hooks can only be used inside functional components or other custom Hooks. Class components cannot use Hooks directly. However, you can wrap a class component in a functional component that uses Hooks and pass data as props.

Why does useEffect run on every render by default?

Reacts default behavior ensures side effects are re-run after every update to keep the UI in sync with state. This is safe and predictable. You control when it runs by providing a dependency array. Omitting it runs on every render; using an empty array runs only once.

Is useState asynchronous?

Yes. Like setState in class components, useState updates are batched and asynchronous. You cannot rely on the updated state value immediately after calling the setter. Use useEffect to react to state changes.

Can I use multiple useState Hooks in one component?

Yes. In fact, its encouraged. Splitting state into multiple useState calls makes components more readable and maintainable than managing one large object.

Whats the difference between useMemo and useCallback?

useMemo memoizes a value, while useCallback memoizes a function. Use useMemo when you want to avoid expensive calculations; use useCallback when you want to prevent unnecessary re-renders of child components due to new function references.

Do Hooks replace Redux?

No. Hooks like useContext and useReducer can handle some global state needs, but Redux Toolkit remains the best choice for complex state logic, middleware, time-travel debugging, and large-scale applications. Theyre complementary, not replacements.

Why does React warn about missing dependencies in useEffect?

Reacts exhaustive-deps rule ensures your effect captures the correct values from the components scope. Missing dependencies can lead to stale closures and bugs. Always include all values used inside the effect in the dependency array.

Can I use Hooks in server-side rendered apps?

Yes. React Hooks work with server-side rendering (SSR) frameworks like Next.js. Just ensure you dont access browser-only APIs (like window or localStorage) during server render. Use conditional checks or useEffect for client-side logic.

Conclusion

React Hooks have fundamentally changed how developers write React applications. By enabling state and side effects in functional components, theyve eliminated the complexity and boilerplate of class-based components. With useState, useEffect, useContext, useReducer, and custom Hooks, you now have a powerful, flexible toolkit to build scalable, maintainable, and performant UIs.

This guide has walked you through the core concepts, best practices, real-world examples, and essential tools to master Hooks. Youve learned how to manage state, handle side effects, avoid performance pitfalls, and create reusable logic that scales across teams and projects.

As you continue building with React, remember that Hooks are not just syntaxthey represent a mindset shift toward composability, simplicity, and clarity. The more you use them, the more intuitive they become. Start small: convert a class component to a functional one with useState and useEffect. Then, extract logic into custom Hooks. Gradually, youll find yourself writing cleaner, more modular code with less duplication.

React Hooks are the present and future of React development. Mastering them isnt just about learning a featureits about embracing a better way to build user interfaces. Keep experimenting, stay curious, and never stop refining your approach. The React ecosystem evolves rapidly, and Hooks are at the heart of that evolution.