# React 19 Canvas Optimization Plan

## Status: React 19.2.3 with Compiler Active — Phase 1 COMPLETE

The canvas package is running React 19.2.3 with the React Compiler active via `babel-plugin-react-compiler` + `@vitejs/plugin-react`. **Phase 1 (core gesture/input files) was completed in v3.0.0.** This plan documents remaining opportunities.

---

## Phase 1: Remove Redundant Manual Memoization — ✅ COMPLETED in v3.0.0

> **Completed files:** `useGestureSystem.ts` (4 useCallback + 1 useMemo), `GestureProvider.tsx` (1 useCallback + 1 useMemo), `useGestureResolver.ts` (1 useCallback), `keyboard.ts` (1 useMemo)
>
> **Remaining files** (not yet cleaned):

The React Compiler auto-memoizes all components and hooks. Manual `useMemo`/`useCallback` calls are now redundant — they add cognitive overhead and dep-array maintenance burden without benefit.

### 1a. `components/Minimap.tsx` — Remove 4 `useCallback` wrappers

**Before:**
```tsx
const getMinimapTransform = useCallback(() => { ... }, [graphBounds, width, height]);
const minimapToWorld = useCallback((mx, my) => { ... }, [getMinimapTransform]);
const panToWorld = useCallback((worldX, worldY) => { ... }, [zoom, viewportRect, setPan]);
```

**After:**
```tsx
const getMinimapTransform = () => { ... };
const minimapToWorld = (mx: number, my: number) => { ... };
const panToWorld = (worldX: number, worldY: number) => { ... };
```

The compiler tracks `graphBounds`, `width`, `height`, `zoom`, `viewportRect`, and `setPan` as dependencies automatically via its SSA analysis.

### 1b. `db/provider.tsx` — Remove `useMemo` wrapper

**Before:**
```tsx
const adapter = useMemo(() => {
  if ('adapter' in props) return props.adapter;
  // ... legacy Supabase path
}, ['adapter' in props ? props.adapter : ...]);
```

**After:**
```tsx
const adapter = (() => {
  if ('adapter' in props) return props.adapter;
  // ... legacy Supabase path
})();
```

Note: The dependency array here is unusual (contains a string expression). The compiler's approach is strictly better — it tracks the actual object identity of `props.adapter` rather than a stringified URL.

**Alternative (cleaner):** Extract to a local variable:
```tsx
const adapter = 'adapter' in props
  ? props.adapter
  : createLegacyAdapter(props.supabaseUrl, props.supabaseAnonKey, setLegacyClient);
```

### 1c. `hooks/useSplitGesture.ts` — Remove 3 `useCallback` wrappers

**Before:**
```tsx
const onPointerDown = useCallback((e: React.PointerEvent) => { ... }, []);
const onPointerMove = useCallback((e: React.PointerEvent) => { ... }, [nodeId, splitNode, screenToWorld]);
const onPointerUp = useCallback((e: React.PointerEvent) => { ... }, []);
```

**After:**
```tsx
const onPointerDown = (e: React.PointerEvent) => { ... };
const onPointerMove = (e: React.PointerEvent) => { ... };
const onPointerUp = (e: React.PointerEvent) => { ... };
```

### Verification

After each file change:
```bash
pnpm --filter @blinksgg/canvas build
pnpm --filter @blinksgg/canvas check-types
```

Confirm compiler output still has `$[` cache slots for these functions (the compiler will still memoize them, just without the manual wrapper).

---

## Phase 2: ~~Add `useTransition` for Layout Operations~~ SKIPPED

After analyzing `useAnimatedLayout.ts`, `useTransition` does **not** apply to these layout hooks:

- `startTransition` only captures **synchronous** state updates within its callback
- Layout state updates (`updateNodePosition`) happen **asynchronously** via `requestAnimationFrame` inside `animate()`
- The rAF-driven animation pattern already yields to the browser between frames
- Deprioritizing animation updates via `startTransition` would make animations janky

`useTransition` is designed for cases like "update a search filter while keeping input responsive" — not rAF-driven animations that already cooperate with the browser's frame budget.

---

## Phase 3: `use(Context)` for Conditional Context Reads (Optional, Low Priority)

### 3a. `commands/CommandProvider.tsx` — `useCommandContext`

Current:
```tsx
export function useCommandContext(): CommandContextValue {
  const context = useContext(CommandContextContext);
  if (!context) throw new Error('...');
  return context;
}
```

React 19:
```tsx
import { use } from 'react';

export function useCommandContext(): CommandContextValue {
  const context = use(CommandContextContext);
  if (!context) throw new Error('...');
  return context;
}
```

Minimal benefit here since these aren't conditional reads. Skip unless doing a broader cleanup pass.

---

## Phase 4: Ref Cleanup Callbacks (Selective, Medium Priority)

React 19 allows ref callbacks to return cleanup functions. `CanvasStyleProvider.tsx` already uses this pattern. Other candidates:

### 4a. `components/Viewport.tsx` (15 refs — audit for cleanup opportunities)

Look for patterns like:
```tsx
useEffect(() => {
  const el = someRef.current;
  if (!el) return;
  const observer = new ResizeObserver(...);
  observer.observe(el);
  return () => observer.disconnect();
}, []);
```

These can become:
```tsx
const refCallback = (node: HTMLElement | null) => {
  if (!node) return;
  const observer = new ResizeObserver(...);
  observer.observe(node);
  return () => observer.disconnect();
};
```

This is cleaner because the ref and its side effect are co-located, but only worth doing when the `useEffect` exists solely to set up/tear down something on the DOM element.

---

## Phase 5: Verify Compiler Correctness (Testing)

### 5a. Compare bundle sizes

```bash
# Before changes
pnpm --filter @blinksgg/canvas build
ls -la packages/canvas/dist/index.mjs  # note size

# After changes
pnpm --filter @blinksgg/canvas build
ls -la packages/canvas/dist/index.mjs  # compare
```

Removing `useMemo`/`useCallback` imports should slightly reduce the bundle since fewer React runtime calls are needed.

### 5b. Run existing tests

```bash
pnpm --filter @blinksgg/canvas test
```

### 5c. Manual testing

- Minimap: click/drag panning still works
- Split gesture: two-finger split on touch device
- Layout: tree/grid/force layouts complete without blocking UI
- DB provider: adapter initialization with both adapter prop and legacy Supabase props

---

## Not Recommended (and Why)

| Idea | Why Skip |
|------|----------|
| Add `React.memo()` wrapping | Compiler handles it better with fine-grained caching |
| Convert all `useContext` to `use()` | No conditional reads, marginal benefit |
| Add `Suspense` boundaries | Jotai atoms handle loading states; canvas is synchronous |
| Server Components | Canvas is inherently client-side (DOM, gestures, canvas 2D) |
| `useDeferredValue` for search | Search store is already fast with Jotai; defer if perf issue arises |

---

## Effort Estimate

| Phase | Files | Complexity | Priority |
|-------|-------|------------|----------|
| 1: Remove manual memo | 3 | Trivial | High |
| 2: Add useTransition | - | - | Skipped (rAF incompatible) |
| 3: use(Context) | 3 | Trivial | Low |
| 4: Ref cleanup callbacks | 1-2 | Medium | Medium |
| 5: Verification | 0 | Low | Required |
