# How Fre's `memo()` HOC Optimizes Component Rendering: A Deep Dive into the Source Code

> Discover how Fre's memo() HOC optimizes rendering by preventing re-renders when props don't change. Explore shallow comparison and custom equality functions.

- Repository: [frejs/fre](https://github.com/frejs/fre)
- Tags: deep-dive
- Published: 2026-03-02

---

**Fre's `memo()` higher-order component prevents unnecessary re-renders by marking functional components with a `memo` flag and using a shallow comparison or custom equality function to skip rendering when props remain unchanged.**

Fre is a lightweight React alternative that implements a fiber-based reconciler. Understanding how its `memo()` higher-order component (HOC) optimizes rendering requires examining the interplay between the component factory in [`src/h.ts`](https://github.com/frejs/fre/blob/main/src/h.ts) and the reconciliation logic in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts).

## What Is Fre's `memo()` HOC?

The `memo()` function is a tiny higher-order component factory located in [`src/h.ts`](https://github.com/frejs/fre/blob/main/src/h.ts) (lines 51-55). It takes a functional component and an optional comparison function, then attaches metadata to the function object that the reconciler checks during the render phase.

Unlike React's implementation, Fre's version is intentionally minimal, adding only two properties to the component function:

- `fn.memo = true` — signals that this component may be skipped
- `fn.shouldUpdate` — optionally stores a custom props comparison function

## How the Reconciler Detects Memoized Components

### The `capture()` Routine

During each reconciliation cycle, Fre's `capture` function in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts) traverses the fiber tree. When it encounters a fiber representing a functional component, it checks whether that component has been marked with `memo`.

The critical logic appears at lines 90-93:

```typescript
if (isMemo(fiber)) {
  fiber.memo = false
  return sibling(fiber)
}

```

If `isMemo(fiber)` returns `true`, the reconciler immediately returns the next sibling fiber instead of recursing into the component's children. This means the component's render function is **not invoked**, its child fibers are not diffed, and no DOM work is scheduled for that subtree.

### The `isMemo()` Logic

The `isMemo` function, defined at lines 104-115 in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts), performs three validation steps before determining whether to skip rendering:

1. **Component type check** — verifies the current and previous fibers refer to the same component type
2. **Props existence check** — confirms a previous props object exists for comparison
3. **Equality comparison** — executes either the user-provided `shouldUpdate` function or Fre's default shallow comparison

The default shallow comparison lives at lines 41-47 in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts):

```typescript
const shouldUpdate = (a, b) => {
  for (let i in a) if (a[i] !== b[i]) return true
  for (let i in b) if (b[i] !== a[i]) return true
  return false
}

```

If `shouldUpdate` returns `false` (indicating props are equal), `isMemo` returns `true`, triggering the skip logic in `capture`.

## Practical Implementation Examples

### Using `memo()` with Custom Comparison

The following example from the Fre demo repository demonstrates using a custom equality function to control when re-renders occur:

```tsx
import { h, render, useState, memo } from '../../src'

function App() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <Counter count={count} />
      <button onClick={() => setCount(count + 1)}>Inc</button>
    </div>
  )
}

// Re-render only when `count` becomes even
const Counter = memo(
  ({ count }: { count: number }) => <div>{count}</div>,
  (prev, next) => next.count % 2 === 0
)

render(<App />, document.body)

```

In this implementation, the `Counter` component skips rendering on every odd update because the custom `shouldUpdate` returns `false` for those cases.

### Verifying Optimization in Tests

The Fre test suite confirms that memoized components avoid unnecessary render calls:

```tsx
// test/memo.tsx
import { h, memo } from '../src/index'

export const memor = async t => {
  const Component = memo(() => {
    console.log('rendered')
    return <div />
  })
  // First render
  t.is(await render(<Component />, document.body), undefined)
  // Subsequent render with same props – should not log again
}

```

This test verifies that when props remain unchanged, the component function is not invoked again, confirming the optimization is working.

## Summary

- **Fre's `memo()`** marks functional components by setting `fn.memo = true` and optionally attaching a custom `shouldUpdate` comparison function in [`src/h.ts`](https://github.com/frejs/fre/blob/main/src/h.ts).
- **The reconciler** checks for memoization during the `capture` phase in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts), calling `isMemo()` to determine if props have changed.
- **Shallow comparison** is the default equality check, though users can provide custom logic to control when updates occur.
- **Subtree skipping** happens immediately when `isMemo` returns true, preventing the render function from executing and avoiding all child reconciliation and DOM operations for that component.

## Frequently Asked Questions

### What is the difference between Fre `memo()` and React `memo()`?

Both implementations prevent re-renders when props remain unchanged, but Fre's version is significantly more lightweight. While React's implementation involves complex internal state management and additional development-mode checks, Fre simply attaches two properties (`memo` and `shouldUpdate`) to the function object and relies on the reconciler's `isMemo` check during the `capture` phase to skip rendering.

### How does Fre's default `shouldUpdate` function work?

The default comparison function performs a shallow equality check on props objects. Located at lines 41-47 in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts), it iterates through all keys in both the previous and current props objects, returning `true` (indicating an update is needed) if any values differ. If all values match, it returns `false`, triggering the memoization skip.

### Can I use `memo()` with Fre hooks like `useState` or `useEffect`?

Yes, `memo()` works seamlessly with Fre's hook system. The HOC only controls whether the component function re-executes based on props changes; internal state updates from `useState` or effects from `useEffect` still function normally when the component does render. However, if a parent re-renders but the memoized component's props haven't changed, the component won't execute at all, meaning its internal hooks won't run during that specific cycle.

### Where does Fre check if a component should update during reconciliation?

The check occurs in the `capture` function within [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts) at lines 90-93. During the reconciliation traversal, when the reconciler encounters a functional component fiber, it calls `isMemo(fiber)` (defined at lines 104-115). This function verifies the component is marked with `memo`, compares props using either the custom or default `shouldUpdate` function, and returns a boolean that determines whether the reconciler should skip the entire subtree.