Skip to main content

Command Palette

Search for a command to run...

16. React useRef and useReducer Explained: Complete Beginner-Friendly Guide with Real-World Examples

Updated
β€’19 min readβ€’View as Markdown
P
Hi there, tech enthusiasts! I'm a passionate Software Developer driven by a love for continuous learning and innovation. I thrive on exploring new tools and technologies, pushing boundaries, and finding creative solutions to complex problems. What You'll Find Here On my Hashnode blog, I share: πŸš€ In-depth explorations of emerging technologies πŸ’‘ Practical tutorials and how-to guides πŸ”§Insights on software development best practices πŸš€Reviews of the latest tools and frameworks πŸ’‘ Personal experiences from real-world projects. Join me as we bridge imagination and implementation in the tech world. Whether you're a seasoned pro or just starting out, there's always something new to discover! Let’s connect and grow together! 🌟

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() syntax

  • Two simple useRef() examples

  • A complete real-world useRef() example

  • Real-life use cases of useRef()

  • What is useReducer()?

  • useReducer() syntax

  • Two simple useReducer() examples

  • A complete real-world useReducer() example

  • Real-life use cases of useReducer()

  • useRef() vs useReducer()

  • 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:

  1. Define the current state.

  2. Define a reducer function.

  3. Dispatch an action.

  4. 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 state

  • useRef() β†’ 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. πŸš€