Component Specification¶
Overview¶
Wilco components are server-defined React/TypeScript UI elements that can be dynamically loaded and rendered by the frontend. This specification defines the structure, discovery, bundling, and composition of components.
Design principles¶
Simplicity: Minimal required files, sensible defaults
Testability: Each component is isolated and independently testable
Debuggability: Full source maps, clear error messages, traceable paths
Reusability: Components can compose other components seamlessly
Flexibility: Optional features enhance but don’t complicate basics
Component structure¶
Directory layout¶
Each component is a directory containing a TypeScript entry point, located under the components directory:
src/wilco/examples/
└── <name>/
├── index.tsx # Required: Component entry point
├── schema.json # Optional: Props schema + metadata
├── __init__.py # Optional: Only needed if used as a Python package
└── *.tsx # Optional: Additional component files
Component naming¶
Component names are derived from their filesystem path relative to the components root. Directory names must be valid Python identifiers (letters, numbers, underscores; cannot start with a number).
components/ui/button/ → "ui.button"
components/forms/text_input/ → "forms.text_input"
components/example/counter/ → "example.counter"
The TSX component inside typically uses PascalCase (e.g., TextInput.tsx
inside text_input/). Use a library like pyheck for case transformations
if needed when generating display names from package paths.
Required files¶
__init__.py (optional)¶
An optional marker file. The __init__.py file is not required for
component discovery. Components only need an index.tsx (or index.ts)
file. Add __init__.py only if the component directory needs to be
importable as a Python package.
Future extensibility: This file may later support optional exports for server-side capabilities such as props validation, data fetching, or props transformation.
index.tsx¶
The entry point for the component. Must export a default React component.
Option 1: Direct implementation
// index.tsx - component defined directly
interface ButtonProps {
label: string;
variant?: 'primary' | 'secondary';
onClick?: () => void;
}
export default function Button({ label, variant = 'primary', onClick }: ButtonProps) {
return (
<button className={`btn btn-${variant}`} onClick={onClick}>
{label}
</button>
);
}
Option 2: Barrel file (recommended for larger components)
// index.tsx - re-exports from implementation file
export { default } from './Button';
// Button.tsx - actual implementation
export default function Button(props: ButtonProps) {
// ...
}
The barrel approach allows better code organization with subcomponents:
button/
├── __init__.py
├── index.tsx # export { default } from './Button'
├── Button.tsx # Main component
├── ButtonIcon.tsx # Subcomponent (internal)
└── styles.ts # Shared styles
All files are bundled together as an atomic package.
Optional files¶
schema.json¶
An extended JSON Schema file that defines component props and metadata. When absent, the component accepts any props and has no UI metadata.
{
"title": "Counter",
"description": "A simple counter component with increment/decrement controls",
"version": "1.0.0",
"type": "object",
"properties": {
"initialValue": {
"type": "number",
"default": 0,
"description": "Starting value for the counter"
},
"step": {
"type": "number",
"default": 1,
"minimum": 1,
"description": "Amount to increment/decrement by"
},
"min": {
"type": "number",
"description": "Minimum allowed value"
},
"max": {
"type": "number",
"description": "Maximum allowed value"
}
},
"required": []
}
Root-level metadata fields (extensions to JSON Schema):
title(string): Human-readable component namedescription(string): Component description for documentationversion(string): Semantic version of the component
Standard JSON Schema fields for props:
type: Must be"object"properties: Prop definitions using JSON Schema typesrequired: Array of required prop names
Component composition¶
The useComponent hook¶
Components can dynamically load and render other components using the
useComponent hook:
import { useComponent } from '@wilcojs/react';
export default function Dashboard() {
const Counter = useComponent('counter');
const Button = useComponent('button');
return (
<div>
<Counter initialValue={10} />
<Button label="Reset" onClick={() => {}} />
</div>
);
}
Implementation with React Query¶
useComponent is built on top of @tanstack/react-query’s
useSuspenseQuery, providing:
Automatic caching: Components are cached after first load
Deduplication: Multiple calls for the same component share one request
Background refetching: Stale components can be refreshed automatically
Suspense integration: Works seamlessly with React Suspense boundaries
// Simplified implementation
import { useSuspenseQuery } from '@tanstack/react-query';
export function useComponent(name: string): React.ComponentType<any> {
const { data: code } = useSuspenseQuery({
queryKey: ['component', name],
queryFn: () => fetchBundleCode(name),
staleTime: Infinity, // Components don't go stale
});
return useMemo(() => compileComponent(code, name), [code, name]);
}
The hook requires a QueryClientProvider and Suspense boundary in the
component tree:
import { QueryClientProvider } from '@tanstack/react-query';
import { Suspense } from 'react';
function App() {
return (
<QueryClientProvider client={queryClient}>
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
</QueryClientProvider>
);
}
Error handling¶
When a component fails to load (not found, bundle error, etc.), the hook throws an error. This should be caught by a React Error Boundary:
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<ErrorBoundary fallback={<ErrorMessage />}>
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
</ErrorBoundary>
);
}
Dependency resolution¶
Dependencies between components are resolved lazily:
Component A renders and calls
useComponent('ui.button')Hook checks if
ui.buttonis cachedIf not cached, initiates fetch (throws promise for Suspense)
Once loaded, component renders with the resolved dependency
No explicit dependency declaration is required. The server does not need to know about component dependencies ahead of time.
Discovery and registry¶
Component discovery¶
The backend discovers components by scanning the components directory:
Find all directories containing
__init__.pyCheck for
index.tsx(orindex.ts) in eachOptionally load
schema.jsonif presentRegister component with name derived from path
# Pseudocode for discovery
for package_dir in find_python_packages(components_root):
if has_file(package_dir, 'index.tsx'):
name = path_to_name(package_dir)
schema = load_optional(package_dir / 'schema.json')
registry.register(name, package_dir, schema)
Registry refresh¶
The registry can be refreshed at runtime to pick up new or modified components without server restart (useful for development).
API endpoints¶
List components¶
GET /api/bundles
Returns list of available component names:
[
{"name": "example.counter"},
{"name": "example.carousel"},
{"name": "ui.button"}
]
Get component bundle¶
GET /api/bundles/{name}.js
Returns bundled JavaScript code for the component. The bundle:
Is in ESM format
Includes inline source maps
Has external dependencies (react, react-dom) excluded
Is generated on-demand via esbuild
Response headers include Cache-Control: public, max-age=31536000, immutable
for aggressive caching. Cache busting is achieved via the content hash
(see HTTP Caching).
Get component metadata¶
GET /api/bundles/{name}/metadata
Returns component metadata from schema.json:
{
"title": "Counter",
"description": "A simple counter component",
"version": "1.0.0",
"props": {
"type": "object",
"properties": {
"initialValue": {"type": "number", "default": 0}
}
}
}
If no schema.json exists, returns minimal metadata:
{
"title": "counter",
"description": "",
"props": {}
}
Bundling¶
esbuild integration¶
Components are bundled using esbuild with the following configuration:
Format: ESM (ES Modules)
Target: ES2020
JSX: Automatic (React 17+ JSX transform)
External: react, react-dom, react/jsx-runtime
Source maps: Inline with original sources
Source map handling¶
Source maps are rewritten to use a custom URL scheme for debugging:
component://example.counter/Button.tsx
This allows the frontend to:
Identify component sources in stack traces
Map generated code back to original TypeScript
Display meaningful error locations
Module resolution¶
The frontend provides a module registry that bundled code uses:
window.__MODULES__ = {
'react': React,
'react/jsx-runtime': jsxRuntime,
'@wilcojs/react': wilco,
};
ESM imports in bundled code are transformed to use this registry:
// Original
import { useState } from 'react';
// Transformed
const { useState } = window.__MODULES__['react'];
Type safety¶
Current: runtime validation¶
Props are validated at runtime using JSON Schema:
Frontend fetches
schema.jsonvia metadata endpointPropsEditor UI enforces schema constraints
Invalid props show validation errors
Future: TypeScript generation¶
Planned but not yet implemented.
TypeScript type definitions will be generated from JSON Schema:
// Generated from schema.json
export interface CounterProps {
initialValue?: number;
step?: number;
min?: number;
max?: number;
}
This will enable:
IDE autocomplete for component props
Compile-time type checking
Better developer experience
Examples¶
Minimal component¶
The simplest possible component:
components/ui/hello/
├── __init__.py
└── index.tsx
// index.tsx
export default function Hello() {
return <div>Hello, World!</div>;
}
Component with props¶
A component with typed props and schema:
components/ui/greeting/
├── __init__.py
├── index.tsx
└── schema.json
// index.tsx
interface GreetingProps {
name: string;
formal?: boolean;
}
export default function Greeting({ name, formal = false }: GreetingProps) {
const greeting = formal ? `Good day, ${name}.` : `Hey ${name}!`;
return <p>{greeting}</p>;
}
{
"title": "Greeting",
"description": "Displays a personalized greeting",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the person to greet"
},
"formal": {
"type": "boolean",
"default": false,
"description": "Use formal greeting style"
}
},
"required": ["name"]
}
Composable component¶
A component that uses other components:
components/dashboard/stats/
├── __init__.py
└── index.tsx
// index.tsx
import { useComponent } from '@wilcojs/react';
interface StatsProps {
title: string;
metrics: Array<{ label: string; value: number }>;
}
export default function Stats({ title, metrics }: StatsProps) {
const Card = useComponent('card');
const Counter = useComponent('counter');
return (
<Card title={title}>
{metrics.map((metric) => (
<div key={metric.label}>
<span>{metric.label}</span>
<Counter initialValue={metric.value} />
</div>
))}
</Card>
);
}
Multi-file component¶
A larger component with internal organization:
components/forms/date_picker/
├── __init__.py
├── index.tsx
├── DatePicker.tsx
├── Calendar.tsx
├── DayCell.tsx
├── utils.ts
└── schema.json
// index.tsx
export { default } from './DatePicker';
// DatePicker.tsx
import Calendar from './Calendar';
import { formatDate } from './utils';
export default function DatePicker(props: DatePickerProps) {
// Uses internal Calendar component
return <Calendar {...props} />;
}
Appendix¶
JSON Schema quick reference¶
Common property types:
{
"stringProp": {"type": "string"},
"numberProp": {"type": "number", "minimum": 0, "maximum": 100},
"booleanProp": {"type": "boolean", "default": false},
"enumProp": {"type": "string", "enum": ["a", "b", "c"]},
"arrayProp": {"type": "array", "items": {"type": "string"}},
"objectProp": {
"type": "object",
"properties": {"nested": {"type": "string"}}
}
}
Error codes¶
Error |
Cause |
|---|---|
ComponentNotFound |
No component with given name in registry |
BundleError |
esbuild failed to bundle the component |
SchemaValidationError |
Props don’t match schema (runtime) |
RenderError |
Component threw during React render |