A small utility library for representing and consuming asynchronous values in React applications using the new use hook and server-rendering APIs. It enables you to wrap promises or values into a consistent "loadable" type, extract state for rendering logic, and seamlessly suspend or read values in both client and server environments.
loadable: Wrap a promise, value, or undefined into aLoadable<T>.extractState: Inspect the current state of aLoadable<T>(pending, loading, or ready).useLoadable: A React hook that leveragesusefor suspense or optional immediate reads.synchronize: Read a readyLoadable<T>synchronously or apply a mapping function when it’s available.
Install from npm:
npm install @silyze/react-loadableimport { loadable, useLoadable } from "@silyze/react-loadable";
import React, { useSyncExternalStore } from "react";
// Wrap an async fetch call into a Loadable
function useData<T>(url: string) {
function fetcher() {
return fetch(url).then((res) => res.json() as Promise<T>);
}
return loadable(useSyncExternalStore(subscribe, fetcher, fetcher));
}
// In a server-rendered or suspense-enabled component
function MyComponent() {
const data = useLoadable(useData<{ message: string }>("/api/msg"));
if (!data) {
return <div>Loading…</div>;
}
return <div>{data.message}</div>;
}A unique Symbol used as the key to store a pending promise inside a loadable object.
const LoadingSymbol: unique symbol;
export type LoadingSymbol = typeof LoadingSymbol;Represents a value that may be loading. Either a direct T, or an object containing a promise under the LoadingSymbol key, or null for pending.
export type Loadable<T> = T | { [LoadingSymbol]: Promise<T> | null };Wraps a promise, value, or undefined into a Loadable<T>:
- If given a
Promise<T>, returns{ [LoadingSymbol]: promise }. - If given
undefined, returns{ [LoadingSymbol]: null }(pending). - Otherwise, returns the raw value
T.
function loadable<T>(promiseOrT: Promise<T> | T | undefined): Loadable<T>;An enumeration of loadable states:
{ status: "pending" }— promise is not yet created.{ status: "loading" }— promise is active (not resolved).{ status: "ok"; value: T }— value is ready.
export type LoadableState<T> =
| { status: "pending" }
| { status: "loading" }
| { status: "ok"; value: T };Inspect a Loadable<T> and return its state:
function extractState<T>(loadable: Loadable<T>): LoadableState<T>;Example:
const state = extractState(loadable(Promise.resolve(42)));
// → { status: "loading" }A React hook for consuming loadables:
- If the loadable is ready, returns the value.
- If pending (
[LoadingSymbol]: null), returnsundefined. - If loading and
waitistrue(default), suspends viause(promise). - If loading and
waitisfalse, returnsundefinedimmediately.
function useLoadable<T>(loadable: Loadable<T>, wait?: boolean): T | undefined;Synchronously read a Loadable<T> without React hooks:
- If not loaded, returns
undefined. - If loaded and
onValueis omitted, returns the rawT. - If loaded and
onValueis provided, returns the mappedR.
function synchronize<T>(loadable: Loadable<T>): T | undefined;
function synchronize<T, R>(
loadable: Loadable<T>,
onValue: (value: T) => R
): R | undefined;import { loadable, extractState } from "@silyze/react-loadable";
const l1 = loadable(Promise.resolve(100));
console.log(extractState(l1)); // { status: "loading" }
const l2 = loadable(42);
console.log(extractState(l2)); // { status: "ok", value: 42 }import React from "react";
import { loadable, extractState } from "@silyze/react-loadable";
function StatusDisplay(loadableValue) {
const state = extractState(loadableValue);
switch (state.status) {
case "pending":
return <div>Pending…</div>;
case "loading":
return <div>Loading…</div>;
case "ok":
return <div>Value: {state.value}</div>;
}
}