Fre's useMemo and useCallback Hooks: A Deep Dive into Memoization

Fre provides useMemo and useCallback hooks that cache expensive computations and stable function references across renders by storing values in a fiber's hook list and comparing dependencies with shallow equality checks.

Fre is a lightweight UI library that implements a React-like hooks system for functional components. Its memoization hooks—useMemo and useCallback—allow developers to optimize performance by avoiding unnecessary recalculations and preserving function identity. These hooks are implemented in src/hook.ts and rely on Fre's fiber-based architecture to maintain state between renders.

How Fre's Memoization System Works

Hook Storage in the Fiber Architecture

Each component render in Fre is represented by a Fiber, which maintains a hooks.list array to store hook state. The helper function getSlot<T>(cursor) in src/hook.ts retrieves the current hook's storage slot and returns both the stored value and the fiber instance. At the start of every render, resetCursor initializes the cursor to zero, ensuring hooks execute in a deterministic order as the cursor increments with each call.

The HookMemo Type and Dependency Detection

The core storage structure for memoized values is defined in src/type.ts as type HookMemo<V = any> = [value: V, deps: DependencyList]. Each slot contains the cached value and its deps array. To detect changes, Fre uses the isChanged(a, b) utility, which performs a shallow comparison using Object.is on dependency arrays【src/type.ts†L42-L48】. If any dependency differs, the hook invalidates the cache and recomputes the value.

useMemo Implementation Details

The useMemo(cb, deps?) function, located in src/hook.ts at lines 74-84, retrieves its storage slot and checks isChanged against previous dependencies【src/hook.ts†L74-L84】. When dependencies have changed—or when deps is omitted, causing recomputation on every render—the callback executes to produce a fresh value. Otherwise, Fre returns the cached result from the fiber's hook list, skipping expensive calculations.

useCallback Implementation Details

useCallback(cb, deps?) is implemented in src/hook.ts at lines 86-90 as a thin wrapper around useMemo【src/hook.ts†L86-L90】. Instead of caching a computed value, it caches the callback function itself, returning a stable reference until dependencies change. This prevents unnecessary function recreation, which is critical when passing callbacks to child components or dependencies to useEffect hooks that rely on reference equality.

Integration with the Render Cycle

When a memoization hook detects changed dependencies and updates its cached value, Fre marks the component's fiber as dirty. This triggers update(current) from src/reconcile.ts to schedule a re-render, ensuring the component receives the new memoized value or callback.

Practical Code Examples

Caching Expensive Calculations with useMemo

import { h, useMemo } from 'fre'

function DataProcessor({ dataset }) {
  const processed = useMemo(() => {
    return dataset.filter(item => item.active).sort((a, b) => a.score - b.score)
  }, [dataset])

  return <div>{processed.length} active items</div>
}

In this example, useMemo stores the filtered and sorted array in a HookMemo slot within the fiber. The computation only reruns when the dataset reference changes, avoiding O(n log n) operations on every render.

Stabilizing Function References with useCallback

import { h, useCallback, useEffect } from 'fre'

function SearchInput({ onSearch }) {
  const handleKeyUp = useCallback((e) => {
    if (e.key === 'Enter') onSearch(e.target.value)
  }, [onSearch])

  useEffect(() => {
    document.addEventListener('keyup', handleKeyUp)
    return () => document.removeEventListener('keyup', handleKeyUp)
  }, [handleKeyUp])

  return <input type="search" />
}

Here, useCallback ensures handleKeyUp maintains the same reference between renders unless onSearch changes. This prevents the useEffect cleanup and re-subscription cycle from executing on every render, optimizing event listener management.

Combining useMemo and useCallback

import { h, useMemo, useCallback } from 'fre'

function ItemList({ items, sortFn }) {
  const sortedItems = useMemo(() => [...items].sort(sortFn), [items, sortFn])
  
  const handleItemClick = useCallback((id) => {
    console.log('Clicked item:', id)
  }, [])

  return (
    <ul>
      {sortedItems.map(item => (
        <li key={item.id} onClick={() => handleItemClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  )
}

This pattern demonstrates optimal performance: sortedItems recalculates only when data or sorting logic changes, while handleItemClick remains a stable reference due to its empty dependency array, preventing unnecessary child component updates.

Summary

  • Fiber-based storage: Fre stores memoized values in a hooks.list array attached to each component's fiber, accessed via getSlot in src/hook.ts.
  • Shallow dependency checking: The isChanged utility in src/type.ts uses Object.is comparison to determine when caches invalidate.
  • useMemo: Caches computed values from expensive calculations, recomputing only when dependencies change or when omitted to force updates every render.
  • useCallback: A specialized useMemo wrapper that caches function references to maintain stable identities across renders.
  • Render integration: Updated memoized values trigger update(current) in src/reconcile.ts to schedule necessary re-renders.

Frequently Asked Questions

How does Fre's useMemo differ from React's implementation?

Fre's useMemo follows the same semantic API as React but implements storage through its fiber architecture rather than React's internal linked list. Both use shallow dependency comparison, but Fre stores values in src/hook.ts using the HookMemo tuple type defined in src/type.ts, making the implementation more compact for the library's lightweight footprint.

What happens if I omit the dependency array in Fre's useMemo or useCallback?

When deps is omitted or set to undefined, the hook treats this as a signal to recompute on every render. In src/hook.ts, the implementation checks isChanged against the previous dependencies; if the new deps argument is undefined, the comparison fails, causing the callback to execute fresh each time—identical to React's behavior without dependencies.

Can useCallback prevent child components from re-rendering?

Yes. useCallback returns a stable function reference from the fiber's hook storage until dependencies change. When passing this stable reference to child components, Fre's reconciliation process in src/reconcile.ts detects unchanged props (via reference equality), potentially skipping unnecessary renders of optimized child components.

Where are the memoization hooks defined in the Fre source code?

The primary implementations reside in src/hook.ts, specifically lines 74-90 for useMemo and useCallback. Type definitions including HookMemo and isChanged exist in src/type.ts at lines 35-48, while the public API exports these functions from src/index.ts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →