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 leveragesuse
for 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-loadable
import { 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
wait
istrue
(default), suspends viause(promise)
. - If loading and
wait
isfalse
, returnsundefined
immediately.
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
onValue
is omitted, returns the rawT
. - If loaded and
onValue
is 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>;
}
}