16. React useRef and useReducer Explained: Complete Beginner-Friendly Guide with Real-World Examples
If you are learning React Hooks, two important hooks you should understand after useState, useEffect, and useContext are:
useRef()useReducer()
Both hooks solve different problems.
useRef() is mainly used when you need to store a value without causing a re-render or when you need to directly access a DOM element.
useReducer() is useful when your state logic becomes more complex and managing everything with multiple useState() calls becomes difficult.
In this article, we will learn:
What is
useRef()?useRef()syntaxTwo simple
useRef()examplesA complete real-world
useRef()exampleReal-life use cases of
useRef()What is
useReducer()?useReducer()syntaxTwo simple
useReducer()examplesA complete real-world
useReducer()exampleReal-life use cases of
useReducer()useRef()vsuseReducer()Frequently Asked Questions
Let's understand both hooks step by step.
Part 1: React useRef()
What is useRef() in React?
useRef() is a React Hook that allows you to create a mutable reference that remains available between renders.
The important thing to remember is:
Changing a value stored inside
useRef()does not cause the component to re-render.
useRef() is commonly used for:
Accessing DOM elements
Storing previous values
Creating timers
Storing interval IDs
Keeping mutable values between renders
Managing focus
Tracking values without triggering a re-render
useRef() Syntax
The basic syntax is:
const ref = useRef(initialValue);
For example:
const countRef = useRef(0);
The value can be accessed using:
countRef.current
So if we want to update the value:
countRef.current = countRef.current + 1;
The .current property stores the actual value.
Simple Example 1: Accessing an Input Element
One of the most common uses of useRef() is accessing a DOM element.
import React, { useRef } from "react";
function App() {
const inputRef = useRef();
const handleFocus = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} placeholder="Enter your name" />
<button onClick={handleFocus}>
Focus Input
</button>
</div>
);
}
export default App;
Simple Explanation
Here:
const inputRef = useRef();
creates a reference.
Then we attach it to the input:
<input ref={inputRef} />
Now React gives us access to that input through:
inputRef.current
So:
inputRef.current.focus();
focuses the input.
In simple words:
Instead of searching for the input manually, React gives us a reference to it.
Simple Example 2: Store a Value Without Re-rendering
import React, { useRef, useState } from "react";
function App() {
const countRef = useRef(0);
const [count, setCount] = useState(0);
const handleClick = () => {
countRef.current++;
setCount(count + 1);
console.log("Ref Value:", countRef.current);
};
return (
<div>
<h2>State: {count}</h2>
<button onClick={handleClick}>
Click
</button>
</div>
);
}
export default App;
Simple Explanation
Here we have two different values:
const countRef = useRef(0);
and:
const [count, setCount] = useState(0);
When we update state:
setCount(count + 1);
React re-renders the component.
But when we update:
countRef.current++;
React does not re-render because of the ref update.
This is one of the biggest differences between useState() and useRef().
Real-World Example: Login Form with Auto Focus and Password Visibility
Let's build a small real-world example.
Imagine a login form where:
The username field should automatically receive focus.
The password field can be shown/hidden.
We want to directly access the username input using
useRef().
import React, { useEffect, useRef, useState } from "react";
function LoginForm() {
const usernameRef = useRef(null);
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
useEffect(() => {
usernameRef.current.focus();
}, []);
const handleSubmit = (e) => {
e.preventDefault();
const username = usernameRef.current.value;
console.log("Username:", username);
console.log("Password:", password);
};
return (
<div>
<h2>Login</h2>
<form onSubmit={handleSubmit}>
<input
ref={usernameRef}
type="text"
placeholder="Enter username"
/>
<br /><br />
<input
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter password"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? "Hide" : "Show"} Password
</button>
<br /><br />
<button type="submit">
Login
</button>
</form>
</div>
);
}
export default LoginForm;
In-Depth Explanation of the Example
Let's understand this example step by step.
Step 1: Create the Reference
const usernameRef = useRef(null);
We created a ref called usernameRef.
Initially:
usernameRef.current
is:
null
because the input has not been connected to the ref yet.
Step 2: Attach the Ref to the Input
<input
ref={usernameRef}
type="text"
/>
After React renders the input, the ref points to the actual DOM element.
Now:
usernameRef.current
refers to the input element.
Step 3: Automatically Focus the Input
We use:
useEffect(() => {
usernameRef.current.focus();
}, []);
When the component loads, React runs the effect.
Then:
usernameRef.current.focus();
focuses the username field.
So when the user opens the login page, they can start typing immediately.
Step 4: Read the Input Value
When the form is submitted:
const username = usernameRef.current.value;
We directly read the value from the input.
This is an example of using a ref to access a DOM element.
Important Rule: useRef() Does Not Trigger Re-render
Consider:
const valueRef = useRef(0);
valueRef.current = valueRef.current + 1;
React will not automatically re-render the component because of this change.
Compare it with:
const [value, setValue] = useState(0);
setValue(value + 1);
setValue() schedules a re-render.
This difference is extremely important when deciding whether to use useState() or useRef().
Real-Life Use Cases of useRef()
1. Input Focus
inputRef.current.focus();
Useful for:
Login forms
Search boxes
OTP forms
Registration forms
Chat applications
2. DOM Manipulation
Refs can be used when you need to interact with a DOM element directly.
Examples:
Focus
Scroll
Play/pause video
Measure an element
Select text
3. Storing Timer IDs
const timerRef = useRef(null);
Useful for:
setTimeout()setInterval()Countdown timers
Debouncing
4. Storing Previous Values
A ref can store a previous value without causing a re-render.
5. Keeping Mutable Data Between Renders
For example:
const requestIdRef = useRef(0);
The value remains available across renders.
When Should You NOT Use useRef()?
Do not use useRef() just because you need to store data.
If changing the value should update the UI, use useState().
For example:
const [name, setName] = useState("");
is better than:
const nameRef = useRef("");
if the name needs to appear on the screen and update when the user types.
Simple rule:
UI needs to update β useState()
Value needs to persist but UI does not need to update β useRef()
Part 2: React useReducer()
Now let's move to the second important hook.
What is useReducer() in React?
useReducer() is a React Hook used to manage complex state logic.
It is an alternative to useState().
Instead of directly changing state, we:
Define the current state.
Define a reducer function.
Dispatch an action.
The reducer decides how the state should change.
The basic idea is:
User Action
β
dispatch()
β
Reducer
β
New State
β
UI Update
useReducer() is especially useful when:
State has multiple related values.
Many actions can change the same state.
State transitions are complex.
You want state logic in one centralized function.
useReducer() Syntax
The basic syntax is:
const [state, dispatch] = useReducer(reducer, initialState);
There are three important concepts here.
1. state
The current state.
2. dispatch
A function used to send an action.
3. reducer
A function that decides how the state should change.
The reducer generally looks like:
function reducer(state, action) {
switch (action.type) {
case "ACTION_NAME":
return newState;
default:
return state;
}
}
Simple Example 1: Counter
Let's create a simple counter.
import React, { useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "increment":
return state + 1;
case "decrement":
return state - 1;
default:
return state;
}
}
function Counter() {
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => dispatch({ type: "increment" })}>
+
</button>
<button onClick={() => dispatch({ type: "decrement" })}>
-
</button>
</div>
);
}
export default Counter;
Simple Explanation
Our initial state is:
0
When the user clicks:
dispatch({ type: "increment" })
the reducer receives:
action.type
as:
increment
Then this code runs:
return state + 1;
For decrement:
return state - 1;
Important Terms
Action
{ type: "increment" }
An action describes what happened.
Dispatch
dispatch(...)
Dispatch sends the action to the reducer.
Reducer
function reducer(state, action)
The reducer decides what the new state should be.
Simple Example 2: Managing Multiple State Values
useReducer() becomes more useful when state contains multiple related values.
import React, { useReducer } from "react";
const initialState = {
name: "",
age: 0
};
function reducer(state, action) {
switch (action.type) {
case "SET_NAME":
return {
...state,
name: action.payload
};
case "SET_AGE":
return {
...state,
age: action.payload
};
default:
return state;
}
}
function UserForm() {
const [state, dispatch] = useReducer(
reducer,
initialState
);
return (
<div>
<input
placeholder="Name"
onChange={(e) =>
dispatch({
type: "SET_NAME",
payload: e.target.value
})
}
/>
<input
type="number"
placeholder="Age"
onChange={(e) =>
dispatch({
type: "SET_AGE",
payload: e.target.value
})
}
/>
<p>Name: {state.name}</p>
<p>Age: {state.age}</p>
</div>
);
}
export default UserForm;
Simple Explanation
Our state contains:
{
name: "",
age: 0
}
If the user changes the name, we dispatch:
{
type: "SET_NAME",
payload: e.target.value
}
The reducer receives the action and updates only the name.
The payload contains the data that we want to send with the action.
Real-World Example: Shopping Cart
A shopping cart is a great example of where useReducer() can be useful.
Imagine an e-commerce application where the user can:
Add products
Remove products
Increase quantity
Decrease quantity
Clear the cart
Managing all of this with many separate useState() calls can become difficult.
Instead, we can centralize the cart logic inside a reducer.
import React, { useReducer } from "react";
const initialState = {
cart: []
};
function cartReducer(state, action) {
switch (action.type) {
case "ADD_ITEM": {
const existingItem = state.cart.find(
(item) => item.id === action.payload.id
);
if (existingItem) {
return {
...state,
cart: state.cart.map((item) =>
item.id === action.payload.id
? {
...item,
quantity: item.quantity + 1
}
: item
)
};
}
return {
...state,
cart: [
...state.cart,
{
...action.payload,
quantity: 1
}
]
};
}
case "REMOVE_ITEM":
return {
...state,
cart: state.cart.filter(
(item) => item.id !== action.payload
)
};
case "INCREASE_QUANTITY":
return {
...state,
cart: state.cart.map((item) =>
item.id === action.payload
? {
...item,
quantity: item.quantity + 1
}
: item
)
};
case "DECREASE_QUANTITY":
return {
...state,
cart: state.cart
.map((item) =>
item.id === action.payload
? {
...item,
quantity: item.quantity - 1
}
: item
)
.filter((item) => item.quantity > 0)
};
case "CLEAR_CART":
return {
...state,
cart: []
};
default:
return state;
}
}
function ShoppingCart() {
const [state, dispatch] = useReducer(
cartReducer,
initialState
);
const products = [
{
id: 1,
name: "Laptop",
price: 50000
},
{
id: 2,
name: "Headphones",
price: 2000
}
];
return (
<div>
<h2>Products</h2>
{products.map((product) => (
<div key={product.id}>
<h3>{product.name}</h3>
<p>βΉ{product.price}</p>
<button
onClick={() =>
dispatch({
type: "ADD_ITEM",
payload: product
})
}
>
Add to Cart
</button>
</div>
))}
<hr />
<h2>Shopping Cart</h2>
{state.cart.length === 0 ? (
<p>Your cart is empty.</p>
) : (
state.cart.map((item) => (
<div key={item.id}>
<h3>{item.name}</h3>
<p>
Price: βΉ{item.price}
</p>
<p>
Quantity: {item.quantity}
</p>
<button
onClick={() =>
dispatch({
type: "DECREASE_QUANTITY",
payload: item.id
})
}
>
-
</button>
<button
onClick={() =>
dispatch({
type: "INCREASE_QUANTITY",
payload: item.id
})
}
>
+
</button>
<button
onClick={() =>
dispatch({
type: "REMOVE_ITEM",
payload: item.id
})
}
>
Remove
</button>
</div>
))
)}
<button
onClick={() =>
dispatch({ type: "CLEAR_CART" })
}
>
Clear Cart
</button>
</div>
);
}
export default ShoppingCart;
In-Depth Explanation of the Shopping Cart
This example contains several important React concepts.
Step 1: Initial State
We start with:
const initialState = {
cart: []
};
The cart is initially empty.
Step 2: Create the Reducer
function cartReducer(state, action) {
The reducer receives two things:
state
action
state tells us the current state.
action tells us what the user wants to do.
Step 3: Add Product
When the user clicks:
dispatch({
type: "ADD_ITEM",
payload: product
});
we send an action.
Here:
type: "ADD_ITEM"
tells the reducer what operation should happen.
And:
payload: product
contains the product information.
Step 4: Reducer Handles the Action
The reducer checks:
switch (action.type)
Then:
case "ADD_ITEM":
handles the add operation.
If the product already exists in the cart, its quantity increases.
If it does not exist, a new item is added.
Step 5: Increase Quantity
When the user clicks +:
dispatch({
type: "INCREASE_QUANTITY",
payload: item.id
});
The reducer finds the product using its ID and increases:
quantity + 1
Step 6: Decrease Quantity
When the user clicks -:
dispatch({
type: "DECREASE_QUANTITY",
payload: item.id
});
The reducer decreases the quantity.
If the quantity becomes 0, we remove the product from the cart.
Step 7: Remove Product
The remove button dispatches:
dispatch({
type: "REMOVE_ITEM",
payload: item.id
});
The reducer uses:
filter()
to create a new cart without that product.
Step 8: Clear Cart
Finally:
dispatch({
type: "CLEAR_CART"
});
sets the cart to:
[]
Why is useReducer() Useful Here?
Imagine managing this shopping cart using separate state variables:
const [cart, setCart] = useState([]);
Then we may create multiple functions:
addItem()
removeItem()
increaseQuantity()
decreaseQuantity()
clearCart()
As the application grows, the state logic can become difficult to maintain.
With useReducer(), related state logic is centralized:
function cartReducer(state, action) {
// all cart state logic
}
This makes the state transitions easier to understand and maintain.
Real-Life Use Cases of useReducer()
1. Shopping Cart
Actions can include:
ADD_ITEM
REMOVE_ITEM
INCREASE_QUANTITY
DECREASE_QUANTITY
CLEAR_CART
2. Complex Forms
For example:
SET_NAME
SET_EMAIL
SET_PASSWORD
SET_PHONE
SUBMIT_FORM
RESET_FORM
3. Authentication State
You might have actions such as:
LOGIN
LOGOUT
UPDATE_USER
LOGIN_ERROR
4. API Request State
A common pattern is:
IDLE
LOADING
SUCCESS
ERROR
For example:
{
status: "loading",
data: null,
error: null
}
The reducer can manage transitions between these states.
5. Multi-Step Forms
For example:
NEXT_STEP
PREVIOUS_STEP
UPDATE_PERSONAL_DETAILS
UPDATE_ADDRESS
SUBMIT_APPLICATION
6. Dashboard Filters
A dashboard may have:
SET_DATE
SET_CATEGORY
SET_SEARCH
RESET_FILTERS
A reducer can keep all related filter logic together.
useRef() vs useReducer()
These two hooks are completely different.
| Feature | useRef() |
useReducer() |
|---|---|---|
| Main purpose | Store mutable references | Manage complex state |
| Causes re-render when updated? | No | Yes |
| Access DOM elements? | Yes | No |
Uses .current? |
Yes | No |
Uses dispatch()? |
No | Yes |
| Has reducer function? | No | Yes |
| Useful for complex state logic? | No | Yes |
| Useful for timers/DOM references? | Yes | No |
Simple way to remember
Think of useRef() as:
"I want to remember something without asking React to re-render."
Think of useReducer() as:
"My state logic is getting complicated, so I want to manage it in one structured place."
Important Concepts You Should Remember
useRef()
const ref = useRef(initialValue);
Access the value using:
ref.current
Changing:
ref.current
does not cause a re-render.
useReducer()
const [state, dispatch] = useReducer(
reducer,
initialState
);
Send an action:
dispatch({
type: "ACTION_NAME"
});
Reducer receives:
(state, action)
and returns the next state.
Frequently Asked Questions β React useRef() and useReducer()
1. What is useRef() in React?
useRef() is a React Hook used to store a mutable value that persists between renders without causing a re-render when its value changes. It is also commonly used to access DOM elements.
2. What is useRef().current?
.current is the property where the value of a ref is stored.
Example:
const countRef = useRef(0);
countRef.current++;
Here, current contains the actual value.
3. Does changing useRef() cause a re-render?
No.
Changing:
ref.current = newValue;
does not cause React to re-render the component.
4. What is the difference between useState and useRef?
useState() is used when a change should update the UI.
useRef() is used when you need to persist a value but changing that value does not need to update the UI.
5. Can useRef() access DOM elements?
Yes.
For example:
const inputRef = useRef(null);
<input ref={inputRef} />
You can then access the element using:
inputRef.current
6. Can we use useRef() to store previous values?
Yes.
A ref can persist a value between renders, which makes it useful for tracking previous values.
7. What is useReducer() in React?
useReducer() is a React Hook used for managing state with a reducer function and actions. It is especially useful when state logic is complex or contains multiple related state transitions.
8. What is a reducer?
A reducer is a function that receives the current state and an action and returns the next state.
function reducer(state, action) {
// return new state
}
9. What is an action in useReducer()?
An action is an object that describes what happened.
Example:
{
type: "ADD_ITEM"
}
It can also contain additional data:
{
type: "ADD_ITEM",
payload: product
}
10. What is dispatch()?
dispatch() is the function used to send an action to the reducer.
Example:
dispatch({
type: "INCREMENT"
});
React then runs the reducer with the current state and this action.
11. What is payload?
payload is commonly used to carry additional data with an action.
Example:
dispatch({
type: "SET_NAME",
payload: "Payal"
});
Here:
type β what should happen
payload β data required to perform the action
12. When should I use useReducer() instead of useState()?
Use useReducer() when:
State logic is complex.
Multiple actions can change the state.
Several state values are related.
You want to centralize state transition logic.
For simple state, useState() is usually easier.
13. Does useReducer() cause a re-render?
Yes.
When the reducer returns a new state, React updates the state and re-renders the component when appropriate.
14. Can useReducer() replace useState()?
In many cases, yes.
However, that does not mean you should always use useReducer().
For simple state:
const [name, setName] = useState("");
is often simpler.
For complex state logic:
const [state, dispatch] = useReducer(
reducer,
initialState
);
can be easier to maintain.
15. Can useRef() and useReducer() be used together?
Yes.
They solve different problems and can be used in the same component.
For example, a complex form could use:
useReducer()β manage form stateuseRef()β focus a specific input
Final Takeaway
useRef() and useReducer() are both important React Hooks, but they solve different problems.
Remember useRef() like this:
Need a DOM reference?
β
useRef()
Need a value to persist
without re-rendering?
β
useRef()
Remember useReducer() like this:
Simple state?
β
useState()
Complex state logic?
β
useReducer()
The goal is not to use every React Hook everywhere.
The goal is to understand why a Hook is needed and when it is the right tool for the problem.
Once you understand useRef() and useReducer(), you have a much stronger foundation for learning more advanced React Hooks and building real-world React applications.
Quick Revision
useRef()
const ref = useRef(initialValue);
Access:
ref.current
Main uses:
DOM access
Focus management
Timers
Previous values
Mutable values
Persisting values without re-render
useReducer()
const [state, dispatch] = useReducer(
reducer,
initialState
);
Dispatch action:
dispatch({
type: "ACTION_NAME",
payload: data
});
Main uses:
Complex state
Shopping carts
Complex forms
Authentication flows
API state
Multi-step forms
Dashboard filters
In short:
useRef()helps you remember or reference something without triggering a re-render.
useReducer()helps you manage complex state changes in a structured way.
π Stay Connected
If you found this article helpful and want to receive more beginner-friendly and industry-relevant React notes, tutorials, and project ideas, stay connected with us!
π© Subscribe to our newsletter by entering your email below.
And if you're someone who wants to prepare for tech interviews while having a little fun and entertainment,
π₯ Don't forget to subscribe to my YouTube channel β Knowledge Factory 22 β for regular content on:
π» Tech concepts
π― Interview preparation
π Career tips
π§βπ» Coding insights
π Practical development tutorials
Stay curious. Keep building. π

