Skip to content

Define And Components

Chris Michael edited this page Jul 18, 2026 · 8 revisions

Effuse

Define And Components

define({ name?, props?, layers?, script, template })

Status: Current.

define creates an Effuse component blueprint. script runs when the component state is created. Its returned object becomes the template context together with reactive props and children.

Basic Shape

const Greeting = define({
  name: 'Greeting',
  props: defineProps<{ name: string }>(),
  script({ props, computed }) {
    const label = computed(() => `Hello, ${props.name}`);
    return { label };
  },
  template: ({ label }) => <h1>{label}</h1>,
});

Props are reactive and are updated in place during reconciliation. Do not copy a prop into an ordinary variable when later prop changes should remain visible.

Schema-First Props

Use defineProps(schema) when props need defaults, runtime validation, nested structures, or input transforms. The schema produces two synchronized types:

  • Caller input: required fields must be supplied; optional and defaulted fields may be omitted; transformed fields use their encoded input type.
  • Resolved props: script and template receive decoded values with defaults applied.
import { Schema } from 'effect';

const userProps = PropSchema.struct({
  count: PropSchema.required(Schema.NumberFromString),
  name: PropSchema.required(PropSchema.String),
  role: PropSchema.optional(PropSchema.String, 'member'),
});

const UserLabel = define({
  props: defineProps(userProps),
  script: ({ props }) => ({
    // count is number and role is string after schema resolution.
    label: `${props.name}:${props.role}:${props.count}`,
  }),
  template: ({ label }) => <span>{label}</span>,
});

UserLabel({ name: 'Ada', count: '3' });

Nested builders retain their own presence rules. An optional nested object may be omitted, while required fields inside it remain required when the object is provided. Invalid initial props and updates throw PropsValidationError; a failed update leaves the previous reactive props unchanged.

Compatibility

The generic declaration remains supported:

props: defineProps<{ name: string }>()

Existing components may continue pairing that form with propsSchema. New components should pass the schema directly to defineProps to avoid duplicate type declarations. Supplying a different schema through both defineProps(schema) and propsSchema throws PropsSchemaConflictError.

Script Context

The current script context includes:

Group Members
Inputs props, layers, router
Reactivity signal, computed, watch, watchMultiple, watchEffect
Lifecycle onBeforeMount, onMount, onBeforeUnmount, onUnmount
Capabilities useLayer, useService, useComponent, store, useStore
Composition provide, inject, expose, useCallback, useMemo

Prefer alias layer records or typed direct layer helpers. String store/service lookups remain escape hatches and compatibility surfaces.

Children

Children are available in the template context:

const Panel = define({
  script: () => ({}),
  template: ({ children }) => <section class="panel">{children}</section>,
});

Lifecycle

const Clock = define({
  script({ signal, onMount, onUnmount }) {
    const now = signal(Date.now());
    let timer: ReturnType<typeof setInterval> | undefined;

    onMount(() => {
      timer = setInterval(() => (now.value = Date.now()), 1000);
    });
    onUnmount(() => {
      if (timer) clearInterval(timer);
    });

    return { now };
  },
  template: ({ now }) => <time>{now}</time>,
});

Watchers created through the script context are scoped to component unmount. Lifecycle callback failures are collected after all sibling callbacks and cleanups run. Configure createApp(root, { onError }) to receive typed LifecycleError aggregates; without a handler, Effuse reports them to the console instead of hiding them.

Rendering Primitives

Effuse exports For, Show, Switch, Dynamic, Repeat, Await, Suspense, and ErrorBoundary. Use these for explicit control flow and async boundaries. JSX expressions may also contain signals and functions that resolve to Effuse children.

Error Surface

A component can define blueprint error/loading behavior through lower-level APIs. Dynamic render failures without a custom boundary produce a visible data-effuse-render-error alert rather than silently dropping the subtree.

Related

Clone this wiki locally