DEV Community

Jalaj Bankar
Jalaj Bankar

Posted on

Promises, Data Flow, and the memo + useCallback Combo That Actually Makes React Fast

A big session with some genuinely advanced React in the second half. The memo and useCallback combination is one of those things that looks complicated but makes complete sense once you understand why it exists.


.then() Can Handle Rejections Too — Not Just .catch()
Most people only use .catch() for errors and .then() for success. But .then() actually accepts two functions — one for success, one for rejection:
promise
.then(successFn) // handles success only
.then(successFn, errorFn) // handles both
.catch(errorFn) // handles rejection only

Here's the quick reference:
.then(successFn) — success ✅, rejection ❌ skipped
.then(successFn, errorFn) — success ✅, rejection ✅
.catch(errorFn) — success ❌ skipped, rejection ✅

And the other thing worth locking in — whatever you return inside a .then() gets passed down to the next .then() or .catch() waiting below it. The chain is just values being handed down one step at a time.


React Data Flow — The Parent is Always in Charge
A few rules that are worth stating clearly because they govern everything in React:
Only one function gets export default — that's your root component.
A prop-name and the variable holding it can be the same name — is perfectly valid.
Re-rendering starts from the Parent — no child re-renders on its own. The parent gives permission, intentionally or not.
Here's the full flow when a child updates state:
Child runs a setter function → Parent's state updates → Parent re-renders → all children re-render by default, whether they needed to or not.
That last part is the problem. A child that didn't receive any new data still re-renders just because it lives inside the parent. In small apps this is fine. In larger ones it becomes a real performance issue.


memo — Telling React "Don't Re-render This Unnecessarily"
memo wraps a component and tells React — "only re-render this if its props actually changed."
import { memo } from 'react';
export const Child1 = memo(({ setToggle }) => {
console.log("Child1 Rendered!");
return <button onClick={() => setToggle((t) => !t)}>Toggle</button>;
});

Sounds perfect. But there's a catch.

The memo Trap — Functions Are Always "New"
Every time App re-renders, any function defined inside it gets recreated from scratch. It looks the same to you, but React sees it as a brand new function reference. So even with memo, Child1 re-renders — because React thinks setToggle changed.
Same logic, new object. memo checks props with === — and two different function references are never === even if they do the exact same thing.


useCallback — Keeping Function References Stable
useCallback wraps a function and says "keep this exact same reference between renders unless the dependencies change."

import { useState, useCallback, memo } from 'react';
export default function App() {
const [toggle, setToggle] = useState(false);
const memoizedSetToggle = useCallback(() => {
setToggle((t) => !t);
}, []);
return (
<>
<Child1 setToggle={memoizedSetToggle} />
<Child2 toggle={toggle} />
</>
);
}
const Child1 = memo(({ setToggle }) => {
console.log("Child1 only renders ONCE now!");
return <button onClick={setToggle}>Toggle</button>;
});
const Child2 = memo(({ toggle }) => {
console.log("Child2 renders when toggle changes.");
return <p>{toggle ? "ON" : "OFF"}</p>;
});

Here's why this works properly now:
Child1 — receives memoizedSetToggle. Since useCallback keeps the same function reference every render, memo correctly sees "props didn't change" and skips the re-render. Renders once, stays there.
Child2 — receives toggle. That value genuinely changes on every click, so Child2 should re-render to show the updated text. And it does.
The mental model is simple — memo skips re-renders when props don't change. useCallback makes sure function props actually don't change between renders. They need each other to work properly. One without the other only solves half the problem.

Top comments (0)