DocsBuilding

State & Data

terminaltui provides reactive state management, computed values, data fetching, and real-time connections. All are imported from "terminaltui".

Reactive State

createState(initial)

Creates a reactive state container. Changes trigger UI re-renders of any dynamic() blocks that read the state.

import { createState } from "terminaltui";

const state = createState({ count: 0, name: "world" });

Reading State

state.get()          // returns { count: 0, name: "world" }
state.get("count")   // returns 0
state.get("name")    // returns "world"

Writing State

state.set("count", 1);
state.update("count", (prev) => prev + 1);

set replaces the value directly. update receives the previous value and returns the new one.

Batching Updates

When changing multiple keys at once, wrap them in batch() to fire a single re-render instead of one per change:

state.batch(() => {
  state.set("count", 10);
  state.set("name", "hello");
});

Subscribing to Changes

// Listen to a specific key
const unsubscribe = state.on("count", (newVal, oldVal) => {
  console.log(`count changed: ${oldVal} -> ${newVal}`);
});

// Listen to all changes
state.on("*", (key, newVal) => {
  console.log(`${key} changed to ${newVal}`);
});

// Stop listening
unsubscribe();

Using State in the UI

The dynamic() block re-renders whenever the state it reads changes:

import { createState, dynamic, markdown } from "terminaltui";

const state = createState({ count: 0 });

// Re-renders on any state change
dynamic(() => markdown(`Count: ${state.get("count")}`))

// Re-renders only when "count" changes (more efficient)
dynamic(["count"], () => markdown(`Count: ${state.get("count")}`))

Computed Values

computed(fn)

Cached derived values. The function runs once and caches the result. Call .invalidate() to force recalculation on the next .get().

import { computed } from "terminaltui";

const total = computed(() => state.get("price") * state.get("quantity"));
console.log(total.get());  // cached result

total.invalidate();         // next get() will recompute

Persistent State

createPersistentState(options)

State that persists to disk as JSON. Same API as createState. Survives app restarts.

import { createPersistentState } from "terminaltui";

const prefs = createPersistentState({
  path: "./data/prefs.json",
  defaults: { theme: "dracula", fontSize: 14 },
  encrypt: false,
});

prefs.get("theme");          // reads from file-backed state
prefs.set("theme", "nord");  // writes to disk automatically
OptionTypeDescription
pathstringFile path for JSON persistence
defaultsTDefault values when file doesn’t exist
encryptbooleanEncrypt the data on disk

Data Fetching

fetcher(options)

Reactive data fetcher with caching, retry logic, and auto-refresh. Returns a result object that updates as the request progresses.

import { fetcher } from "terminaltui";

const api = fetcher({
  url: "https://api.example.com/data",
  refreshInterval: 30000,
  retry: 3,
  retryDelay: 1000,
  cache: true,
  cacheTTL: 60000,
  transform: (data) => data.items,
  onError: (err) => console.error(err),
});

Fetcher Options

OptionTypeDescription
urlstringURL to fetch
fetch() => Promise<T>Custom fetch function (alternative to url)
methodstringHTTP method
headersRecord<string, string>Request headers
bodyanyRequest body
refreshIntervalnumberAuto-refresh interval in ms
cachebooleanEnable caching (default: true)
cacheTTLnumberCache TTL in ms (default: 60000)
retrynumberRetry count (default: 0)
retryDelaynumberRetry delay in ms (default: 1000)
transform(data) => TTransform the response
onError(err) => voidError handler

Fetcher Result

api.data       // T | null -- the fetched data
api.loading    // boolean -- whether a request is in progress
api.error      // Error | null -- the last error
api.refresh()  // manually trigger a re-fetch
api.mutate(d)  // optimistically update data
api.clear()    // clear cached data
api.destroy()  // stop auto-refresh and clean up

Using with API Routes

If your config defines API routes, relative URLs automatically resolve to the local API server:

// Hits your "GET /stats" API route — no port number needed
const stats = fetcher({ url: "/stats", refreshInterval: 5000 });

See API Routes for the full reference.

request(options) / request.get / .post / .put / .delete / .patch

Simple HTTP request helper for one-shot requests. Returns a promise.

import { request } from "terminaltui";

// Full form
const res = await request({
  url: "https://api.example.com/data",
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: { name: "test" },
  timeout: 5000,
});

// Shorthand methods
const res = await request.get("https://api.example.com/data");
const res = await request.post("https://api.example.com/data", { name: "test" });
const res = await request.put("https://api.example.com/data/1", { name: "updated" });
const res = await request.delete("https://api.example.com/data/1");
const res = await request.patch("https://api.example.com/data/1", { name: "patched" });

Passing Headers with Shorthand Methods

The third argument is a flat headers object (not { headers: {...} }):

const res = await request.post(
  "https://api.example.com/data",
  { name: "test" },
  { Authorization: "Bearer sk-..." }
);

Request Result

res.data     // T | null
res.error    // Error | null
res.status   // number (HTTP status code)
res.ok       // boolean (true if 2xx)

Real-Time Data

liveData(options)

Real-time data via WebSocket or Server-Sent Events. Returns a LiveDataConnection.

WebSocket

import { liveData } from "terminaltui";

const ws = liveData({
  type: "websocket",
  url: "wss://api.example.com/ws",
  onMessage: (data) => {
    state.set("messages", [...state.get("messages"), data]);
  },
  onConnect: () => console.log("Connected"),
  onDisconnect: () => console.log("Disconnected"),
  onError: (err) => console.error(err),
  reconnect: true,
  reconnectInterval: 5000,
});

ws.send("hello");
ws.close();
ws.connected;  // boolean

Server-Sent Events (SSE)

const sse = liveData({
  type: "sse",
  url: "https://api.example.com/events",
  onMessage: (event) => {
    // event.data, event.type, event.lastEventId
    state.set("latest", event.data);
  },
  headers: { Authorization: "Bearer ..." },
});

Combining with dynamic()

Connect real-time data to the UI using state and dynamic blocks:

const state = createState({ price: 0 });

const ws = liveData({
  type: "websocket",
  url: "wss://api.example.com/prices",
  onMessage: (data) => state.set("price", data.price),
  reconnect: true,
});

// In your page content:
dynamic(["price"], () => markdown(`Current price: **$${state.get("price")}**`))

Edit this page on GitHub