🧠 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:
useCallbackuseCallback 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 useMemoBoth 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
setSearchParamswithuseCallback? 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.