React performance depends on knowing when to memoize. Learn the mental model for using useMemo and useCallback effectively without falling into optimization traps.
I remember sitting through an on-call rotation where a senior dev—who should have known better—wrapped every single function in useCallback and every object in useMemo. They thought they were building a "fast" app. Instead, they built a memory-hungry monster that was actually slower than the original implementation.
We've all been there, tempted to reach for useMemo or useCallback the moment a component feels a bit sluggish. But memoization isn't free. If you're struggling to understand the right time to reach for these hooks, you aren't alone. It’s a common hurdle when you’re moving from "making it work" to "making it scale."
Before we dive into the syntax, let’s talk about the cost. Memoization works by storing the result of a calculation or a function reference in memory. On subsequent renders, React compares the dependencies you provided to the previous ones. If they’re the same, it returns the cached version.
The catch? That comparison isn't free. You’re trading CPU cycles (for re-running code) for memory (to store the cached result) and extra logic (to compare dependencies). Sometimes, the cost of checking the dependencies is higher than the cost of just re-running the function.
I usually tell juniors to look at it this way:
React.memo component?).If the answer to all three is "no," you don't need memoization.
You should use useMemo when you have a function that performs a heavy computation. In a recent project, we had a data transformation utility that took around 15ms to run on every render. That doesn't sound like much, but when you have a list of 50 rows, you’re suddenly looking at a 750ms blocking time just for data processing.
JAVASCRIPTconst filteredData = useMemo(() => { return performExpensiveCalculation(rawData, filterCriteria); }, [rawData, filterCriteria]);
That’s a classic win. The calculation only runs when rawData or filterCriteria changes. If the component re-renders because of a parent state change that doesn't affect these two variables, the calculation is skipped entirely.
The useCallback hook is slightly different. It doesn't cache a value; it caches a function reference. This is crucial when you pass functions down as props to child components wrapped in React.memo.
If you don't use useCallback, the child component sees a "new" function on every render, even if the logic inside is identical. This triggers a re-render in the child, effectively breaking the optimization you were trying to achieve.
Here is where I see most people go wrong: they wrap every event handler in useCallback. Don't do that. Only use it when:
React.memo.useEffect hook.If the component is just a standard button or an input, the overhead of creating the function is smaller than the overhead of managing the useCallback dependency array.
If you find yourself constantly guessing, stop. You need to verify your assumptions. I highly recommend spending time on React performance debugging: How to trace component re-renders before you start sprinkling memoization everywhere.
Often, the issue isn't a slow calculation—it's an unnecessary re-render caused by state being lifted too high or a missing key prop in a list. I’ve seen developers spend hours memoizing components only to realize their state management was the real bottleneck.
If you are dealing with massive data sets, you might also want to look into Progressive Hydration: Boosting Perceived Performance on Large Dashboards. Sometimes the solution isn't making the code faster, but changing when the code runs.
Q: Should I wrap all my event handlers in useCallback? A: No. It adds complexity and memory overhead. Only do it if you are passing the function to a memoized child component that depends on prop stability to avoid re-renders.
Q: Does useMemo always make things faster? A: Definitely not. For simple calculations (like adding two numbers), the cost of the hook and the dependency comparison can be slower than the calculation itself.
Q: How do I know if my calculation is "expensive"? A: If you aren't sure, don't guess. Use the React DevTools Profiler. If a component isn't taking more than a few milliseconds to render, you likely don't need to optimize it.
The best advice I can give you is to treat memoization as a surgical tool, not a blunt instrument. Start by building your UI cleanly. If you notice jank or the Profiler shows a component re-rendering unnecessarily, investigate why.
I’m still surprised by how often I see devs reach for useMemo before they even measure the render time. Next time, try deleting your memoization hooks and see if the app actually gets slower. You might be surprised to find it makes zero difference—or that it actually feels snappier because you’ve removed the overhead of the dependency checks.
React form handling doesn't have to be complex. Learn the trade-offs between controlled and uncontrolled components to decide when to sync your UI state.