From fe06effd21aacec84850d044862ae8a8c539cf5f Mon Sep 17 00:00:00 2001
From: David Almeida <1083195+davidbarna@users.noreply.github.com>
Date: Thu, 5 Mar 2026 08:35:55 +1100
Subject: [PATCH] feat: modernize React Memoization chapter
- Update React.memo to memo() named import style (React 18+)
- Convert all code snippets from JS/JSX to TypeScript/TSX with type annotations
- Replace curried event handler example with idiomatic name-attribute pattern
- Add fiber node caveats to simplified useCallback/useMemo implementations
- Expand "when to use / when not to" with dedicated slides and code examples
- Add React Compiler section covering auto-memoization in React 19
- Restructure intro into Equality in JavaScript / Functions are objects sections
---
src/md/ReactMemoization.md | 404 +++++++++++++++++++++++--------------
1 file changed, 251 insertions(+), 153 deletions(-)
diff --git a/src/md/ReactMemoization.md b/src/md/ReactMemoization.md
index 7c20736..5786322 100644
--- a/src/md/ReactMemoization.md
+++ b/src/md/ReactMemoization.md
@@ -2,34 +2,21 @@
-## Functions are objects
-
-> In JavaScript, almost everything is an object.
-
-```js
-typeof [] // "object"
-typeof {} // "object"
-typeof function() {} // "function" — but still an object
-```
-
-Functions inherit from `Object` — they have properties, can be stored, passed, and returned.
-
-[MDN // Function object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function)
+## Equality in JavaScript
-### Primitives are the exception
+### Primitives
-```js
+```ts
typeof 'hello' // "string"
typeof 42 // "number"
typeof true // "boolean"
```
-> Primitives are not objects.
> They are compared by **value**.
-```js
+```ts
'hello' === 'hello' // true
42 === 42 // true
```
@@ -38,7 +25,7 @@ typeof true // "boolean"
### Objects are compared by reference
-```js
+```ts
const a = {}
const b = {}
@@ -51,9 +38,57 @@ a === b // false
-### Functions follow the same rule
+## Functions are objects
+
+> In JavaScript, almost everything is an object.
+
+```ts
+typeof [] // "object"
+typeof {} // "object"
+typeof function() {} // "function" — but still an object
+```
+
+Functions inherit from `Object` — they have properties, can be stored, passed, and returned.
+
+[MDN // Function object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function)
+
+
+
+### Function properties and methods
```js
+Function.prototype = {
+ length: Number, // Specifies the number of arguments expected.
+ name: String, // The name of the function.
+ /* ... */
+ apply: Function, // Calls a function and sets its this to the provided value
+ call: Function, // Calls (executes) a function and sets its this to the provided value
+ bind: Function, // Creates a new function which, when called, has its this set to the provided value.
+ /* ... */
+ __proto__: Object.prototype,
+};
+```
+
+
+
+> Any function is an **instance of** the `Function` constructor
+
+```ts
+console.log((function() {}).length); // 0
+console.log((function(a) {}).length); // 1
+console.log((function(a, b) {}).length); // 2
+console.log((function(a, b = 1) {}).length); // 1
+console.log((function(...args) {}).length); // 0
+
+console.log((function() {}).name); // ""
+console.log((function doSomething() {}.name); // "doSomething"
+```
+
+
+
+### As objects, functions are compared by reference
+
+```ts
const greet = () => 'Hello'
const greet2 = () => 'Hello'
@@ -62,13 +97,21 @@ greet === greet2 // false
Each declaration allocates a **new function object** in memory.
+
+
+
+## React rendering and referential equality
+
+> By default, every time a parent component re-renders, all its children re-render too —
+> even if their props have not changed.
+
-### In React, this matters
+### Functions are still objects
A component function re-executes on every render.
-```jsx
+```tsx
function MyComponent() {
const handleClick = () => console.log('clicked')
// ↑ a brand new object on every render
@@ -80,22 +123,17 @@ function MyComponent() {
> Every render produces a new `handleClick` instance,
> even though nothing about it has changed.
-
-
-## React rendering and referential equality
-
-> By default, every time a parent component re-renders, all its children re-render too —
-> even if their props have not changed.
-
-### React.memo
+### memo
-> `React.memo` is a higher-order component that **skips re-rendering**
+> `memo` is a higher-order component that **skips re-rendering**
> when props are shallowly equal to the previous render.
-```jsx
-const Button = React.memo(({ onClick, label }) => {
+```tsx
+import { memo } from 'react'
+
+const Button = memo(({ onClick, label }: { onClick: () => void; label: string }) => {
console.log('Button rendered')
return
})
@@ -105,14 +143,14 @@ const Button = React.memo(({ onClick, label }) => {
-### How React.memo works
+### How memo works
-```jsx
+```tsx
// Simplified
-function memo(Component) {
- let prevProps = null
- let prevResult = null
- return function Memoized(props) {
+function memo
>(Component: (props: P) => JSX.Element) {
+ let prevProps: P | null = null
+ let prevResult: JSX.Element | null = null
+ return function Memoized(props: P) {
if (prevProps !== null && shallowEqual(props, prevProps)) {
return prevResult // ← skip re-render
}
@@ -129,7 +167,7 @@ It stores the previous props and result, and returns the cached result when prop
### The referential equality trap
-```jsx
+```tsx
function Parent() {
const handleClick = () => console.log('clicked')
// ↑ new object on every render of Parent
@@ -138,24 +176,24 @@ function Parent() {
}
```
-`React.memo` compares `onClick` using `===`.
+`memo` compares `onClick` using `===`.
`handleClick` is a new object every time → comparison is always `false`.
-> **`Button` re-renders on every parent render, regardless of `React.memo`.**
+> **`Button` re-renders on every parent render, regardless of `memo`.**
### The root cause
-```jsx
+```tsx
const prev = () => console.log('clicked')
const next = () => console.log('clicked')
prev === next // false — different objects, same body
```
-`React.memo`'s shallow comparison cannot tell that two functions
+`memo`'s shallow comparison cannot tell that two functions
with identical bodies are "the same".
> We need to **preserve the reference** across renders.
@@ -171,7 +209,7 @@ with identical bodies are "the same".
> `useCallback` returns a **memoized function reference**.
> It only creates a new function when one of its dependencies changes.
-```jsx
+```tsx
const handleClick = useCallback(() => {
console.log('clicked')
}, []) // empty array: stable reference for the component's lifetime
@@ -183,25 +221,28 @@ const handleClick = useCallback(() => {
### How useCallback works
-```jsx
+```ts
// Simplified
-function useCallback(fn, deps) {
- const ref = useRef(null)
- if (ref.current === null || !depsEqual(ref.current.deps, deps)) {
- ref.current = { fn, deps }
+let cached: { fn: T; deps: unknown[] } | null = null
+
+function useCallback(fn: T, deps: unknown[]): T {
+ if (cached === null || !depsEqual(cached.deps, deps)) {
+ cached = { fn, deps }
}
- return ref.current.fn
+ return cached.fn
}
```
On each render, it compares `deps` against the previous call.
If they are equal, it returns the **same function reference** as before.
+> React stores `cached` on the component's fiber node internally.
+
### Fixing the referential equality trap
-```jsx
+```tsx
function Parent() {
const handleClick = useCallback(() => {
console.log('clicked')
@@ -213,7 +254,7 @@ function Parent() {
`handleClick` is now the **same reference** across renders.
-`React.memo` sees `onClick` as unchanged → `Button` skips re-rendering.
+`memo` sees `onClick` as unchanged → `Button` skips re-rendering.
@@ -222,10 +263,13 @@ function Parent() {
> `useMemo` returns a **memoized value**.
> It only recomputes when one of its dependencies changes.
-```jsx
-const sortedList = useMemo(() => {
- return [...items].sort((a, b) => a.name.localeCompare(b.name))
-}, [items])
+```tsx
+interface Item { name: string }
+
+const sortedList = useMemo(
+ () => [...items].sort((a: Item, b: Item) => a.name.localeCompare(b.name)),
+ [items]
+)
```
Use this for expensive computations whose result depends on specific values.
@@ -236,27 +280,30 @@ Use this for expensive computations whose result depends on specific values.
### How useMemo works
-```jsx
+```ts
// Simplified
-function useMemo(factory, deps) {
- const ref = useRef(null)
- if (ref.current === null || !depsEqual(ref.current.deps, deps)) {
- ref.current = { value: factory(), deps }
+let cached: { value: T; deps: unknown[] } | null = null
+
+function useMemo(factory: () => T, deps: unknown[]): T {
+ if (cached === null || !depsEqual(cached.deps, deps)) {
+ cached = { value: factory(), deps }
}
- return ref.current.value
+ return cached.value
}
```
Same mechanism as `useCallback` — stores the last result and deps,
recomputes only when deps change.
+> Same caveat: `cached` lives on the fiber node internally.
+
### useCallback is useMemo
`useCallback(fn, deps)` is equivalent to `useMemo(() => fn, deps)`.
-```jsx
+```tsx
const handleClick = useCallback(() => doSomething(), [])
// is the same as:
@@ -273,18 +320,18 @@ const handleClick = useMemo(() => () => doSomething(), [])
When grouping related handlers (e.g., a context value), memoize the whole object once:
-```jsx
+```tsx
// Instead of this:
-const onCreate = useCallback(() => { /* ... */ }, [])
-const onUpdate = useCallback(() => { /* ... */ }, [])
-const onDelete = useCallback(() => { /* ... */ }, [])
+const onCreate = useCallback(() => { /* ... */ }, [dep1])
+const onUpdate = useCallback(() => { /* ... */ }, [dep1, dep2])
+const onDelete = useCallback(() => { /* ... */ }, [dep1])
// Prefer this:
const handlers = useMemo(() => ({
onCreate: () => { /* ... */ },
onUpdate: () => { /* ... */ },
onDelete: () => { /* ... */ },
-}), [])
+}), [dep1, dep2])
```
One allocation. One dependency check. One stable reference.
@@ -297,25 +344,116 @@ One allocation. One dependency check. One stable reference.
-### When to use
+### Stable callback for a memo-wrapped child
-- **`useCallback`** — when passing a callback to a `React.memo`-wrapped child, or when a function is a dependency in a `useEffect`
-- **`useMemo`** — when a computation is genuinely expensive and its inputs change rarely
+```tsx
+const MemoChild = memo(({ onClick }: { onClick: () => void }) => {
+ console.log('MemoChild rendered')
+ return
+})
-```jsx
-const filtered = useMemo(
- () => largeList.filter(item => item.active),
- [largeList]
-)
+function Parent() {
+ const handleClick = useCallback(() => {
+ console.log('clicked')
+ }, [])
+
+ return
+}
+```
+
+Without `useCallback`, `MemoChild` re-renders every time `Parent` does — defeating `memo`.
+
+
+
+### Callback as a useEffect dependency
+
+```tsx
+function SearchResults({ query }: { query: string }) {
+ const fetchResults = useCallback(async () => {
+ const res = await fetch(`/api/search?q=${query}`)
+ return res.json()
+ }, [query])
+
+ useEffect(() => {
+ fetchResults().then(setResults)
+ }, [fetchResults]) // ← stable ref prevents infinite loops
+}
+```
+
+If `fetchResults` were recreated on every render, `useEffect` would fire endlessly.
+
+
+
+### Expensive computation with useMemo
+
+```tsx
+interface Row { category: string; amount: number }
+
+function Report({ rows }: { rows: Row[] }) {
+ const summary = useMemo(() => {
+ // Imagine thousands of rows — genuinely expensive
+ return rows.reduce((acc, row) => {
+ acc[row.category] = (acc[row.category] ?? 0) + row.amount
+ return acc
+ }, {} as Record)
+ }, [rows])
+
+ return
+}
+```
+
+`rows` changes rarely (e.g. on fetch), so the reduction is cached across most renders.
+
+
+
+### When not to: child is not memo-wrapped
+
+```tsx
+function Child({ onClick }: { onClick: () => void }) {
+ console.log('Child rendered') // ← logs on every Parent render
+ return
+}
+
+function Parent() {
+ const handleClick = useCallback(() => console.log('clicked'), [])
+ return
+}
+```
+
+`Child` is **not** wrapped in `memo`, so it re-renders regardless. The `useCallback` is pure overhead.
+
+
+
+### When not to: trivial computation
+
+```tsx
+// ❌ useMemo overhead exceeds the savings
+const count = useMemo(() => items.length, [items])
+const label = useMemo(() => `${count} items`, [count])
+
+// ✅ Just compute it
+const count = items.length
+const label = `${count} items`
```
+Property access and string interpolation are essentially free — memoizing them costs more than recomputing.
+
-### When not to use
+### When not to: no measured problem
-- The child is **not wrapped** in `React.memo` — the re-render happens anyway
-- The computation is **trivial** — memoization overhead exceeds the savings
-- You haven't **measured** a real performance problem
+```tsx
+// ❌ Memoizing everything "just in case"
+function Dashboard({ user }: { user: User }) {
+ const greeting = useMemo(() => `Hello, ${user.name}`, [user.name])
+ const initials = useMemo(() => user.name.slice(0, 2).toUpperCase(), [user.name])
+ const handleLogout = useCallback(() => logout(user.id), [user.id])
+
+ return
+}
+```
+
+Three hooks, three dependency comparisons, three cached values — all for operations that take microseconds. Profile first with the React DevTools Profiler.
> Premature memoization is premature optimisation.
@@ -347,9 +485,9 @@ before reaching for `useCallback` or `useMemo`.
-### useCallback without React.memo
+### useCallback without memo
-```jsx
+```tsx
function Parent() {
const handleClick = useCallback(() => console.log('clicked'), [])
// ↑ overhead on every render of Parent
@@ -360,14 +498,14 @@ function Parent() {
`Child` re-renders on every `Parent` render regardless.
-> `useCallback` only helps when the receiving component is wrapped in `React.memo`.
+> `useCallback` only helps when the receiving component is wrapped in `memo`.
### Dependencies that always change
-```jsx
-function Component({ items }) {
+```tsx
+function Component({ items }: { items: Item[] }) {
const filtered = useMemo(
() => items.filter(item => item.active),
[{ items }] // ← new object on every render
@@ -383,7 +521,7 @@ The dependency array is compared with `===`. An inline object is always a new re
### Trivial computations
-```jsx
+```tsx
const count = useMemo(() => items.length, [items])
```
@@ -395,7 +533,7 @@ The comparison of `items` between renders costs more than computing `.length` di
### Cascading memoization
-```jsx
+```tsx
// ❌ Three layers of memoization for a single pipeline
const a = useMemo(() => compute(x), [x])
const b = useMemo(() => transform(a), [a])
@@ -404,7 +542,7 @@ const c = useMemo(() => format(b), [b])
Every layer adds allocation and comparison overhead. If `x` changes frequently, all three recompute anyway.
-```jsx
+```tsx
// ✅ One memoization for the whole pipeline
const c = useMemo(() => format(transform(compute(x))), [x])
```
@@ -423,9 +561,16 @@ const c = useMemo(() => format(transform(compute(x))), [x])
### A component with a big function inside
-```jsx
-function ProductCard({ product }) {
- const getDisplayInfo = (product) => {
+```tsx
+interface Product {
+ name: string
+ price: number
+ discount?: number
+ stock: number
+}
+
+function ProductCard({ product }: { product: Product }) {
+ const getDisplayInfo = (product: Product) => {
const name = product.name.trim().toUpperCase()
const price = product.discount
? product.price * (1 - product.discount / 100)
@@ -470,17 +615,17 @@ parses its body, and creates a new closure.
`getDisplayInfo` does four things. Give each its own function:
-```js
-const formatName = (name) =>
+```ts
+const formatName = (name: string): string =>
name.trim().toUpperCase()
-const applyDiscount = (price, discount) =>
+const applyDiscount = (price: number, discount?: number): number =>
discount ? price * (1 - discount / 100) : price
-const formatPrice = (price) =>
+const formatPrice = (price: number): string =>
`$${price.toFixed(2)}`
-const getStockBadge = (stock) =>
+const getStockBadge = (stock: number): string | null =>
stock === 0 ? 'Out of stock' : stock < 5 ? 'Low stock' : null
```
@@ -490,13 +635,13 @@ Each function does one thing. Each is independently testable.
### Hoist them out of the component
-```jsx
-const formatName = (name) => name.trim().toUpperCase()
-const applyDiscount = (price, discount) => discount ? price * (1 - discount / 100) : price
-const formatPrice = (price) => `$${price.toFixed(2)}`
-const getStockBadge = (stock) => stock === 0 ? 'Out of stock' : stock < 5 ? 'Low stock' : null
+```tsx
+const formatName = (name: string) => name.trim().toUpperCase()
+const applyDiscount = (price: number, discount?: number) => discount ? price * (1 - discount / 100) : price
+const formatPrice = (price: number) => `$${price.toFixed(2)}`
+const getStockBadge = (stock: number) => stock === 0 ? 'Out of stock' : stock < 5 ? 'Low stock' : null
-function ProductCard({ product }) {
+function ProductCard({ product }: { product: Product }) {
const name = formatName(product.name)
const price = formatPrice(applyDiscount(product.price, product.discount))
const badge = getStockBadge(product.stock)
@@ -516,57 +661,6 @@ These 4 functions are created **once**, no matter how many `ProductCard`s render
-### Currying for event handlers
-
-Form field handlers close over `profile` and `onChange` — they can't simply be hoisted:
-
-```jsx
-// ❌ One function body per field, all recreated on every render
-function ProfileForm({ profile, onChange }) {
- const handleName = (e) => onChange({ ...profile, name: e.target.value })
- const handleEmail = (e) => onChange({ ...profile, email: e.target.value })
- const handlePhone = (e) => onChange({ ...profile, phone: e.target.value })
-
- return (
-
- )
-}
-```
-
-Three function bodies. Add a field → add another.
-
-
-
-### Currying for event handlers — solution
-
-```jsx
-// ✅ Curried: onChange → profile → field → event
-const makeFieldHandler = (onChange) => (profile) => (field) => (e) =>
- onChange({ ...profile, [field]: e.target.value })
-
-function ProfileForm({ profile, onChange }) {
- const handleField = makeFieldHandler(onChange)(profile)
- // one partial application — no body
-
- return (
-
- )
-}
-```
-
-`makeFieldHandler` is defined once. The component contains **no function bodies**.
-Add a field → add one line in JSX, nothing else.
-
-
-
### The rule
> If it's pure, hoist it.
@@ -574,3 +668,7 @@ Add a field → add one line in JSX, nothing else.
> Reach for `useCallback` only when you've exhausted these options.
Hoisting is free — no hooks overhead, no dependency arrays, no re-creation.
+
+
+
+## THANK YOU