🧠 Demystifying Custom Hooks, Memoization, and useCallback
in React
React’s flexibility empowers developers to write expressive, component-driven code. But with that power comes architectural responsibility—especially when abstracting shared logic. This post will walk you through:
useCallback
mattersuseSearchParams
custom hookA custom hook is simply a function that uses other hooks. Nothing more, nothing less.
function useCount() {const [count, setCount] = useState(0)const increment = () => setCount(c => c + 1)return { count, increment }}
We can then use it like this:
function Counter() {const { count, increment } = useCount()return <button onClick={increment}>{count}</button>}
Using the use
prefix is not just a naming convention—it ensures React treats it like a hook and applies the rules of hooks properly (e.g., only calling hooks at the top level).
🧠 Why use custom hooks? They encapsulate logic so you can reuse it across components without repeating yourself.
Let’s say you’re using a function returned from a hook (like increment
) inside a useEffect
:
function Counter() {const { count, increment } = useCount()useEffect(() => {const id = setInterval(increment, 1000)return () => clearInterval(id)}, [increment])return <div>{count}</div>}
Even though increment
does the same thing each time, its identity changes on every render. That means your useEffect
re-runs every time.
This is bad because:
useCallback
useCallback
lets you tell React to “remember” the same function instance as long as its dependencies haven’t changed.
function useCount() {const [count, setCount] = useState(0)const increment = useCallback(() => setCount(c => c + 1), [])return { count, increment }}
Now increment
stays stable across renders.
You can think of useCallback
as a cache:
let lastCallbackfunction useCallback(callback, deps) {if (depsChanged(deps)) {lastCallback = callbackreturn callback}return lastCallback}
Memoization is a performance optimization technique: instead of recalculating values, we store and reuse results for the same inputs.
Basic memoization:
const cache = {}function addOne(num: number) {if (cache[num] === undefined) {cache[num] = num + 1}return cache[num]}
Generic memoize function:
function memoize<Arg, Result>(cb: (arg: Arg) => Result) {const cache: Record<string, Result> = {}return function(arg: Arg) {if (cache[arg as any] === undefined) {cache[arg as any] = cb(arg)}return cache[arg as any]}}
useCallback
vs useMemo
Both are for memoization.
useCallback(fn, deps)
is shorthand for useMemo(() => fn, deps)
useCallback
returns a memoized functionuseMemo
returns a memoized valueconst increment = useCallback(() => setCount(c => c + 1), [])// is equivalent to:const increment = useMemo(() => () => setCount(c => c + 1), [])
Always remember: dependencies must be stable, or your memoization will be broken.
useSearchParams
HookSuppose you have some logic in your App
component that manipulates the browser’s search params:
const [searchParams, setSearchParams] = useState(...)
You want to extract this logic into a custom hook so other components can reuse it.
function useSearchParams(): [URLSearchParams,(newParams: Record<string, string>) => void] {const [params, setParams] = useState(() => new URLSearchParams(window.location.search))const setSearchParams = useCallback((newParams: Record<string, string>) => {const newSearchParams = new URLSearchParams()for (const key in newParams) {newSearchParams.set(key, newParams[key])}const newUrl = `${window.location.pathname}?${newSearchParams.toString()}`window.history.pushState(null, '', newUrl)setParams(newSearchParams)}, [])return [params, setSearchParams]}
Now use it in your component like this:
function App() {const [searchParams, setSearchParams] = useSearchParams()function handleClick() {setSearchParams({ query: 'hello' })}return <button onClick={handleClick}>Search</button>}
✅ Why wrap
setSearchParams
withuseCallback
? So it stays referentially stable, in case someone needs to use it inside auseEffect
.
useCallback
?No! Overusing useCallback
can actually hurt performance by cluttering your code and forcing React to do unnecessary dependency checks.
Only use it when:
useEffect
or useMemo
dependency arrayReact.memo
)useCallback
memoizes functions so they’re stable across renders.useCallback
when it matters—especially in reusable hooks.🎉 Congratulations! You’ve taken a deep dive into memoization, abstraction, and dependency management in React.
Quick Links
Legal Stuff
Social Media