Photo by Lautaro Andreani on Unsplash

React Hooks 101

Pratik Rai
10 min readMar 5, 2025

React Hooks are a powerful way to “hook into” React’s features without needing to write class components. They let you manage state, handle side effects, and much more, all while keeping your code clean and functional.

In this post, I will cover the basics of React Hooks — perfect for beginners. By the end, you’ll know enough to start using them in your projects. Let’s dive in!

Types of Hooks

React Hooks can be divided into two categories:

  1. Built-in Hooks — These are provided by React itself and cover most use cases.
  2. Custom Hooks — These are hooks you can create yourself to encapsulate reusable logic.

In this post, we’ll focus on built-in hooks, especially the most commonly used ones:

  • useState
  • useEffect
  • useRef
  • useMemo
  • useCallback

Each hook is unique and serves a specific purpose. Let’s explore them one by one, starting with the foundation of state management: useState.

useState

Before we get into the useState hook, let’s understand what state means in React.

What is State?

State is like a memory for your React component. It’s information that your component keeps track of and may need to update or use later. For example, imagine a checkbox on your webpage. When a user clicks it, the state can track whether it’s checked or unchecked.

For more on state, check out the official guide: State: A Component’s Memory.

What Does useState Do?

The useState hook allows you to add state to your functional components.

Syntax:

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

Here’s a breakdown of how it works:

  • state: This stores the current value of your state.
  • setState: This is the function used to update your state. By convention, it starts with "set" for better readability.
  • initialValue: The value you want your state to have initially. This can be a boolean, number, string, or even an object.

Example:

Let’s say you’re tracking whether a checkbox is checked:

const [isChecked, setIsChecked] = React.useState(false);

Here:

  • isChecked stores the state (initially false).
  • setIsChecked updates the state when the checkbox is toggled.

How Does useState Work

The useState hook returns an array with two values:

  1. The Current State
  2. This is the value stored in the state at any given time. During the initial render, it takes the initialValue.
  3. The State Setter Function (setState)
  4. This function lets you update the state. Whenever you call it, React compares the previous value with the new value:
  • If they’re the same, no re-render occurs.
  • If they’re different, React triggers a re-render to update the UI.

Example of useState:

import React from "react";
const CheckboxExample = () => {
// initialize state with useState
const [isChecked, setIsChecked] = React.useState(false);
// create a function to handle state updates
const handleCheckboxChange = () => {
setIsChecked((prevState) => !prevState); // change the state
};
return (
<div>
<label>
<input
type="checkbox"
checked={isChecked} // track the current state value
onChange={handleCheckboxChange} // update state
/>
{isChecked ? "Checked" : "Unchecked"}
</label>
</div>
);
};
export default CheckboxExample;

This ensures your UI stays in sync with the latest state.

That’s it for useState! Remember its a simple tool to manage your state. Don’t overcomplicate it.

useEffect()

The useEffect hook lets you perform side effects in your React components. Side effects include things like:

  • Fetching data from an API
  • Updating the DOM
  • Subscribing to events
  • Setting up timers

Think of useEffect as a way to react to changes in your component and run code when needed.

Syntax

useEffect(setupFunction, [dependencies]);

Example:

useEffect(() => {
doSomething();
}, [dependencies]);

You can also include a cleanup function inside useEffect:

useEffect(() => {
console.log("Effect runs");
return () => {
console.log("Cleanup runs");
};
}, []);

How useEffect Works

  1. Runs on Initial Render
  • When the component first mounts (renders for the first time), the setupFunction inside useEffect executes.
  1. Runs When Dependencies Change
  • After the initial render, useEffect re-runs whenever any value inside the [dependencies] array changes.
  1. Cleanup Function (Optional but Important!)
  • Before running the effect again (on a re-render), React first runs the cleanup function from the previous render.
  • The cleanup function also runs when the component unmounts, making it great for things like removing event listeners or stopping timers.

Understanding Dependencies

The dependency array ([dependencies]) determines when useEffect should run:

  • No dependencies ([]) → Runs only once after the first render.
  • With dependencies ([someValue]) → Runs whenever someValue changes.
  • No array (useEffect(() => {...})) → Runs after every render, which is usually not ideal.

Example Use Case

Let’s say we want to update the document title whenever a user updates a text input.

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

const TitleUpdater = () => {
const [title, setTitle] = useState("Hello, World!");
useEffect(() => {
document.title = title; // Side effect: updating the document title
}, [title]); // Runs whenever 'title' changes
return (
<div>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Update page title"
/>
</div>
);
};

export default TitleUpdater;

How This Works:

  1. The user types into the input field.
  2. The title state updates.
  3. Since title is in the dependency array, useEffect triggers and updates the document title.

When to Use useEffect

Use useEffect when your component needs to:

  • Fetch data from an API
  • Subscribe to events (e.g., window.addEventListener)
  • Update the document title, theme, or animations
  • Set up timers or intervals

Ok, now lets move forward and learn about useRef() hook.

useRef()

The useRef hook is used to create a reference to a value or a DOM element without triggering a re-render when the value changes.

Think of it as a way to store mutable data that persists across renders but doesn’t cause the component to update.

Syntax

const refContainer = useRef(initialValue);

When to Use useRef

  1. Accessing DOM Elements
  • You can use useRef to get direct access to a DOM element without using document.querySelector.
  1. Storing Mutable Values Without Re-rendering
  • Unlike useState, changing a useRef value doesn’t cause the component to re-render.
  1. Keeping Previous Values Between Renders
  • You can store a previous value inside useRef and access it later.

Example 1: Accessing a DOM Element

Let’s say we want to focus on an input field when a button is clicked.

import React, { useRef } from "react";

const InputFocus = () => {
const inputRef = useRef(null); // creating a refernce to the input element
const focusInput = () => {
inputRef.current.focus(); // accessing it directly using ref
};
return (
<div>
<input ref={inputRef} type="text" placeholder="Type something..." />
<button onClick={focusInput}>Focus Input</button>
</div>
);
};

export default InputFocus;

How This Works:

  1. useRef(null) creates a reference (inputRef).
  2. The ref attribute is assigned to the <input> element.
  3. When the button is clicked, inputRef.current.focus() gives direct access to the input and focuses it.

Example 2: Storing a Mutable Value

Unlike useState, updating a useRef value doesn’t trigger a re-render.

import React, { useState, useRef } from "react";

const CounterWithRef = () => {
const [count, setCount] = useState(0);
const prevCountRef = useRef(0); // storing the prev count
useEffect(() => {
prevCountRef.current = count; // Update the ref value after render
}, [count]);
return (
<div>
<p>Current Count: {count}</p>
<p>Previous Count: {prevCountRef.current}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
export default CounterWithRef;

How This Works:

  1. The prevCountRef stores the previous count value.
  2. When count updates, useEffect runs and updates prevCountRef.current.
  3. The previous count is displayed without triggering a re-render.

When to Use useRef

  • When you need to directly access and manipulate DOM elements
  • When storing mutable data that doesn’t trigger re-renders
  • When you want to keep track of previous values.

useMemo()

The useMemo hook helps optimize performance by caching the result of an expensive calculation so it doesn’t get re-computed on every render.

If you’ve ever noticed your React component slowing down because of heavy computations, useMemo can help by memorizing the output and recalculating it only when necessary.

Syntax

const memoizedValue = useMemo(() => computeExpensiveValue(dependencies), [dependencies]);

Why Do We Need useMemo?

In React, every time a component re-renders, all functions inside it run again, even if their result hasn’t changed.

Imagine you have a function that takes a long time to compute (like filtering a huge dataset). Without useMemo, this function runs on every render, slowing down the UI.

With useMemo, React remembers the previous result and only recalculates when the dependencies change.

Example 1: Preventing Unnecessary Computation

Let’s say we have a function that calculates the sum of numbers from 1 to n, which is expensive for large n.

import React, { useState, useMemo } from "react";

const ExpensiveCalculation = ({ num }) => {
// Expensive function that runs every time the component re-renders
const calculateSum = (n) => {
console.log("Calculating sum...");
let sum = 0;
for (let i = 1; i <= n; i++) {
sum += i;
}
return sum;
};
// Memoized value: Recalculates only if 'num' changes
const sum = useMemo(() => calculateSum(num), [num]);
return <p>Sum of numbers from 1 to {num}: {sum}</p>;
};
const App = () => {
const [num, setNum] = useState(100);
const [count, setCount] = useState(0);
return (
<div>
<ExpensiveCalculation num={num} />
<button onClick={() => setCount(count + 1)}>Re-render ({count})</button>
</div>
);
};
export default App;

What’s Happening Here?

  1. The calculateSum() function takes time to compute for large numbers.
  2. Normally, every render would call calculateSum() again, even if num hasn’t changed.
  3. useMemo prevents unnecessary re-runs by caching the result and only recomputing if num changes.
  4. Clicking the “Re-render” button updates count, causing a re-render, but useMemo stops the sum from being recalculated unnecessarily.

Example 2: Optimizing Filtering

useMemo is also useful when filtering large lists. Without it, the filter function runs on every render, even if the data hasn’t changed.

import React, { useState, useMemo } from "react";

const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" },
{ id: 4, name: "David" },
];
const UserList = ({ search }) => {
// Filter users based on search term
const filteredUsers = useMemo(() => {
return users.filter((user) => user.name.toLowerCase().includes(search.toLowerCase()));
}, [search]); // Runs only when 'search' changes
return (
<ul>
{filteredUsers.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};

Key Takeaways

  • We use useMemo when you have expensive calculations that don’t need to be re-run every render.
  • It only recalculates when dependencies change, avoiding unnecessary computation.
  • Great for optimizing lists, filtering, and expensive calculations.

When NOT to Use useMemo

🚫 Don’t use useMemo for every small function—React is already optimized for most cases.

🚫 If the computation is fast, adding useMemo can add unnecessary complexity.

🚫 useMemo is used for memoizing only values and not callback functuons —for that, we should use useCallback. (More on that next! 👀)

useCallback()

The useCallback hook is used to memoize functions, ensuring they don’t get re-created on every render.

If your function doesn’t change between renders but is still being re-created, it can lead to unnecessary re-renders of child components. useCallback helps prevent unnecessary re-renders by keeping the function reference stable unless dependencies change.

Syntax

const memoizedFunction = useCallback(() => {
// function logic here
}, [dependencies]);

Why Do We Need useCallback?

In React, functions are re-created on every render, even if their logic remains the same.

This is usually fine, but it can cause performance issues when:

  1. Functions are passed as props to child components → The child re-renders unnecessarily.
  2. Functions are used inside useEffect → React sees a "new" function and triggers the effect again.

useCallback memoizes the function, meaning React will only re-create it when dependencies change, reducing unnecessary renders.

Example 1: Preventing Child Component Re-renders

Let’s say we have a child component that only needs to re-render when its onClick function changes.

import React, { useState, useCallback } from "react";

// Child component
const Button = React.memo(({ onClick }) => {
console.log("Button re-rendered!");
return <button onClick={onClick}>Click Me</button>;
});
const App = () => {
const [count, setCount] = useState(0);
// Without useCallback, this function is recreated on every render
const handleClick = useCallback(() => {
setCount((prev) => prev + 1);
}, []); // No dependencies → function remains the same
return (
<div>
<p>Count: {count}</p>
<Button onClick={handleClick} />
</div>
);
};
export default App;

What’s Happening Here?

  1. Normally, without useCallback, handleClick would be re-created on every render.
  2. Since handleClick is passed as a prop to the Button component, React thinks it's a new function and re-renders the child component unnecessarily.
  3. useCallback fixes this by keeping the function reference the same, preventing unnecessary re-renders.
  4. React.memo further optimizes by only re-rendering if props actually change.

Example 2: Optimizing Functions Inside useEffect

When using useEffect, React checks if dependencies have changed. Since functions are recreated on every render, useEffect will run again unnecessarily.

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

const FetchData = () => {
const [data, setData] = useState(null);
// Without useCallback, fetchData is recreated on every render, triggering useEffect again
const fetchData = useCallback(async () => {
console.log("Fetching data...");
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
const result = await response.json();
setData(result);
}, []); // Empty dependency means function stays the same
useEffect(() => {
fetchData(); // Will only run once on mount
}, [fetchData]); // If fetchData changed, useEffect would re-run
return <pre>{JSON.stringify(data, null, 2)}</pre>;
};

export default FetchData;

Key Takeaways

✔ Use useCallback when passing functions as props to prevent unnecessary child component re-renders.

✔ Use it inside useEffect to avoid unnecessary effect executions.

useCallback memoizes the function itself, not its result. (For memoizing values, use useMemo instead.)

When NOT to Use useCallback

🚫 If the function doesn’t cause unnecessary re-renders, you don’t need useCallback.

🚫 For simple inline event handlers, creating a new function is fine — React optimizes this well.

🚫 If the function depends on frequently changing values, memoization won’t help.

These are all the important hooks that you need to know, to get your hands dirty with react.

If you like the post, do share with your friends.

THE END

Thanks for Reading and stay tuned for more

--

--

No responses yet