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
render
(props: Props, ctx: WidgetContext) => VNode
required
Render function receiving props and context.
Display name for debugging.
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.
Access semantic color tokens.
ctx.useViewport
() => ResponsiveViewportSnapshot
Current viewport dimensions.
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
Animation
useTransition, useSpring
Widget Catalog
Built-in widgets