Expressive MVC
API Reference

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 T default 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, runs mount(), 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.fallback inside catch() 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 a use() method never runs. For an instance outside the tree, use MyComponent.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:

PropTypeDescription
is(instance: T) => voidCalled once with the created instance
refRef<T>Standard React ref - the instance after mount, null on unmount
fallbackReactNode | falseOverrides 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
childrenReactNodePassed 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 with instance.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>
AttributeDescription
forA State class - constructed, owned, destroyed with the element - or an instance someone else owns
isCalled with each instance the element constructs; class only
fallbackWraps children in a Suspense boundary; without it, <Component for> adds none
catchError 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

On this page