Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/RtlZeroMemory/Rezi/llms.txt

Use this file to discover all available pages before exploring further.

Overview

defineWidget enables building reusable, stateful components with hooks. Each widget instance maintains its own state that persists across renders.
import { defineWidget, ui } from "@rezi-ui/core";

type CounterProps = {
  initial: number;
  key?: string; // Required for keying
};

const Counter = defineWidget<CounterProps>((props, ctx) => {
  const [count, setCount] = ctx.useState(props.initial);

  return ui.row([
    ui.text(`Count: ${count}`),
    ui.button({
      id: ctx.id("inc"),
      label: "+",
      onPress: () => setCount(c => c + 1),
    }),
  ]);
});

// Usage
ui.column([Counter({ initial: 0 }), Counter({ initial: 10, key: "c2" })]);

API

defineWidget

render
(props: Props, ctx: WidgetContext) => VNode
required
Render function receiving props and context.
options
DefineWidgetOptions
Optional configuration.
options.name
string
Display name for debugging.
options.wrapper
'column' | 'row'
Container wrapper kind (default: 'column').
Returns: Widget factory function

WidgetContext

The context provides these APIs:
ctx.id
(suffix: string) => string
Generate scoped ID unique to this widget instance.
ctx.useState
<T>(initial: T | (() => T)) => [T, (v: T | ((prev: T) => T)) => void]
Local state that persists across renders.
ctx.useRef
<T>(initial: T) => { current: T }
Mutable ref that doesn’t trigger re-renders.
ctx.useEffect
(effect: () => void | (() => void), deps?) => void
Side effects with cleanup.
ctx.useMemo
<T>(factory: () => T, deps?) => T
Memoized computed value.
ctx.useCallback
<T>(callback: T, deps?) => T
Memoized callback reference.
ctx.useAppState
<T>(selector: (s: S) => T) => T
Select app state slice.
ctx.useTheme
() => ColorTokens | null
Access semantic color tokens.
ctx.useViewport
() => ResponsiveViewportSnapshot
Current viewport dimensions.
ctx.invalidate
() => void
Request widget re-render.

Props Best Practices

Always Include key Prop

type MyWidgetProps = {
  data: string;
  key?: string; // Required for reconciliation
};

const MyWidget = defineWidget<MyWidgetProps>((props, ctx) => {
  return ui.text(props.data);
});

Use Readonly Props

type ReadonlyProps = Readonly<{
  count: number;
  items: readonly string[];
}>;

const Widget = defineWidget<ReadonlyProps>((props, ctx) => {
  // props is immutable
  return ui.text(`${props.count}`);
});

State Management

Local State

Use ctx.useState for widget-local state:
const TodoList = defineWidget((props, ctx) => {
  const [todos, setTodos] = ctx.useState<string[]>([]);
  const [input, setInput] = ctx.useState("");

  const addTodo = () => {
    if (input.trim()) {
      setTodos(prev => [...prev, input]);
      setInput("");
    }
  };

  return ui.column([
    ui.input({ id: ctx.id("input"), value: input, onChange: setInput }),
    ui.button({ id: ctx.id("add"), label: "Add", onPress: addTodo }),
    ...todos.map((todo, i) => ui.text(`${i + 1}. ${todo}`)),
  ]);
});

App State Access

type AppState = { user: { name: string }; theme: string };

const UserBadge = defineWidget<{}, AppState>((props, ctx) => {
  const userName = ctx.useAppState(s => s.user.name);
  const theme = ctx.useAppState(s => s.theme);

  return ui.badge({ label: userName, variant: theme === "dark" ? "solid" : "outline" });
});

Scoped IDs

Use ctx.id() for unique IDs:
const Form = defineWidget((props, ctx) => {
  const [name, setName] = ctx.useState("");
  const [email, setEmail] = ctx.useState("");

  return ui.column([
    ui.input({ id: ctx.id("name"), value: name, onChange: setName }),
    ui.input({ id: ctx.id("email"), value: email, onChange: setEmail }),
    ui.button({ id: ctx.id("submit"), label: "Submit" }),
  ]);
});

// Generated IDs: "Form_0_name", "Form_0_email", "Form_0_submit"
// Second instance: "Form_1_name", "Form_1_email", "Form_1_submit"

Effects and Lifecycle

const DataFetcher = defineWidget<{ url: string }>((props, ctx) => {
  const [data, setData] = ctx.useState(null);

  ctx.useEffect(() => {
    let cancelled = false;

    fetch(props.url)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) setData(data);
      });

    return () => { cancelled = true; }; // Cleanup
  }, [props.url]);

  return ui.text(data ? JSON.stringify(data) : "Loading...");
});

Animation

import { defineWidget, useTransition, ui } from "@rezi-ui/core";

const FadeIn = defineWidget<{ visible: boolean }>((props, ctx) => {
  const opacity = useTransition(ctx, props.visible ? 1 : 0, {
    duration: 300,
  });

  return ui.box({ opacity }, [ui.text("Fading content")]);
});

Composition

const Panel = defineWidget<{ title: string; children: VNode[] }>((props, ctx) => {
  const [collapsed, setCollapsed] = ctx.useState(false);

  return ui.column([
    ui.row([
      ui.text(props.title, { variant: "heading" }),
      ui.button({
        id: ctx.id("toggle"),
        label: collapsed ? "Expand" : "Collapse",
        onPress: () => setCollapsed(!collapsed),
      }),
    ]),
    ...(!collapsed ? props.children : []),
  ]);
});

// Usage
Panel({ title: "Details", children: [ui.text("Content")] });

Testing

import { createTestRenderer, ui } from "@rezi-ui/core";
import { describe, test, assert } from "@rezi-ui/testkit";

const MyWidget = defineWidget<{ value: number }>((props, ctx) => {
  return ui.text(`Value: ${props.value}`);
});

describe("MyWidget", () => {
  test("renders value", () => {
    const renderer = createTestRenderer({ width: 80, height: 24 });
    const tree = MyWidget({ value: 42 });
    const result = renderer.render(tree);

    assert(result.text.includes("Value: 42"));
  });
});

Hook Rules

All hooks must be called in the same order every render. No conditional hook calls.
// Bad
if (props.enabled) {
  const [count, setCount] = ctx.useState(0); // Error!
}

// Good
const [count, setCount] = ctx.useState(0);
if (!props.enabled) return ui.empty();

State Hooks

useState, useRef, useMemo

Lifecycle

useEffect

Animation

useTransition, useSpring

Widget Catalog

Built-in widgets

Build docs developers (and LLMs) love