Unlocking the Power of React Hooks: Mastering useState, useEffect, useRef, useMemo & useCallback


1. Introduction
Hooks transformed React forever. They let you write cleaner, simpler, and more powerful functional components.
In this post, we’ll cover five essential hooks you need to master:
useState
useEffect
useRef
useMemo
useCallback
By the end, you’ll understand when and why to use each to write better React apps.
2. useState
What is useState?
useState is a React Hook that lets you add state to your functional components. Before hooks came along, only class components could have state, which made writing React components more verbose and complex. useState changed the game by allowing you to use state inside simpler, cleaner function components.
How does useState work?
When you call useState, you get back an array of two things:
The current state value
A function to update that state
You can think of it as:
const [state, setState] = useState(initialValue);
state holds the current value.
setState is a function you call to change the state and tell React to re-render the component with the new value.
A Simple Example
Let's look at a simple counter that increases a number every time you click a button:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // initialize state to 0
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
How this works:
On the first render,
count
is set to0
because of the argument passed touseState(0)
.Clicking the button calls
setCount(count + 1)
, which updates the state and triggers a re-render.The updated
count
value then shows up in the UI.
Quick tips:
Always use the setter function (setCount) to update state — never mutate the state directly.
The setter function can also take a callback if the new state depends on the previous state:
setCount(prevCount => prevCount + 1); // prevCount is not the state(count) but holds the current value of the state
3. useEffect
What is useEffect?
useEffect is a React Hook that lets you perform side effects in function components.
Side effects are anything your component does that affects something outside its scope — like:
Fetching data from an API
Subscribing to events
Manually updating the DOM
Timers or intervals
Logging or analytics
Think of it as a function that lets you perform certain tasks when some values in the component change. Before hooks, these tasks were managed using lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount in class components. useEffect combines all that functionality into one simple API for function components.
How does useEffect work?
You pass a function to useEffect, and React will call that function after the component renders. This function can also optionally return a cleanup function that runs before the component unmounts or before the effect runs again.
useEffect(() => {
// Code here runs after render
return () => {
// Cleanup code runs before next effect or unmount
};
}, [dependencies]);
The dependency array (optional) tells React when to re-run the effect:
If empty (
[]
), run once after the first render (like componentDidMount).If it contains variables, re-run whenever those variables change.
if omitted, the effect runs after every render.
Simple Example: Logging a message when a count changes
import React, { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`You clicked ${count} times`);
}, [count]); // Only re-runs when `count` changes
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
When and where to use useEffect?
Fetching data on component mount or when dependencies change.
Setting up and cleaning up event listeners or subscriptions.
Updating the DOM outside React’s control (e.g., integrating third-party libraries).
Running timers or intervals and clearing them on unmount.
Performing side effects based on state or props changes.
Why useEffect is important?
It handles side effects cleanly inside functional components.
Combines lifecycle events into a unified, flexible API.
Helps keep UI and side effects in sync with React’s render cycle.
Avoids common bugs like memory leaks by supporting cleanup.
Quick tips:
- Always clean up subscriptions or timers inside the cleanup function to avoid leaks.
useEffect(() => {
const timer = setInterval(() => {
console.log('Tick');
}, 1000);
return () => clearInterval(timer); // Cleanup on unmount or before next effect
}, []);
Be mindful of what you put in the dependency array — incorrect dependencies can cause infinite loops or missed updates.
4. useRef
What is useRef?
useRef is a React Hook that lets you create a mutable reference that persists across renders without causing re-renders when it changes.
Think of it as a box where you can keep a value that won’t trigger your component to update if it changes — perfect for storing:
DOM elements references
Mutable variables that need to persist between renders but shouldn’t cause a re-render when updated.
How does useRef work?
You call useRef and get back an object with a .current
property.
const myRef = useRef(initialValue);
myRef.current
initially equalsinitialValue
.You can read or write to
myRef.current
at any time.Updating
myRef.current
does not cause the component to re-render.
Common Use Cases for useRef
1. Referencing DOM elements directly
function TextInputWithFocusButton() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus(); // Accessing the DOM node directly
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus the input</button>
</>
);
}
Here, inputRef.current
points to the actual DOM input element, letting you call methods like .focus()
.
2. Storing mutable values that don’t trigger re-render
function Timer() {
const countRef = useRef(0);
useEffect(() => {
const interval = setInterval(() => {
countRef.current += 1;
console.log(countRef.current); // Keeps track without re-rendering
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>Open console to see the count</div>;
}
Quick tips:
Don’t use useRef to replace state for data that affects rendering UI. Use useState for that.
useRef updates don’t cause renders, so UI won’t reflect changes automatically.
5. useMemo
What is useMemo?
useMemo is a React Hook that memoizes (remembers) the result of a function so that React recomputes it only when its dependencies change.
It helps optimize performance by avoiding expensive recalculations on every render.
How does useMemo work?
You pass a function and a dependency array to useMemo:
const memoizedValue = useMemo(() => {
// Expensive calculation here
return computeSomething();
}, [dependencies]);
React runs the function and stores the result.
On subsequent renders, React returns the stored result without recalculating — unless one of the dependencies has changed.
If dependencies change, React recomputes and stores the new result.
Simple Example: Memoizing an expensive calculation
import React, { useState, useMemo } from 'react';
function ExpensiveComponent({ number }) {
const expensiveCalculation = (num) => {
console.log('Calculating...');
let result = 0;
for (let i = 0; i < 1000; i++) {
result += num * i;
}
return result;
};
const memoizedResult = useMemo(() => expensiveCalculation(number), [number]);
return <div>Result: {memoizedResult}</div>;
}
The expensive calculation only runs when
number
changes.If the component re-renders without
number
changing, React skips the calculation and uses the memoized value, improving performance.
When and where to use useMemo?
Use it to optimize expensive computations that don’t need to run on every render.
Use it when you want to avoid recalculating values unnecessarily, especially in large lists, complex calculations, or derived data.
Use it carefully — don’t overuse. Memoization itself has a cost; only apply it when the performance gain is worth it.
Why useMemo is important?
Prevents unnecessary recalculations, improving render performance.
Helps keep your app responsive, especially with complex UIs or large datasets.
Works well with
React.memo
to avoid unnecessary component re-renders.
Quick tips:
The dependency array is critical — if dependencies are wrong or missing, you may get stale values or unnecessary recalculations.
Don’t memoize everything blindly; focus on parts that really benefit.
Use tools like React DevTools Profiler to identify performance bottlenecks before adding memoization.
6. useCallback
What is useCallback?
useCallback is a React Hook that memoizes a function — it returns a cached version of the callback function that only changes if one of its dependencies changes.
This helps prevent unnecessary re-creations of functions on every render, which can avoid needless re-renders of child components or expensive effects.
How does useCallback work?
You pass a function and a dependency array to useCallback:
const memoizedCallback = useCallback(() => {
// Your callback code here
}, [dependencies]);
React returns the same function instance between renders as long as dependencies stay the same.
If dependencies change, React creates and returns a new function.
Why memoize functions?
In React, functions are recreated on every render by default. This means:
If you pass functions as props to child components, those children may re-render unnecessarily because props changed (new function reference).
Some hooks like
useEffect
oruseMemo
depend on stable function references.
useCallback helps you keep function references stable, preventing these issues.
Example: Prevent unnecessary child re-renders
import React, { useState, useCallback } from 'react';
const Child = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click me</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// Without useCallback, this function changes every render!
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
return (
<>
<div>Count: {count}</div>
<Child onClick={handleClick} />
</>
);
}
handleClick
is wrapped withuseCallback
and won't be recreated unless dependencies change.Because of this,
Child
won’t re-render unnecessarily.
When and where to use useCallback?
When you pass callbacks to memoized child components (
React.memo
).When you want to avoid re-creating functions that are dependencies of other hooks (
useEffect
,useMemo
, etc.).When performance matters and you want to reduce unnecessary renders or computations.
Important notes
useCallback itself has some overhead; don’t overuse it everywhere — use it when it solves actual performance or re-render issues.
If your component or children aren’t memoized, useCallback usually doesn’t add much value.
Now, I hope this article helped you clarify the key concepts of these React hooks and how to use them effectively in your projects. If you have any questions or want me to cover more React topics, just let me know!
Subscribe to my newsletter
Read articles from Abhishek Wilson directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Abhishek Wilson
Abhishek Wilson
I break things, debug for hours, and somehow still love it. Writing to reflect, simplify, and grow one post at a time.