How Instatic Visual Components Handle Typed Parameters and Slot Synchronization

Instatic Visual Components use TypeBox schemas for compile-time and runtime parameter validation, coupled with a deterministic slot-sync algorithm that keeps slot instances synchronized with outlet definitions.

In the CoreBunch/Instatic codebase, Visual Components (VCs) provide a reusable abstraction for building dynamic interfaces. Understanding how Instatic Visual Components handle typed parameters and slot synchronization is essential for developers extending the editor or creating custom component libraries. The system ensures end-to-end type safety through JSON Schema definitions while maintaining tree consistency via a pure, side-effect-free synchronization routine.

Typed Parameter Handling with TypeBox Schemas

Schema Definition in schemas.ts

Every Visual Component ships with a TypeBox schema that describes its parameters, types, defaults, and validation rules. In src/core/visualComponents/schemas.ts, the VisualComponent type defines this contract. The schema generates a TypeScript type via Static<typeof ParamsSchema>, which is used throughout the editor, server, and publishing pipeline.

The result is a strongly typed plain object that satisfies the generated VisualComponentParams type. This guarantees that all callers—from the canvas to the publishing worker—agree on the shape of the data.

Runtime Validation with propGuards.ts

When a VC is instantiated (for example, when dragged onto the canvas), the engine validates supplied parameter values against the schema. The helper functions in src/core/visualComponents/propGuards.ts enforce these constraints at runtime, ensuring that only valid data enters the node tree. This validation step acts as a guard between the UI layer and the internal state, preventing malformed props from propagating to the server or static generation pipeline.

Slot Outlet Architecture and Synchronization Logic

Defining Slots with base.slot-outlet Nodes

A VC exposes slots by placing a base.slot-outlet node inside its internal tree. The slot-outlet itself serves as the slot definition—there is no separate parameter list for slots. When the outlet omits a name, it defaults to "children".

When a VC reference (base.visual-component-ref) appears on a page, it must maintain exactly one base.slot-instance child for each slot-outlet declared by the referenced VC. This invariant is enforced by the slot-sync module to ensure the tree structure always mirrors the component definition.

The Slot Synchronization Algorithm

The synchronization logic lives in src/core/visualComponents/slotSync.ts and operates as a pure, idempotent function:

  1. Collect slot names – collectSlotOutletNames walks the VC’s internal tree and returns the ordered list of slot-outlet names.
  2. Compute diff – syncSlotInstances receives the VC-ref node, the VC schema, and the current node map. It produces a SyncResult describing inserts, renames, and deletions needed to align the ref’s children with the VC’s slots.
  3. Apply changes – applySlotSyncResult mutates the mutable node map (within a Mutative recipe) by adding new base.slot-instance nodes, renaming existing ones, deleting stray children, and re-ordering the children array to match the VC’s slot order.

Running the algorithm on an already-synced ref yields an empty operation list, making it safe to invoke repeatedly during reactive updates.

Integration: Instantiation and Store Updates

The end-to-end flow connects schema validation with slot synchronization through two primary integration points.

In src/core/visualComponents/instantiate.ts, the instantiateVisualComponent function creates a VC reference node, validates supplied parameters against the VC’s schema, and immediately runs slot-sync so the new reference starts with a proper set of slot-instance children.

At the state management layer, src/admin/pages/site/store/slices/visualComponentsSlice.ts invokes syncSlotInstances whenever a VC reference is added, moved, or when the underlying VC definition changes. This guarantees that the page tree remains consistent with the component API surface, which is crucial for correct publishing and for plugin-level mutation contracts.

/* 1️⃣ Define a visual component with typed parameters and a slot outlet */
import { Type, Static } from '@sinclair/typebox'

export const MyCardSchema = Type.Object({
  title: Type.String({ description: 'Card title' }),
  showImage: Type.Boolean({ default: true })
})

export type MyCardParams = Static<typeof MyCardSchema>

export const myCardVc = {
  moduleId: 'my.card',
  paramsSchema: MyCardSchema,
  // the internal tree includes a slot-outlet named “footer”
  tree: {
    rootNodeId: 'root',
    nodes: {
      root: { id: 'root', moduleId: 'base.container', props: {}, children: ['slot'] },
      slot: { id: 'slot', moduleId: 'base.slot-outlet', props: { slotName: 'footer' }, children: [] }
    }
  }
}
/* 2️⃣ Instantiate the component on a page */
import { instantiateVisualComponent } from '@core/visualComponents/instantiate'

const pageNodeMap = { /* mutable map of BaseNode */ }
const vcRefNode = instantiateVisualComponent({
  vc: myCardVc,
  params: { title: 'Welcome', showImage: false },
  treeNodes: pageNodeMap
})
// The function validates `params` against `MyCardSchema` and then runs
// `syncSlotInstances` so `vcRefNode.children` now contains a single
// `base.slot-instance` whose props.slotName === 'footer'.
/* 3️⃣ Updating the VC definition (e.g. adding a new slot) */
import { syncSlotInstances, applySlotSyncResult } from '@core/visualComponents/slotSync'

// Suppose the VC now declares a second slot‑outlet called “badge”.
const result = syncSlotInstances(vcRefNode, updatedVc, pageNodeMap)
applySlotSyncResult(pageNodeMap, result, vcRefNode.id)
// After applying, the ref has two slot‑instance children (footer & badge)
// in the correct order, and any stray child nodes have been removed.

Summary

Frequently Asked Questions

How does Instatic ensure type safety for Visual Component parameters across the entire stack?

Instatic leverages TypeBox schemas defined in src/core/visualComponents/schemas.ts to generate TypeScript types using Static<typeof ParamsSchema>. This type propagates through the editor, server, and publishing pipeline, while src/core/visualComponents/propGuards.ts performs runtime validation to catch mismatches at instantiation time.

What happens to existing page instances when a Visual Component adds a new slot outlet?

When a VC definition changes (for example, adding a "badge" slot), the syncSlotInstances function detects the new requirement by comparing the current base.slot-instance children against the updated outlet list. It then generates insertion operations, and applySlotSyncResult adds the missing base.slot-instance nodes to the VC reference, preserving existing content while aligning the tree structure.

Is the slot synchronization algorithm safe to run redundantly or during rapid state updates?

Yes. According to the implementation in src/core/visualComponents/slotSync.ts, the algorithm is pure (no side effects) and idempotent. Running it on an already-synchronized reference produces an empty operation list, making it safe to invoke repeatedly as users drag components or undo/redo mutations.

Where does parameter validation occur when dragging a component onto the canvas?

Validation happens inside instantiateVisualComponent in src/core/visualComponents/instantiate.ts. This function checks supplied parameters against the VC’s TypeBox schema before the node is inserted into pageNodeMap, ensuring only valid data enters the document tree.

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 →