React Hooks 101
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!
Table Of Content
Types of Hooks
1. useState
2. useEffect()
3. useRef()
4. useMemo()
5. useCallback()
Types of Hooks
React Hooks can be divided into two categories:
- Built-in Hooks — These are provided by React itself and cover most use cases.
- 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 (initiallyfalse
).setIsChecked
updates the state when the checkbox is toggled.
How Does useState
Work
The useState
hook returns an array with two values:
- The Current State
- This is the value stored in the state at any given time. During the initial render, it takes the
initialValue
. - The State Setter Function (
setState
) - 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
- Runs on Initial Render
- When the component first mounts (renders for the first time), the
setupFunction
insideuseEffect
executes.
- Runs When Dependencies Change
- After the initial render,
useEffect
re-runs whenever any value inside the[dependencies]
array changes.
- 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 wheneversomeValue
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:
- The user types into the input field.
- The
title
state updates. - 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
- Accessing DOM Elements
- You can use
useRef
to get direct access to a DOM element without usingdocument.querySelector
.
- Storing Mutable Values Without Re-rendering
- Unlike
useState
, changing auseRef
value doesn’t cause the component to re-render.
- 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:
useRef(null)
creates a reference (inputRef
).- The
ref
attribute is assigned to the<input>
element. - 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:
- The
prevCountRef
stores the previous count value. - When
count
updates,useEffect
runs and updatesprevCountRef.current
. - 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?
- The
calculateSum()
function takes time to compute for large numbers. - Normally, every render would call
calculateSum()
again, even ifnum
hasn’t changed. useMemo
prevents unnecessary re-runs by caching the result and only recomputing ifnum
changes.- Clicking the “Re-render” button updates
count
, causing a re-render, butuseMemo
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:
- Functions are passed as props to child components → The child re-renders unnecessarily.
- 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?
- Normally, without
useCallback
,handleClick
would be re-created on every render. - Since
handleClick
is passed as a prop to theButton
component, React thinks it's a new function and re-renders the child component unnecessarily. useCallback
fixes this by keeping the function reference the same, preventing unnecessary re-renders.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