Component
The Component class - every method, prop, and lifecycle hook
Component extends State and works directly as a React component. It's a real React class component under the hood, so it integrates with error boundaries, devtools, refs, and the React tree.
import { Component } from '@expressive/react';
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, 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.
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 a parameter (or without
render()altogether), children pass through a context provider.
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) optional
class SafeView extends Component {
async catch(error: Error) {
this.fallback = <p>Recovering...</p>;
await reportError(error);
}
render() {
return <Risky />;
}
}Error boundary handler. Called 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. - Without
catch(), errors propagate automatically. - Sync
catch()triggers immediate retry.
Lifecycle hooks
Component inherits the full State lifecycle. See State for details.
new()- one-shot setup, return cleanup.catch(error)- error boundary, above.- Destruction runs on unmount or
this.set(null).
A
use()method never runs for a Component rendered as JSX - it belongs to theState.use()hook path only.
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 |
children | ReactNode | Passed through unless render() is declared without a parameter |
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.
TypeScript shape
declare class Component extends State {
readonly props: Component.Props<this>;
fallback: Component.Node;
render(props?: {}): Component.Node;
catch?(error: Error): Promise<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.use,State.get,Provider,Consumer.