Most React performance tutorials end with the same bad advice: wrap everything in useMemo and useCallback and call it a day. If you have followed that advice, you have probably made your application slower. These hooks are not free. Each one allocates memory, compares dependencies, and stores cached values. Used without purpose, they become overhead instead of optimization.
Let’s strip this down to what actually matters.
What Each Hook Really Does
useMemo remembers a value. You hand it a function that does heavy work, and it returns the result. On the next render, if your dependencies have not changed, React skips the calculation and hands back the old result.
useCallback remembers a function. It does not run the function for you. It simply returns the same function instance between renders as long as its dependencies stay the same.
That is the entire difference. One caches a computed value. One caches a reference. Mixing these up leads to code that looks optimized but behaves identically to unwrapped code while costing extra memory.
Why Function Identity Breaks Your Tree
When a component re-renders, React executes the entire function body again. Every variable is recreated. Every inline function gets a brand new address in memory.
In JavaScript, two functions that contain the exact same logic are not equal. () => {} === () => {} evaluates to false. The same rule applies to objects and arrays. If your parent component defines handleSubmit and passes it to a child, that child receives a new prop on every single render. Even if the child is wrapped in React.memo, it cannot tell that the new function does the same thing as the old one. The reference changed, so the child re-renders.
This is the root problem useCallback was built to solve. It is not about speed. It is about stability.
When useMemo Earns Its Keep
You need useMemo when you are performing work that is objectively expensive and you can see a measurable lag.
Think about filtering a massive dataset. If you have a table with tens of thousands of rows and a search input, you might write something like this inside your component:
const visibleRows = rows.filter(r => r.name.includes(query));
Without useMemo, that loop runs on every render. If the user clicks a button that toggles a sidebar, the parent re-renders, and your filter runs again even though rows and query never changed. On a large dataset, that stutter is visible.
useMemo fixes this by pinning the result:
const visibleRows = useMemo(() => {
return rows.filter(r => r.name.includes(query));
}, [rows, query]);
Now React only re-runs that filter when the dependencies actually change.
The same logic applies to complex mathematical calculations, transforming API responses into chart-friendly formats, or deriving state that would otherwise be recalculated constantly.
There is a second, less obvious use case. If you create an object or array locally and include it in a useEffect dependency array, you can accidentally trigger that effect on every render. Inline objects and arrays get new identities each time, so the effect sees a changed dependency and fires again. Memoizing that object with useMemo keeps the reference stable and lets your effect run only when the underlying data actually changes.
When useCallback Becomes Necessary
useCallback matters most when you are passing handlers into child components that are optimized with React.memo.
Imagine a parent component that holds a counter. It also renders an expensive child list:
function Parent() {
const [count, setCount] = useState(0);
const handleItemClick = (id) => {
console.log(id);
};
return (
<div>
<button onClick={() => setCount(c + 1)}>{count}</button>
<ExpensiveList onItemClick={handleItemClick} />
</div>
);
}
Every time count changes, Parent re-renders. A fresh handleItemClick is created. Because ExpensiveList receives a new prop reference, it re-renders too. If ExpensiveList is wrapped in React.memo, that memoization is completely wasted because the function prop changed.
useCallback preserves the reference:
const handleItemClick = useCallback((id) => {
console.log(id);
}, []);
Now ExpensiveList only re-renders when it truly needs to.
Another critical situation involves useEffect. If an effect subscribes to a function defined inside your component, and that function changes identity every render, the effect will teardown and resubscribe repeatedly. Memoizing the function keeps the effect stable.
The Dependency Array Trap and Stale Closures
Both hooks rely on dependency arrays, and this is where most bugs hide.
dependency array-ൽ നിന്ന് ഒരു വേരിയബിൾ ഒഴിവാക്കിയാൽ, നിങ്ങളുടെ memoized function അല്ലെങ്കിൽ value ആ വേരിയബിളിന്റെ പഴയ പതിപ്പിനെയാണ് (old version) ഉപയോഗിക്കുന്നത്. ഇതിനെയാണ് stale closure എന്ന് പറയുന്നത്. UI പുതിയ ഡാറ്റ കാണിച്ചേക്കാം, പക്ഷേ നിങ്ങളുടെ callback മൂന്ന് renders മുൻപുള്ള state ആണ് നോക്കുന്നത്. ഇതിനുള്ള പരിഹാരം ലളിതമാണ്, പക്ഷേ code reviews സമയത്ത് ഇത് ശ്രദ്ധിക്കപ്പെടാതെ പോകാം: hook-നുള്ളിൽ ഉപയോഗിക്കുന്നതും മാറാൻ സാധ്യതയുള്ളതുമായ എല്ലാ മൂല്യങ്ങളും (values) ഉൾപ്പെടുത്തുക.
react-hooks/exhaustive-deps എന്ന ESLint rule പ്രവർത്തിപ്പിക്കുക. ഇത് വ്യക്തമായ പിഴവുകൾ കണ്ടെത്തും. എന്നാൽ ഇതിനെ ഒരു റോബോട്ടിനെപ്പോലെ കാണരുത്. ഓരോ dependency-യും എന്തുകൊണ്ട് പ്രധാനമാണെന്ന് മനസ്സിലാക്കുക.
അമിതമായ ഒപ്റ്റിമൈസേഷന്റെ (Over-Optimization) മറഞ്ഞിരിക്കുന്ന വില
തുടക്കക്കാർ പലപ്പോഴും സുരക്ഷിതമാണെന്ന് തോന്നുന്നതുകൊണ്ട് എല്ലാ function-കിലും value-കളിലും ഈ hooks ഉപയോഗിക്കാറുണ്ട്. എന്നാൽ ഈ ശീലം തിരിച്ചടിയാകും.
React കാഷെ ചെയ്ത (cached) മൂല്യങ്ങൾ മെമ്മറിയിൽ സൂക്ഷിക്കേണ്ടതുണ്ട്. ഓരോ render സമയത്തും, അത് നിങ്ങളുടെ dependency array പരിശോധിക്കുകയും Object.is ഉപയോഗിച്ച് ഓരോ ഐറ്റവും താരതമ്യം ചെയ്യുകയും ചെയ്യുന്നു. ഈ താരതമ്യം വേഗത്തിൽ നടക്കുമെങ്കിലും അത് സൗജന്യമല്ല. onClick={() => setOpen(true)} പോലുള്ള ലളിതമായ ഒരു event handler useCallback-നുള്ളിൽ ഉൾപ്പെടുത്തിയാൽ, വളരെ വേഗത്തിൽ നിർമ്മിക്കാവുന്ന ഒരു function ഒഴിവാക്കാൻ വേണ്ടി നിങ്ങൾ മെമ്മറിയും CPU ശേഷിയും പാഴാക്കുകയാണ് ചെയ്യുന്നത്.
ഈ hooks കോഡിൽ അനാവശ്യമായ സങ്കീർണ്ണത (noise) കൂട്ടുന്നു. useMemo, useCallback എന്നിവ ഉപയോഗിച്ച് എഴുതിയ കോഡ് വായിക്കാനും പരിപാലിക്കാനും (maintain) പ്രയാസമാണ്. ഓരോ dependency array-യും നിങ്ങൾക്ക് പ്രശ്നങ്ങൾ ഉണ്ടാക്കാൻ സാധ്യതയുള്ള ഒരു stale closure ആയി മാറാം.
യഥാർത്ഥ തത്വം ലളിതമാണെങ്കിലും ഫലപ്രദമാണ്: ആദ്യം സാധാരണ രീതിയിൽ കോഡ് എഴുതുക. ഒരു പ്രശ്നം ഉണ്ടെന്ന് ഉറപ്പായ ശേഷം മാത്രം ഒപ്റ്റിമൈസ് ചെയ്യുക. ഏതെല്ലാം components ആണ് കൂടുതൽ സമയം എടുക്കുന്നത് എന്നും ഏതെല്ലാം renders ആണ് അനാവശ്യമാണെന്നും കണ്ടെത്താൻ React DevTools Profiler ഉപയോഗിക്കുക. ഒരു render ഏതാനും മില്ലിസെക്കൻഡുകൾ താഴെ മാത്രം എടുക്കുന്നുണ്ടെങ്കിൽ, ഒരു ഉപയോക്താവും അത് ശ്രദ്ധിക്കില്ല, അങ്ങനെയുള്ള സാഹചര്യത്തിൽ നിങ്ങളുടെ memoization കൊണ്ട് ഒരു പ്രയോജനവുമില്ല.
ചുരുക്കത്തിൽ
useMemo എന്നത് expensive ആയ മൂല്യങ്ങൾക്കാണ്. useCallback എന്നത് stable ആയ function references-ന് വേണ്ടിയുള്ളതാണ്. ഈ hooks ഉപയോഗിക്കുന്നത് കൊണ്ട് മാത്രം നിങ്ങളുടെ component വേഗത്തിൽ render ആകില്ല; അവ അനാവശ്യമായ ജോലികൾ ഒഴിവാക്കുകയാണ് ചെയ്യുന്നത്. ഇവ ഇല്ലാതെ തന്നെ തുടങ്ങുക, ടൂളുകൾ ഉപയോഗിച്ച് അളക്കുക, പ്രൊഫൈലർ ഒരു തടസ്സം (bottleneck) കാണിക്കുന്ന ഇടങ്ങളിൽ മാത്രം ഇവ കൃത്യമായി ഉപയോഗിക്കുക. ഇടയ്ക്കിടെ rerender ആകുന്ന വൃത്തിയുള്ള കോഡ്, എല്ലാം memoize ചെയ്യുന്ന അമിതമായി സങ്കീർണ്ണമാക്കിയ (over-engineered) കോഡിനേക്കാൾ എപ്പോഴും മികച്ചതായിരിക്കും.
