extension

@statelyjs/ui Extension System

Extensions provide a type-safe, composable way for plugins to modify behavior. An extension point is a named hook that accepts transformers - functions that receive state and return (possibly modified) state.

Core Concepts

  • Extension Point: A named hook defined by a plugin (e.g., addStringModes)
  • Transformer: A function (T) => T that modifies state
  • Composition: Multiple transformers chain together via extend()

For Plugin Authors

Define extension points to allow customization of your plugin's behavior:

// Define the state shape
export interface StringEditState {
  mode: string;
  modeGroups: StringModeGroup[];
  component?: ComponentType<...>;
}

// Create the extension point
export const addStringModes = defineExtension<StringEditState>({
  id: 'core.addStringModes',
  summary: 'Add custom input modes to string fields',
});

// In your component, apply the extension
const state = addStringModes.transform(initialState);

For Users & Plugin Consumers

Extend behavior by registering transformers:

import { addStringModes } from '@statelyjs/stately/extensions';

addStringModes.extend(state => ({
  ...state,
  modeGroups: [...state.modeGroups, myCustomModes],
}));

Composition

Transformers compose in registration order. If plugin A and plugin B both extend the same point:

// Plugin A (registered first)
point.extend(s => ({ ...s, value: s.value + 1 }));

// Plugin B (registered second)
point.extend(s => ({ ...s, value: s.value * 2 }));

// Result: initial { value: 0 } → A: { value: 1 } → B: { value: 2 }
point.transform({ value: 0 }); // { value: 2 }

Interfaces

Extensible

Defined in: packages/ui/src/extension.tsx:310

An extension object for registering transformers.

This is the second element of the tuple returned by createExtensible. Use this at module/plugin initialization time to register extensions.

Extends

Type Parameters

TState

TState

The state shape that flows through transformers

Properties

extension

readonly extension: ExtensionPoint<TState>

Defined in: packages/ui/src/extension.tsx:315

The underlying extension point. Use this for advanced scenarios requiring direct pipeline access.

id

readonly id: string

Defined in: packages/ui/src/extension.tsx:80

Unique identifier for the extension point.

Use the convention {plugin}.{feature} to avoid collisions.

Examples
'core.addStringModes'
'files.filePreview'
Inherited from

ExtensionPointConfig.id

summary

readonly summary: string

Defined in: packages/ui/src/extension.tsx:89

Human-readable summary describing what this extension point does.

This is used for documentation generation and IDE hints.

Example
'Add custom input modes to string fields'
Inherited from

ExtensionPointConfig.summary

Methods

extend()

extend(input): void

Defined in: packages/ui/src/extension.tsx:339

Extend the state with a partial update or transformer function.

Two forms are supported:

  1. Partial object - Merged into state unconditionally:

    feature.extend({ label: 'New Label', count: 5 });
  2. Transformer function - Receives state, returns partial to merge:

    feature.extend(state => ({
      component: state.mode === 'code' ? CodeEditor : state.component,
    }));

In both cases, you never need to spread state - the framework handles deep merging automatically.

Parameters
input

ExtendInput<TState>

Partial state object or transformer function

Returns

void


ExtensibleConfig

Defined in: packages/ui/src/extension.tsx:270

Configuration for creating an extensible hook.

Extends

Type Parameters

TOptions

TOptions

Options passed to the hook

TState

TState

The state shape that flows through transformers

Properties

id

readonly id: string

Defined in: packages/ui/src/extension.tsx:80

Unique identifier for the extension point.

Use the convention {plugin}.{feature} to avoid collisions.

Examples
'core.addStringModes'
'files.filePreview'
Inherited from

ExtensionPointConfig.id

initial()

initial: (options) => TState

Defined in: packages/ui/src/extension.tsx:278

Factory function that creates initial state from hook options. Called each time the hook is used.

Parameters
options

TOptions

Options passed to the hook

Returns

TState

Initial state before any transformers are applied

summary

readonly summary: string

Defined in: packages/ui/src/extension.tsx:89

Human-readable summary describing what this extension point does.

This is used for documentation generation and IDE hints.

Example
'Add custom input modes to string fields'
Inherited from

ExtensionPointConfig.summary


ExtensionPoint

Defined in: packages/ui/src/extension.tsx:101

An extension point that plugins can extend with transformers.

Extension points are the primary mechanism for plugins to allow customization of their behavior. They follow a simple pattern: state in, state out (T => T).

Extends

Type Parameters

T

T

The state type that flows through the extension

Properties

id

readonly id: string

Defined in: packages/ui/src/extension.tsx:80

Unique identifier for the extension point.

Use the convention {plugin}.{feature} to avoid collisions.

Examples
'core.addStringModes'
'files.filePreview'
Inherited from

ExtensionPointConfig.id

summary

readonly summary: string

Defined in: packages/ui/src/extension.tsx:89

Human-readable summary describing what this extension point does.

This is used for documentation generation and IDE hints.

Example
'Add custom input modes to string fields'
Inherited from

ExtensionPointConfig.summary

Methods

extend()

extend(transformer): void

Defined in: packages/ui/src/extension.tsx:139

Register a transformer to this extension point.

Transformers are composed in registration order. Each transformer receives the output of the previous transformer (or the initial state for the first transformer).

Parameters
transformer

(state) => T

A function that receives state and returns modified state

Returns

void

Example
addStringModes.extend(state => ({
  ...state,
  modeGroups: [...state.modeGroups, myModeGroup],
  component: state.mode === 'code' ? CodeEditor : state.component,
}));
transform()

transform(state): T

Defined in: packages/ui/src/extension.tsx:119

Apply all registered transformers to the input state.

Transformers are applied in registration order. If no transformers are registered, returns the input state unchanged.

Parameters
state

T

The initial state

Returns

T

The transformed state

Example
const resolved = addStringModes.transform({
  mode: 'text',
  modeGroups: [coreModes],
});

ExtensionPointConfig

Defined in: packages/ui/src/extension.tsx:71

Configuration for defining an extension point.

Extended by

Properties

id

readonly id: string

Defined in: packages/ui/src/extension.tsx:80

Unique identifier for the extension point.

Use the convention {plugin}.{feature} to avoid collisions.

Examples
'core.addStringModes'
'files.filePreview'
summary

readonly summary: string

Defined in: packages/ui/src/extension.tsx:89

Human-readable summary describing what this extension point does.

This is used for documentation generation and IDE hints.

Example
'Add custom input modes to string fields'

Type Aliases

ExtendInput

ExtendInput<T> = Partial<T> | (state) => Partial<T>

Defined in: packages/ui/src/extension.tsx:289

Input type for extend() - either a partial state object or a transformer function.

  • Partial<T>: Merged into state (deep merge)
  • (state: T) => Partial<T>: Function that receives state and returns partial to merge

In both cases, the framework handles merging. You never need to spread state.

Type Parameters

T

T


ExtensibleHook()

ExtensibleHook<TOptions, TState> = (options) => TState

Defined in: packages/ui/src/extension.tsx:300

A React hook that returns transformed state from an extensible.

This is the first element of the tuple returned by createExtensible. Use this inside React components.

Type Parameters

TOptions

TOptions

Options passed to the hook

TState

TState

The state shape that flows through transformers

Parameters

options

TOptions

Returns

TState


ExtensionState

ExtensionState<E> = E extends ExtensionPoint<infer T> ? T : never

Defined in: packages/ui/src/extension.tsx:205

Type helper to extract the state type from an extension point.

Type Parameters

E

E

Example

type State = ExtensionState<typeof addStringModes>;
// Infers the state type from the extension point

Functions

createExtensible()

createExtensible<TOptions, TState>(config): [ExtensibleHook<TOptions, TState>, Extensible<TState>]

Defined in: packages/ui/src/extension.tsx:399

Create an extensible hook with ergonomic extension API.

This is the primary way for plugin authors to define extension points. It wraps defineExtension with:

  • A React hook for use in components
  • An extend() method that handles deep merging automatically
  • No need to spread state in transformers

Defining an Extension Point

export interface MyFeatureState {
  items: string[];
  component?: ComponentType<any>;
}

export const [useMyFeature, myFeature] = createExtensible<
  { formId: string },
  MyFeatureState
>({
  id: 'myPlugin.myFeature',
  summary: 'Customize my feature',
  initial: (opts) => ({
    items: ['default'],
    component: undefined,
  }),
});

Using in Components

function MyComponent({ formId }) {
  const { items, component: Component } = useMyFeature({ formId });
  // ...
}

Extending (from other plugins)

// Simple partial - always applied
myFeature.extend({ items: ['custom'] });

// Transformer - conditional logic, but still no spreading
myFeature.extend(state => ({
  component: state.mode === 'custom' ? CustomComponent : state.component,
}));

Type Parameters

TOptions

TOptions

Options passed to the hook

TState

TState extends object

The state shape (must be an object)

Parameters

config

ExtensibleConfig<TOptions, TState>

Configuration including id, summary, and initial state factory

Returns

[ExtensibleHook<TOptions, TState>, Extensible<TState>]

Tuple of [hook, extension] - hook for components, extension for registration


defineExtension()

defineExtension<T>(config): ExtensionPoint<T>

Defined in: packages/ui/src/extension.tsx:180

Define a new extension point.

Extension points allow plugins to expose hooks that other plugins or user code can extend. The pattern is simple: transformers are functions (T) => T that receive state and return (possibly modified) state.

Type Parameters

T

T

The state type that flows through the extension

Parameters

config

ExtensionPointConfig

Configuration for the extension point

Returns

ExtensionPoint<T>

A new extension point

Example

// Define the state shape
export interface MyExtensionState {
  options: string[];
  selectedOption?: string;
}

// Create the extension point
export const myExtension = defineExtension<MyExtensionState>({
  id: 'myPlugin.myExtension',
  summary: 'Customize available options',
});

// Use it in your component
const state = myExtension.transform({
  options: ['default'],
  selectedOption: undefined,
});

// Others can extend it
myExtension.extend(state => ({
  ...state,
  options: [...state.options, 'custom'],
}));