Component
The Component class - every method, prop, and lifecycle hook
Component extends State and works directly as a React component - React mounts it as a class component, so it integrates with error boundaries, devtools, refs, and the React tree. The class itself is still a model: fields and getters hold state, and render is a projection of it.
import { Component } from '@expressive/mvc';
class Counter extends Component {
count = 0;
render() {
return <button onClick={() => this.count++}>{this.count}</button>;
}
}
<Counter />;Instance properties
this.props
Readonly. Every prop passed to the JSX element - state-derived props, render-declared props, and special props (is, fallback, catch, children).
props is itself observable: this.set('props', callback) fires whenever a render delivers new props, and effects reading this.props re-run with it.
this.fallback
ReactNode | false. Content displayed while the component or its children are suspended, and during error recovery. Can be set as a class field, updated inside catch(), or overridden by the JSX fallback prop.
Defaults to null - nothing renders while suspended. Set false to opt out of the component's own Suspense boundary entirely; suspension then bubbles to the nearest ancestor boundary.
this.key
Readonly string. Identity used when the instance is rendered directly in a collection. Defaults to the instance's unique id (String(this)); override it as a field (override readonly key = 'nav') to supply your own.
Instance methods
render(props?)
Override to define output. Properties accessed via this in the render body are reactive - changes trigger a re-render.
Accept an optional parameter to declare extra props beyond state fields:
class Card extends Component {
title = '';
render(props = {} as { className: string }) {
return <div className={props.className}>{this.title}</div>;
}
}- The
= {} as Tdefault satisfies TypeScript's JSX inference and is safely ignored at runtime. - Non-optional parameter fields become required JSX attributes.
- Without
render()altogether, children pass through - the instance still provides context, runsmount(), and places its boundaries.
When a subclass overrides render(), it composes with the base rather than replacing it. Each render up the prototype chain wraps the one below as props.children, base-outermost - no super.render() call. A wrapper that omits props.children drops the inner content (the getter is lazy, so it never runs). See Render composition.
The whole composed chain runs in a single host render. Prefer this for reactivity over React hooks inside render(), and if you do use hooks, beware the sharp edge: a hook in a layer below a wrapper that conditionally renders props.children breaks the rules of hooks. For a scope isolated from that, use a subcomponent.
catch(error, instance) optional
class SafeView extends Component {
async catch(error: Error) {
this.fallback = <p>Recovering...</p>;
await reportError(error);
}
render() {
return <Risky />;
}
}Error boundary handler. Called with the thrown error and the instance when a child throws during render. While catch() is pending, this.fallback is displayed; when it resolves, render() is retried.
- Setting
this.fallbackinsidecatch()gives error-specific UI (reverted after recovery). - Rejecting from
catch()propagates the error to the next parent boundary. - If the retry throws again, that error propagates to the parent; a later error is caught again.
- Without
catch()(method or attribute), the component has no error boundary - errors propagate. - Sync
catch()triggers immediate retry.
Only render errors arrive here. Lifecycle and effect failures of the instance go to State.on({ catch }).
Lifecycle hooks
Component inherits the full State lifecycle. See State for details.
new()- one-shot setup on activation (runs on the server too), return cleanup.mount()- client-only effects, once on commit; return a function to run on unmount. Never runs during server render, nor for an instance placed as{component}- a placement does not own what it renders.catch(error, instance)- error boundary, above.- Destruction runs on unmount or
this.set(null).
A Component is rendered, not used -
MyComponent.use()throws, and ause()method never runs. For an instance outside the tree, useMyComponent.new().
JSX props
Settable state fields become optional JSX props, applied to the instance on every render:
<Counter count={5} />A field passed on one render and omitted on the next is cleared - reset to undefined, not left at its previous value. Props are a live binding, not one-time seeds.
Special props
Every Component also accepts these, regardless of state fields:
| Prop | Type | Description |
|---|---|---|
is | (instance: T) => void | Called once with the created instance |
ref | Ref<T> | Standard React ref - the instance after mount, null on unmount |
fallback | ReactNode | false | Overrides this.fallback; false defers to an ancestor boundary |
catch | (error, instance) => void | Promise<void> | Handles this element's errors in place of the catch() method |
children | ReactNode | Passed through when no render() is declared; otherwise read from the render parameter or this.props |
is runs after props apply but before the new() lifecycle hook - so it may configure state that new() then observes. Use ref (object or callback, per React convention) for post-mount imperative access instead.
Subcomponents
Any method whose name starts with a capital letter becomes a React component scoped to this:
class Dashboard extends Component {
items: string[] = [];
Sidebar(props: { label: string }) {
return (
<aside>
<h2>{props.label}</h2>
<ul>
{this.items.map((i) => (
<li key={i}>{i}</li>
))}
</ul>
</aside>
);
}
render() {
return <this.Sidebar label="Items" />;
}
}- Each usage subscribes independently to the parent instance.
- Accept props like any React component.
- Accessible via
Dashboard.get()then<dashboard.Sidebar />.
Overriding a subcomponent
A subclass swaps one out by assigning an instance field - the function receives the live instance as this:
function CustomSidebar(this: MyDashboard) {
return <aside>{this.items.length} items</aside>;
}
class MyDashboard extends Dashboard {
Sidebar = CustomSidebar;
}Assignment also works at runtime (instance.Sidebar = fn), which triggers a refresh wherever it's rendered. This makes a behavior-complete base with placeholder subcomponents a plug-and-play kit for subclasses.
Rendering an instance
An activated Component instance is itself a renderable element - place it as {instance}, alone or in an array:
const items = [Item.new({ label: 'First' }), Item.new({ label: 'Second' })];
<ul>{items}</ul>;- Each is keyed by
key. - Placing subscribes, provides context and installs the instance's boundaries, but doesn't own it:
mount()doesn't run, and unmounting only detaches. Whoever created it destroys it withinstance.set(null). - The same instance may be placed beneath separate parents.
Providing a State: <Component for>
The bare Component class - not a subclass - with a for attribute provides one State to its children. This replaces the removed Provider.
<Component for={Session} name="Ada" is={(session) => session.load()}>
<App />
</Component>| Attribute | Description |
|---|---|
for | A State class - constructed, owned, destroyed with the element - or an instance someone else owns |
is | Called with each instance the element constructs; class only |
fallback | Wraps children in a Suspense boundary; without it, <Component for> adds none |
catch | Error boundary for children, (error, instance) |
[field] | Typed from the provided State; assigned to it on every render |
A provided class's mount() runs with the element's; an instance passed in is never mounted or destroyed by it. Changing for releases the previous item and provides the next. One State per element - compose several as fields of a parent State. See Context.
TypeScript shape
declare class Component extends State {
readonly props: Component.Props<this>;
readonly key: string;
fallback: Component.Node;
render(props?: {}): Component.Node;
catch?(error: Error, instance: this): Promise<void> | void;
mount?(): (() => void) | void;
}Props are inferred automatically from state fields plus any render(props) declaration.
Component.Node is the host's element type - ReactNode once @expressive/react is loaded. In a host-agnostic package (one importing only @expressive/mvc), it falls back to any so un-annotated render overrides still type-check as JSX; annotate returns as Component.Node in library code meant to run under any adapter.
See also
- Components guide - patterns, subclass composition, children, context.
- State - the base class.
- Hooks -
State.useandState.get.