Before Hooks, React relied heavily on class components to manage state, handle lifecycle methods, and build complex user interfaces. While classes worked, they often made the code longer, harder to read, and difficult to reuse.
React Hooks are special functions introduced in React 16.8. They allow functional components to use React features such as state and lifecycle behavior.
Hooks solve several common problems:
* They reduce repetitive and duplicated code
* They make components easier to read and understand
* They allow logic reuse through custom hooks
* They eliminate the need for complex class components
Overall, hooks help you write cleaner, simpler, and more maintainable React code.
Today, almost every modern React application is built using Hooks. If you plan to build websites or applications using React, learning Hooks is essential.
1. Rules of Hooks
React enforces two mandatory rules:
Rule 1. Only call hooks at the top level
Never call hooks inside loops, conditions, nested functions, or event handlers.
Correct:
function App() {
const [count, setCount] = useState(0);
}
Incorrect:
if (true) {
useState(0);
}
Rule 2. Only call hooks inside React components or custom hooks
You cannot call hooks in regular JavaScript functions.
Correct:
function MyComponent() {
useEffect(() => {});
}
Incorrect:
function helper() {
useEffect(() => {});
}
These rules ensure React tracks hook calls correctly.
2. General Steps for Writing React Hooks
Let’s see the steps by understanding the useState Hook
The useState hook allows you to add state to a functional component. State represents data that can change over time and affects what is displayed on the screen.
Step 1: Import useState
Before using useState, you must import it from React:
import { useState } from “react”;
Step 2: Declare a State Variable
Inside your component, you can declare a state variable like this:
const [showMenu, setShowMenu] = useState(false);
Let’s break this down:
* `showMenu` is the current state value
* `setShowMenu` is the function used to update the state
* `false` is the initial value of the state
This syntax uses array destructuring. The `useState` hook returns an array with two values, and destructuring makes the code short and readable.
3. List of Core Hooks
3.1 useState
Manages component-local state.
UseStateExample.js
import { useState } from “react”;
function UseStateExample() {
const [isVisible, setIsVisible] = useState(false);
return (
<div>
<button onClick={() => setIsVisible(!isVisible)}>
Toggle Text
</button>
{isVisible && <p>Text is now visible</p>}
</div>
);
}
export default UseStateExample;
App.js
import UseStateExample from “./UseStateExample”;
function App() {
return (
<div>
<h1>useState Hook Example</h1>
<UseStateExample />
</div>
);
}
export default App;
In the above example
- It creates a state variable named isVisible with an initial value of false.
- A button is displayed on the screen with the label “Toggle Text”.
- When the button is clicked, setIsVisible changes the state from false to true or from true to false.
- If isVisible is true, the text “Text is now visible” is shown.
- If isVisible is false, the text is hidden.
You’ll most often use this in everyday situations such as:
- Handling form inputs like text fields, checkboxes, or dropdowns
- Showing or hiding parts of the UI, for example opening a modal or toggling a menu
- Keeping track of simple counts, such as clicks, likes, or items in a cart
- Storing small local flags, like whether something is loading, active, or completed
3.2 useEffect
Performs side effects. Equivalent to componentDidMount, DidUpdate, and WillUnmount.
Signature:
useEffect(effectCallback, dependencyArray);
Common use cases:
* Fetching data
* Setting up event listeners
* Updating document title
* Running code when state changes
Example: Fetch API data on mount
UseEffectExample.js
import { useState, useEffect } from “react”;
function UseEffectExample() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Clicked ${count} times`;
}, [count]);
return (
<div>
<h2>Button Click Counter</h2>
<p>You clicked the button {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click Me
</button>
</div>
);
}
export default UseEffectExample;
App.js
import UseEffectExample from “./UseEffectExample”;
function App() {
return (
<div>
<h1>useEffect Hook Example</h1>
<UseEffectExample />
</div>
);
}
export default App;
In the above example
- When the button is clicked, the state value is updated.
- Updating the state causes the component to re-render.
- After the re-render is completed, the useEffect hook is executed.
- The useEffect hook then performs side effects such as updating the browser tab title.
3.3 useContext
State management is one of the most important concepts in React applications. As applications grow, managing and sharing data between components becomes increasingly complex. This is where the React Context API plays a crucial role. It provides a built-in way to share data across the component tree without manually passing props at every level.
The Context API is a feature in React that allows you to create global data that can be accessed by any component in your application, regardless of how deeply nested it is.
In simple terms, Context API helps you avoid prop drilling by providing a centralized place to store and consume shared data such as:
- User authentication status
- Theme (dark/light mode)
- Language or locale
The Context API allows you to store shared data at a higher level and access it directly in any component that needs it—without passing props manually.
It works using three main concepts:
- createContext
- Provider
- Consumer (or useContext hook)
Step 1: Create Context
ThemeContext.js
import { createContext } from “react”;
const ThemeContext = createContext();
export default ThemeContext;
Step 2: Provide Context
App.js
import ThemeContext from “./ThemeContext”;
import Child from “./Child”;
function App() {
return (
<ThemeContext.Provider value=“dark”>
<h1>useContext Example</h1>
<Child />
</ThemeContext.Provider>
);
}
export default App;
Step 3: Consume Context
Child.js
import { useContext } from “react”;
import ThemeContext from “./ThemeContext”;
function Child() {
const theme = useContext(ThemeContext);
return <p>Current theme: {theme}</p>;
}
export default Child;
Output
useContext Example
Current theme: dark
What This Program Does
- Creates a context called ThemeContext
- Shares the value “dark” from the parent component
- Allows the Child component to access the value directly
- Avoids passing props manually through components
3.4 useRef
useRef provides a mutable reference that persists across renders.
Use cases:
* Accessing DOM elements
* Storing values that should not trigger re-renders
Example:
UseRefExample.js
import { useRef } from “react”;
function UseRefExample() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type=“text” />
<button onClick={focusInput}>Focus Input</button>
</div>
);
}
export default UseRefExample;
App.js
import UseRefExample from “./UseRefExample”;
function App() {
return (
<div>
<h1>useRef Hook Example</h1>
<UseRefExample />
</div>
);
}
export default App;
In the above example
- Page loads → input and button are displayed
- useRef stores a reference to the input element
- Button is clicked
- focus() is called using the reference
- Cursor appears inside the input box
3.5 useMemo
useMemo memoizes expensive calculations to improve performance.
useMemoExample.js
import { useMemo, useState } from “react”;
function UseMemoExample() {
const [number, setNumber] = useState(1);
const squaredNumber = useMemo(() => {
console.log(“Calculating…”);
return number * number;
}, [number]);
return (
<div>
<input
type=“number”
value={number}
onChange={(e) => setNumber(Number(e.target.value))}
/>
<p>Squared Value: {squaredNumber}</p>
</div>
);
}
export default UseMemoExample;
App.js
import UseMemoExample from “./UseMemoExample”;
function App() {
return (
<div>
<h1>useMemo Hook Example</h1>
<UseMemoExample />
</div>
);
}
export default App;
In the above example
- The component renders an input field and displays the initial number and its squared value.
- When the user types a new number, the state is updated using setNumber.
- Updating the state causes the component to re-render.
- The useMemo hook recalculates the squared value only when the number changes.
- The updated squared value is displayed on the screen, avoiding unnecessary recalculations.
3.6 useCallback
useCallback memoizes functions to prevent unnecessary re-renders.
const memoizedFn = useCallback(() => {
doSomething();
}, [dependency]);
Useful when passing functions as props to child components.
Example
UseCallbackExample.js
import { useCallback, useState } from “react”;
function UseCallbackExample() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increase</button>
</div>
);
}
export default UseCallbackExample;
App.js
import UseCallbackExample from “./UseCallbackExample”;
function App() {
return (
<div>
<h1>useCallback Hook Example</h1>
<UseCallbackExample />
</div>
);
}
export default App;
In the above example
- The component renders a counter (`count`) and a button labeled Increase.
- `useState` initializes the `count` state with `0`.
- `useCallback` memoizes the `increment` function so it does not get recreated on every render.
- When the button is clicked, the `increment` function runs and updates the count using `setCount`
- React re-renders the component with the updated count, and the memoized function remains stable.
3.7 useReducer
When your state logic becomes complicated or involves multiple transitions, `useReducer` is a better alternative to `useState`.
App.js
import { useReducer } from “react”;
// Reducer function
function reducer(state, action) {
switch (action.type) {
case “increment”:
return state + 1;
case “decrement”:
return state – 1;
default:
return state;
}
}
// App component
function App() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div style={{ textAlign: “center”, marginTop: “40px” }}>
<h2>useReducer Example</h2>
<p>Count: {count}</p>
<button onClick={() => dispatch({ type: “increment” })}>
Increment
</button>
<button
onClick={() => dispatch({ type: “decrement” })}
style={{ marginLeft: “10px” }}
>
Decrement
</button>
</div>
);
}
export default App;
In the above example
- The useReducer hook is used to handle state when there are clear actions that change the value, such as increasing or decreasing a count.
- The reducer function works like a decision-maker—it checks the action type and decides how the state should update.
- useReducer gives us two things: the current count value and a dispatch function to send actions to the reducer.
- When a button is clicked, a separate handler function dispatches an action (increment or decrement) to update the count.
- Each time the state changes, React automatically re-renders the component to show the updated count on the screen.
4. Custom Hooks
A custom hook allows you to extract reusable logic into a separate function starting with the name `use`.
Example:
useWindowWidth.js
import { useState, useEffect } from “react”;
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener(‘resize’, handleResize);
return () => window.removeEventListener(‘resize’, handleResize);
}, []);
return width;
}
export default useWindowWidth;
App.js
import useWindowWidth from “./useWindowWidth”
function App() {
const width = useWindowWidth();
return <p>Window width: {width}</p>;
}
export default App;
1. Code Reusability – You can extract logic that is used in multiple components into a single custom hook. This avoids repeating the same code in different places.
2. Separation of Concerns – Custom hooks help organize logic separately from the UI. This makes components remain cleaner and focused on rendering.
3. Simplifies Complex Logic – If a component has multiple `useState` or `useEffect` logic, you can move them into a custom hook. It makes the component easier to read and maintain.
4. Easier Testing – Custom hooks can be tested independently of the UI. This improves reliability and maintainability.
5. Consistency Across Components – When multiple components share the same logic (e.g., form handling, API fetching), a custom hook ensures consistent behavior everywhere