Skip to Content

Portalling UIs

React provides createPortal that lets you render some children into a different part of the DOM. This combined with Cascaide’s WorkflowRenderer allows us to do some interesting things.

Check out the React createPortal docs  for a primer.

Let’s ground this in an example.

Set Up: A standard Chat interface with a ReAct agent running. This is the pattern used to render mini chat windows in the recursive ReAct agent in the starter projects. Check out the starter templates for full code.

The Problem

WorkflowRenderer renders all active UI nodes inside a single div. This works well for most layouts, but sometimes you need an agent’s UI to appear somewhere completely different in the page. For example, a sidebar, a fixed overlay, a panel that lives outside your main content area, etc.

The Solution: React Portals

React’s createPortal lets a component render its DOM output into any node in the document while remaining a full participant in the React tree. It keeps its Cascaide hooks, its access to workflow state, its ability to spawn nodes and signal completion works exactly as normal. Only the physical DOM placement changes.

Creating the UI Node

export default function Tracker({ nodeId }: { nodeId: string }) { const { nodeData } = useWorkflow(nodeId); const tasks: DelegationTask[] = nodeData?.initialContext?.history ?? []; if (tasks.length === 0) return null; const byDepth = tasks.reduce<Record<number, DelegationTask[]>>((acc, task) => { const d = getDepthFromCascadeId(task.subCascadeId); (acc[d] ??= []).push(task); return acc; }, {}); const target = document.getElementById('right-sidebar-slot-1'); if (!target) return null; return createPortal( <div className="p-3"> {Object.entries(byDepth) .sort(([a], [b]) => Number(a) - Number(b)) .map(([depth, depthTasks]) => ( <DepthCard key={depth} depth={Number(depth)} tasks={depthTasks} /> ))} </div>, target ); }

Register this uiNode in your clientWorkflowConfig and add portal root elements to your HTML layout wherever you need them.

import React from 'react'; const SLOTS = [1, 2, 3, 4] as const; export function RightSidebar() { return ( <aside style={{ width: 320, minWidth: 320, height: '100%', flexShrink: 0, padding: '16px', // Added padding for better spacing borderLeft: '1px solid #eaeaea', // Optional: adds definition overflowY: 'auto', }} > <h2 style={{ fontSize: '1.25rem', fontWeight: '600', marginBottom: '16px', color: '#333', }} > Recursion Tracker </h2> {SLOTS.map(id => ( <div key={id} id={`right-sidebar-slot-${id}`} /> ))} </aside> ); }

When your agent spawns this uiNode, WorkflowRenderer will render it and the component will be portalled into your target.

💡

With this, your UI nodes are no longer constrained. You can render them wherever you want.

Last updated on