# How the Visual Editor Routes Mutations Between Page and Component Trees in Instatic

> Discover how the Instatic visual editor routes mutations between page and component trees using the mutateActiveTree helper for efficient updates.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-08-01

---

**The visual editor uses a single `mutateActiveTree` helper that delegates to `resolveActiveTreeTarget` to determine whether to mutate the active page tree or visual-component tree, centralizing all routing logic in one location.**

All tree mutations in the Instatic visual editor flow through a unified pipeline. Whether you're editing a page or a visual component (VC), the same mutation primitives apply—only the target tree changes. This design eliminates duplicate code paths and ensures consistent undo/redo behavior across both document types.

## The Core Routing Mechanism: `resolveActiveTreeTarget`

The routing decision happens in **`resolveActiveTreeTarget`** in [`src/admin/pages/site/store/slices/site/helpers.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/store/slices/site/helpers.ts). This function is the **only location** in the codebase that checks `activeDocument.kind === 'visualComponent'`, a constraint enforced by the architecture test [`no-vc-mode-branches-in-mutations.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/no-vc-mode-branches-in-mutations.test.ts).

```typescript
// src/admin/pages/site/store/slices/site/helpers.ts
export function resolveActiveTreeTarget(state) {
  const { site, activeDocument } = state;
  if (activeDocument?.kind === 'visualComponent') {
    const vc = site.visualComponents.find(v => v.id === activeDocument.vcId);
    return { tree: vc!.tree as NodeTree<PageNode>, vc };
  }
  const pageId = activeDocument?.kind === 'page' ? activeDocument.pageId : state.activePageId;
  const page = site.pages.find(p => p.id === pageId);
  return page ? { tree: page, vc: null } : null;
}

```

The function returns both the target tree and, for VCs, the component object itself. For pages, `vc` is null. This simple branch handles all routing between page and component trees.

## Mutation Execution Pipeline

Once the target is resolved, **`mutateActiveTree(fn)`** orchestrates the actual mutation:

```typescript
// src/admin/pages/site/store/slices/site/helpers.ts
function mutateActiveTree(fn, opts) {
  return runHistoricMutation(
    draft => runActiveTreeRecipe(draft, fn),
    opts?.coalesceKey ?? null,
  );
}

```

The pipeline works in three stages:

- **Tree resolution** – `runActiveTreeRecipe` calls `resolveActiveTreeTarget` to get the active tree
- **Mutation execution** – The caller-provided `fn` runs against the resolved tree
- **VC slot synchronization** – In VC mode, if slot-outlet names changed, `syncAllVCRefSlotInstances` propagates updates across all referencing pages

## Store Actions: Thin Wrappers Around the Router

The 11 named mutation actions never inspect `activeDocument.kind`. Each delegates directly to `mutateActiveTree` in [`src/admin/pages/site/store/slices/site/nodeActions.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/store/slices/site/nodeActions.ts):

```typescript
// src/admin/pages/site/store/slices/site/nodeActions.ts
export const insertNode = (node, parentId, index) => {
  const { mutateActiveTree } = helpers;
  mutateActiveTree(tree => insertNode(tree, node, parentId, index));
};

```

Other actions like `deleteNode`, `moveNode`, `duplicateNode`, and `updateNodeProps` follow the same pattern. The routing transparency means new mutation types require no additional branching logic.

## Undo/Redo and Historic Mutations

The **`runHistoricMutation`** wrapper in [`helpers.ts`](https://github.com/CoreBunch/Instatic/blob/main/helpers.ts) captures Mutative patches and applies only *site-scoped* patches to the undo history. This guarantees that a mutation step on either tree type is recorded exactly once, maintaining a consistent undo stack regardless of which document type is active.

## Practical Code Examples

### Insert a node in the active tree

```typescript
import { useSiteHelpers } from '@site/store/hooks';

function addParagraph() {
  const { mutateActiveTree } = useSiteHelpers();

  mutateActiveTree(tree => {
    const newNode = createNode({ type: 'paragraph', props: { text: 'Hello' } });
    insertNode(tree, newNode, tree.rootNodeId, 0);
  });
}

```

### Move a node with automatic VC slot propagation

```typescript
mutateActiveTree(tree => {
  moveNode(tree, nodeId, newParentId, 2);
});

```

When the active document is a VC, `runActiveTreeRecipe` detects slot-outlet reordering and invokes `syncAllVCRefSlotInstances` so every referencing page receives the updated outlet order.

### Combined tree and site mutation

For operations needing both tree and site-level changes, use **`mutateActiveTreeAndSite`**:

```typescript
mutateActiveTreeAndSite((tree, site) => {
  const dupResult = duplicateNode(tree, nodeId);
  if (dupResult) {
    addStyleRule(site, dupResult.newClassRule);
  }
  return true;
});

```

## Key Implementation Files

| File | Purpose |
|------|---------|
| [`src/admin/pages/site/store/slices/site/helpers.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/store/slices/site/helpers.ts) | Core routing: `resolveActiveTreeTarget`, `mutateActiveTree`, historic mutation engine |
| [`src/admin/pages/site/store/slices/site/nodeActions.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/site/store/slices/site/nodeActions.ts) | 11 thin action wrappers delegating to `mutateActiveTree` |
| [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts) | Tree-agnostic primitives: `insertNode`, `moveNode`, `deleteNode`, etc. |
| [`src/core/visualComponents/slotSync.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/slotSync.ts) | Slot-instance synchronization for VC trees |
| [`docs/reference/page-tree.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/reference/page-tree.md) | Architectural documentation of mutation routing |
| [`docs/editor.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/editor.md) | Editor store overview and `mutateActiveTree` usage |

## Summary

- **Single routing point**: `resolveActiveTreeTarget` in [`helpers.ts`](https://github.com/CoreBunch/Instatic/blob/main/helpers.ts) is the sole location that distinguishes page from VC trees
- **Unified mutation API**: `mutateActiveTree` handles both tree types transparently
- **No action-level branching**: All 11 store actions delegate to the same helper without inspecting document kind
- **Automatic VC propagation**: Slot-outlet changes in VC mode trigger site-wide synchronization
- **Consistent history**: `runHistoricMutation` ensures undo/redo works identically for pages and components

## Frequently Asked Questions

### What happens if `activeDocument` is null?

`resolveActiveTreeTarget` falls back to `state.activePageId` and resolves the corresponding page tree. This default behavior ensures the editor always has a valid mutation target.

### Why is there an architecture test for VC mode branches?

The test [`no-vc-mode-branches-in-mutations.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/no-vc-mode-branches-in-mutations.test.ts) enforces that `resolveActiveTreeTarget` contains the only `kind === 'visualComponent'` check. This prevents mutation logic from scattering across multiple files, maintaining the centralized routing design.

### How does slot synchronization work in practice?

When a VC mutation reorders slot-outlet definitions, `runActiveTreeRecipe` compares outlet names before and after the mutation. If they differ, it calls `syncAllVCRefSlotInstances` from [`src/core/visualComponents/slotSync.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/visualComponents/slotSync.ts) to update every page that references the VC, ensuring outlet order consistency across the site.