Skip to content

Hooks

@termuijs/data exports a set of reactive hooks for fetching and streaming data inside TermUI components. Each hook manages its own lifecycle: it starts work on mount and cleans up on unmount. You never touch timers, sockets, or AbortControllers directly.

All hooks follow the same return shape:

·CODE
{ data, error, loading }

data holds the latest result, error holds the last failure, and loading is true while the first result is in flight.


useFetch

Fetches a URL and returns the parsed JSON. Subsequent calls to the same URL return a cached copy if the data is still fresh. Failed requests retry with exponential backoff.

·CODE
import { useFetch } from '@termuijs/data'

function StatsPanel() {
    const { data, error, loading } = useFetch<{ users: number }>(
        'https://api.example.com/stats',
        { staleTime: 30_000, retry: 3, retryDelay: 500 }
    )

    if (loading) return <Text>Loading...</Text>
    if (error)   return <Text color="red">{error.message}</Text>

    return <Text>Users: {data.users}</Text>
}

Signature

·CODE
function useFetch<T = unknown>(
    url: string,
    options?: UseFetchOptions
): UseFetchResult<T>

Options

OptionTypeDefaultDescription
staleTimenumber0How long (ms) a cached response stays fresh before a new request fires.
retrynumber0Max retry attempts after the first failure.
retryDelaynumber300Base backoff (ms). Delay doubles each attempt: retryDelay * 2^attempt.
keyunknown,When this value changes, a new fetch fires regardless of cache state. Useful for manual refresh buttons.

Return type

·CODE
interface UseFetchResult<T> {
    data:    T | null;
    error:   Error | null;
    loading: boolean;
}

Cache invalidation

Call invalidate(url) to clear the cache entry and trigger a fresh fetch on the next render:

·CODE
import { invalidate } from '@termuijs/data'

invalidate('https://api.example.com/stats')

Or pass a changing key to the hook:

·CODE
const [tick, setTick] = useState(0)
const { data } = useFetch('/api/data', { key: tick })

// In a key handler:
setTick(t => t + 1)  // triggers a new fetch

useWebSocket

Connects to a WebSocket URL and returns the latest message, the connection state, and a send function. The hook reconnects automatically on close using exponential backoff, starting at 1s and capping at 10s.

·CODE
import { useWebSocket } from '@termuijs/data'

function LiveFeed() {
    const { message, state, send } = useWebSocket('wss://stream.example.com/events')

    return (
        <col>
            <Text dim>Status: {state}</Text>
            <Text>{message ?? 'Waiting for data...'}</Text>
        </col>
    )
}

Signature

·CODE
function useWebSocket(url: string): UseWebSocketReturn

Return type

·CODE
type WebSocketState = 'connecting' | 'open' | 'closed' | 'error'

interface UseWebSocketReturn {
    message: string | null;     // Latest text message received
    state:   WebSocketState;    // Current connection state
    send:    (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void;
}

send is a no-op when the socket is not open, so you can call it without checking the state first.

Reconnect behaviour

RetryDelay
1st1s
2nd2s
3rd4s
4th8s
5th+10s (cap)

Retries reset to zero when the connection opens. Cleanup on unmount cancels any pending reconnect timer and closes the socket.


useSSE

Subscribes to a Server-Sent Events endpoint and returns the latest event payload. The hook closes the EventSource on unmount or when the URL changes.

·CODE
import { useSSE } from '@termuijs/data'

interface LogEvent { level: string; msg: string }

function LogStream() {
    const { data, error, loading } = useSSE<LogEvent>(
        '/api/log-stream',
        raw => JSON.parse(raw)   // optional parser
    )

    if (loading) return <Text dim>Connecting...</Text>
    if (error)   return <Text color="red">SSE error: {error.message}</Text>

    return <Text>[{data.level}] {data.msg}</Text>
}

Signature

·CODE
function useSSE<T = string>(
    url: string,
    parse?: (raw: string) => T
): UseSSEResult<T>

The parse function transforms the raw event string. When omitted, data is the raw string.

Return type

·CODE
interface UseSSEResult<T> {
    data:    T | null;
    error:   Error | null;
    loading: boolean;
}

loading stays true until the first message arrives. error is set if the EventSource fires an error event or if EventSource is unavailable in the environment.


usePolling

Calls an async function on a fixed interval and exposes the result reactively. Unlike useFetch, you supply the function, so you can poll any async data source: a database, a local file, a CLI tool output, etc.

The hook supports pause, resume, and manual refresh without resetting state or restarting the timer.

·CODE
import { usePolling } from '@termuijs/data'

async function fetchMetrics() {
    const res = await fetch('/api/metrics')
    return res.json() as Promise<{ rps: number }>
}

function MetricsPanel() {
    const { data, error, loading, paused, pause, resume, refresh } =
        usePolling(fetchMetrics, 3000)

    return (
        <col>
            <Text>RPS: {data?.rps ?? '--'}</Text>
            <Text dim>{paused ? 'Paused' : 'Live'}</Text>
        </col>
    )
}

Signature

·CODE
function usePolling<T>(
    fn:       () => Promise<T>,
    interval: number,
    deps?:    unknown[]
): UsePollingResult<T>

deps is an optional dependency array. When any value in deps changes, the hook re-runs fn immediately and restarts the interval. This works the same as the deps array in useEffect.

Return type

·CODE
interface UsePollingResult<T> {
    data:    T | null;
    error:   Error | null;
    loading: boolean;
    paused:  boolean;
    pause:   () => void;
    resume:  () => void;
    refresh: () => void;
}

refresh() fires fn immediately without waiting for the next interval tick and without affecting the timer schedule.


useMutation

Sends data to an HTTP endpoint and tracks the in-flight state. Use this for POST, PUT, PATCH, or DELETE actions triggered by user input.

·CODE
import { useMutation } from '@termuijs/data'

function CreateUserForm() {
    const { mutate, loading, error, data } = useMutation<{ id: string }>(
        '/api/users',
        'POST'
    )

    async function handleSubmit(name: string) {
        await mutate({ name })
    }

    return (
        <col>
            {error && <Text color="red">{error.message}</Text>}
            {data  && <Text color="green">Created user {data.id}</Text>}
        </col>
    )
}

Signature

·CODE
function useMutation<T = unknown>(
    url:    string,
    method: 'POST' | 'PUT' | 'PATCH' | 'DELETE'
): UseMutationReturn<T>

method defaults to 'POST'.

Return type

·CODE
interface UseMutationReturn<T> {
    mutate:        (payload: unknown) => Promise<T>;
    reset:         () => void;
    data:          T | null;
    error:         Error | null;
    loading:       boolean;
    mutationCount: number;
}

mutate throws if the request fails, wrap it in try/catch if you want to handle the error imperatively. reset() clears data, error, and loading back to their initial values. mutationCount increments on each successful mutation, useful for triggering re-fetches in other hooks via the key option.


useInfiniteQuery

Loads data page-by-page. The first page fetches on mount. You call fetchNextPage() to append the next page. When getNextPageParam returns undefined, hasNextPage becomes false and further calls are no-ops.

·CODE
import { useInfiniteQuery } from '@termuijs/data'

interface Page { items: string[]; cursor: string | null }

function LogList() {
    const { pages, loading, hasNextPage, fetchNextPage } = useInfiniteQuery<Page, string | null>({
        initialPageParam: null,
        queryFn: async (cursor) => {
            const url = cursor ? `/api/logs?cursor=${cursor}` : '/api/logs'
            const res = await fetch(url)
            return res.json()
        },
        getNextPageParam: (lastPage) => lastPage.cursor ?? undefined,
    })

    const allItems = pages.flatMap(p => p.items)

    return (
        <col>
            {allItems.map(item => <Text key={item}>{item}</Text>)}
            {hasNextPage && !loading && (
                <Text onPress={fetchNextPage}>Load more</Text>
            )}
        </col>
    )
}

Signature

·CODE
function useInfiniteQuery<T, P = number>(
    options: InfiniteQueryOptions<T, P>
): UseInfiniteQueryResult<T>

Options

·CODE
interface InfiniteQueryOptions<T, P> {
    queryFn:          (pageParam: P) => Promise<T>;
    initialPageParam: P;
    getNextPageParam: (lastPage: T, allPages: T[]) => P | undefined;
}

getNextPageParam receives the last fetched page and the full list of fetched pages. Return the param for the next page, or undefined to stop.

Return type

·CODE
interface UseInfiniteQueryResult<T> {
    pages:         T[];
    error:         Error | null;
    loading:       boolean;
    hasNextPage:   boolean;
    fetchNextPage: () => void;
}

Calling fetchNextPage() while a fetch is in flight is safe; it does nothing until the current request settles.


useFileWatch

Watches a file or directory for changes using Node's fs.watch. The hook fires on every rename or change event and cleans up the watcher on unmount.

·CODE
import { useFileWatch } from '@termuijs/data'

function ConfigWatcher() {
    const { data, error } = useFileWatch('/etc/myapp/config.json')

    return (
        <col>
            {error && <Text color="red">Watch error: {error.message}</Text>}
            {data  && <Text>File changed: {data.eventType} — {data.filename}</Text>}
        </col>
    )
}

Signature

·CODE
function useFileWatch(
    path:     string,
    options?: UseFileWatchOptions
): UseFileWatchResult

Options

·CODE
interface UseFileWatchOptions {
    persistent?: boolean;  // Keep process alive while watching (default: Node's default)
    recursive?:  boolean;  // Watch subdirectories (macOS and Windows only)
}

Return type

·CODE
interface FileWatchData {
    eventType: 'rename' | 'change';
    filename:  string | null;
}

interface UseFileWatchResult {
    data:    FileWatchData | null;
    error:   Error | null;
    loading: boolean;
}

data is null until the first change event. loading turns false after the first event or after an error.


Quick reference

HookDescriptionDefault interval
useFetchFetch JSON with cache and retryOn mount
useWebSocketLive WebSocket with auto-reconnectEvent-driven
useSSEServer-Sent Events subscriptionEvent-driven
usePollingPoll any async functionYour choice
useMutationPOST/PUT/PATCH/DELETE with stateOn call
useInfiniteQueryPage-by-page data loadingOn demand
useFileWatchWatch file or directory for changesEvent-driven

See also