DevState: Extensible Debug Renderer
October 21, 2025
DevState: Extensible Debug Renderer#
Render complex runtime data structures with type-specific handlers. Register custom renderers to format atoms, queries, and domain objects without touching the core.
Registry Pattern#
Define custom renderers with type guards:
export function defineRenderer<T extends UnknownObject>(
test: (data: UnknownObject) => data is T,
render: Renderer<T>,
): RegistryItem<any> {
return { test, render };
}// Match by type predicate
defineRenderer(
(data) => isAtom(data),
({ data, showPath, r }) => {
const value = useAtomValue(data);
return <r.Unknown name={data.toString()} data={value} showPath={showPath} />;
},
);String-based renderers parse and transform:
export function defineStringRenderer<T>(
parse: (data: string) => null | undefined | T,
render: Renderer<T>,
): StringRegistryItem<any> {
return { parse, render };
}// Parse UIDs from strings
defineStringRenderer(
(data) => (World.isUID(data) ? data : null),
({ data, r, showPath }) => (
<r.Custom rawValue={data} icon={Ta.IconBox} showPath={showPath}>
<UIDElement uid={data} />
</r.Custom>
),
);Renderer Components#
Built-in primitives handle common cases:
const r: RendererComponents = {
Array: <Array data={items} name="Items" />,
Object: <Object data={obj} name="Config" commaSeparatedOmitKeys="internal,_private" />,
Unknown: <Unknown data={value} name="Result" />,
Placeholder: <Placeholder name="WeakMap" />,
Custom: (
<Custom icon={Ta.Icon} name="Type">
...
</Custom>
),
};Use commaSeparatedOmitKeys to filter object keys without memoizing the data object.
Integration#
Register renderers at app root:
export const ProvideAppDevStateRenderers = ({ children }: { children: React.ReactNode }) => {
return (
<ProvideDevStateRenderers registry={RENDERERS} stringRegistry={STRING_RENDERERS} orderedKeys={KEY_ORDER_PREFERENCE}>
{children}
</ProvideDevStateRenderers>
);
};const RENDERERS: RegistryItem<any>[] = [
defineRenderer(
(thing) => thing instanceof DevString,
({ data, r }) => <r.Custom name="DevString">...</r.Custom>,
),
defineRenderer(
objectPredicateMacro<Queryable>(["label", "hash"], (d) => d._tag === "db"),
({ data, r }) => <LiveQueryRenderer query={data} r={r} />,
),
];
export const ProvideAppDevStateRenderers = ({ children }) => (
<ProvideDevStateRenderers
registry={RENDERERS}
stringRegistry={STRING_RENDERERS}
orderedKeys={["devInfo", "name", "source"]}
>
{children}
</ProvideDevStateRenderers>
);Real Examples#
Jotai Atoms: Read atom value and render live
defineRenderer(
(thing) => isAtom(thing),
({ data, r }) => {
const value = useAtomValue(data);
return <r.Unknown name={data.debugLabel} data={value} />;
},
);DevString with location links:
defineRenderer(
(thing) => thing instanceof DevString,
({ data, r, showPath }) => {
const { context, message } = data.toJSON();
return (
<r.Custom name="DevString" nameChild={<DevStringLink reason={data} />}>
{message.map((part, i) =>
typeof part === "string" ? <span>{part}</span> : <r.Unknown data={part} showPath={["msg"]} />,
)}
{data.cause && <r.Unknown name="Reason" data={data.cause} />}
</r.Custom>
);
},
);Key Features#
- Type-safe matching: Type guards ensure correct data shape
- Composable: Renderers use
r.*components to recurse - React hooks: Use atoms, queries, state inside renderers
- Depth tracking: Automatic collapse at configurable depth
- Circular detection: Prevents infinite recursion
- Ordered keys: Control property display order globally