jotai-eager
Version:
Jōtai utilities that help with asynchronous atoms
286 lines (278 loc) • 13 kB
TypeScript
import { Atom, ExtractAtomValue, Getter, Setter, WritableAtom } from 'jotai/vanilla';
type AwaitAtomsValues<TTuple extends readonly [Atom<unknown>, ...Atom<unknown>[]]> = {
[Index in keyof TTuple]: Awaited<ExtractAtomValue<TTuple[Index]>>;
};
/**
* Awaits all `deps` if necessary, then runs `op` given all deps in the same order.
* If computing the value fails (throws), a rejected Promise is returned no matter if
* the processing happened synchronously or not.
*
* @param deps An array of atoms whose values will be awaited and passed to the operation function.
* @param op A function that takes the awaited values of the dependencies and returns a new value. It is called synchronously if all dependencies are fulfilled, otherwise asynchronously.
* @returns An atom that resolves to the result of the operation or a promise of the result if dependencies are pending.
* @deprecated Use `eagerAtom` for new code, as it provides a better developer experience, and matches more closely to how regualr atoms are defined.
*
* @example
* ```ts
* import { atom } from 'jotai';
* import { derive } from 'jotai-eager';
*
* const aAtom = atom(async () => 1);
* const bAtom = atom(async () => 2);
* const sumAtom = derive([aAtom, bAtom], (a, b) => a + b);
* ```
*/
declare function derive<TDeps extends readonly [Atom<unknown>, ...Atom<unknown>[]], TValue>(deps: TDeps, op: (...depValues: AwaitAtomsValues<TDeps>) => TValue): Atom<TValue | Promise<Awaited<TValue>>>;
type AwaitedAll<T extends readonly unknown[]> = {
[K in keyof T]: Awaited<T[K] extends Atom<infer Value> ? Value : T[K]>;
};
interface EagerGetter {
/**
* Retrieves the atom's fulfilled value.
* If the value is not yet available, it interrupts the execution
* of the eager atom until it's available.
*/
<Value>(atom: Atom<Value>): Awaited<Value>;
/**
* Retrieves the Promise's fulfilled value.
* If the value is not yet available, it interrupts the execution
* of the eager atom until it's available.
*/
await<T>(promiseOrValue: T): Awaited<T>;
/**
* Retrieves the fulfilled value of all passed in Promises.
* If the values are not yet available, it interrupts the execution
* of the eager atom until they're available.
*/
awaitAll<T extends readonly unknown[]>(args: T): AwaitedAll<T>;
/**
* Retrieves the fulfilled value of all passed in atoms.
* If the values are not yet available, it interrupts the execution
* of the eager atom until they're available.
*/
all<T extends readonly Atom<unknown>[]>(atoms: T): AwaitedAll<T>;
}
type Read<Value> = (get: EagerGetter) => Value;
type Write<Args extends unknown[], Result> = (get: Getter, set: Setter, ...args: Args) => Result;
type AsyncReadFunctionError = 'ERROR: The `read` function of eager atoms cannot be asynchronous, or return a Promise.';
/**
* A drop-in replacement for vanilla atoms with custom async read functions, that
* removes unnecessary suspensions. The read function is written as if it was
* synchronous, which allows for:
* - eager computation of the atom's value in case all of its dependencies are fulfilled
* (which is not the case for vanilla async atoms).
* - interrupting computation if a dependency is not yet fulfilled.
* The `get` parameter provides methods like `all()` to await multiple atoms simultaneously
* and `await()` for non-atom promises.
*
* @param read A synchronous function that computes the atom's value using the eager getter, which can await dependencies directly.
* @param write An optional function to handle writes to the atom, receiving the standard getter, setter, and arguments.
* @returns An atom that resolves to the computed value or a promise of the result if dependencies are pending. For writable atoms, includes write functionality.
*
* @example
* ```ts
* import { atom } from 'jotai';
* import { eagerAtom } from 'jotai-eager';
*
* const petsAtom = atom(Promise.resolve(['cat', 'dog']));
* const filterAtom = atom('cat');
* const filteredPetsAtom = eagerAtom((get) => {
* const pets = get(petsAtom);
* const filter = get(filterAtom);
* return pets.filter(name => name.includes(filter));
* });
* ```
*/
declare function eagerAtom<Value, Args extends unknown[], Result>(...args: [Value] extends [PromiseLike<unknown>] ? [AsyncReadFunctionError] : [read: Read<Value>, write: Write<Args, Result>]): WritableAtom<Promise<Value> | Value, Args, Result>;
declare function eagerAtom<Value>(...args: [Value] extends [PromiseLike<unknown>] ? [AsyncReadFunctionError] : [read: Read<Value>]): Atom<Promise<Value> | Value>;
/**
* Only useful if the eager atom's read function involves a try {} catch {}. Can be used to
* detect whether a thrown value originates from `jotai-eager`, in which case should be rethrown.
*
* @param error The error thrown during eager atom computation to check.
* @returns True if the error is a suspension trigger from jotai-eager, indicating computation should be paused; otherwise, false.
*
* @example
* ```ts
* import { eagerAtom, isEagerError } from 'jotai-eager';
*
* const myAtom = eagerAtom((get) => {
* try {
* // some computation
* } catch (e) {
* if (isEagerError(e)) {
* throw e; // Rethrow to let jotai-eager handle
* }
* // Handle other errors
* }
* });
* ```
*/
declare function isEagerError(error: unknown): boolean;
type Loadable<Value> = {
state: 'loading';
} | {
state: 'hasError';
error: unknown;
} | {
state: 'hasData';
data: Awaited<Value>;
};
/**
* Wraps an atom to provide a loadable state, representing its value as 'loading', 'hasError', or 'hasData'.
* Shares a Promise cache between all jotai-eager APIs, further minimizing suspensions.
*
* @param anAtom The atom whose value should be wrapped in a loadable state.
* @returns An atom that returns a Loadable object indicating the current state of the input atom's value.
*
* @example
* ```ts
* import { atom } from 'jotai';
* import { loadable } from 'jotai-eager';
*
* const asyncAtom = atom(async () => 'data');
* const loadableAtom = loadable(asyncAtom);
* // loadableAtom returns: { state: 'loading' } | { state: 'hasError', error: unknown } | { state: 'hasData', data: 'data' }
* ```
*/
declare function loadable<Value>(anAtom: Atom<Value>): Atom<Loadable<Value>>;
/**
* Executes `process` with `data` as input synchronously if `data` is known, meaning
* it is not an unresolved promise of the value.
*
* @param data The data to process, now or later (soon).
* @param process The processing function that takes the awaited data and returns a result.
* @returns The result of processing the data synchronously if available, or a promise of the result if data is pending.
*
* @example
* ```ts
* import { atom } from 'jotai';
* import { soon } from 'jotai-eager';
*
* const dataAtom = atom(Promise.resolve('hello'));
* const resultAtom = atom((get) => {
* const data = get(dataAtom);
* return soon(data, (resolved) => resolved.toUpperCase());
* });
* ```
*/
declare function soon<TInput, TOutput>(data: TInput, process: (knownData: Awaited<TInput>) => TOutput): TOutput | Promise<Awaited<TOutput>>;
/**
* Executes `process` with `data` as input synchronously if `data` is known, meaning
* it is not an unresolved promise of the value.
*
* @param process The processing function that takes the awaited data and returns a result.
* @returns A function that can be called with `data`, and returns the result (or promise of result) from running `process` on `data`.
*
* @example
* ```ts
* import { atom } from 'jotai';
* import { soon } from 'jotai-eager';
*
* const processFn = soon((str: string) => str.toUpperCase());
* const result = processFn(Promise.resolve('hello')); // Promise<'HELLO'> or 'HELLO'
* ```
*/
declare function soon<TInput, TOutput>(process: (knownData: NoInfer<Awaited<TInput>>) => TOutput): (data: TInput) => TOutput | Promise<Awaited<TOutput>>;
type PromiseOrValue<T> = Promise<T> | T;
type SoonAll<T extends readonly unknown[]> = PromiseOrValue<{
[Index in keyof T]: Awaited<T[Index]>;
}>;
/**
* Given array `values`, if all elements are known (are not unresolved promises),
* returns an array of the same length with Awaited `values`. Otherwise, it returns a
* promise to that array.
*
* @param values An array of values or promises to await collectively.
* @returns An array of the awaited values if all are resolved synchronously, or a promise resolving to that array if any are pending.
*
* @example
* ```ts
* import { soonAll } from 'jotai-eager';
*
* const values = [Promise.resolve(1), 2, Promise.resolve(3)];
* const result = soonAll(values); // [1, 2, 3] or Promise<[1, 2, 3]>
* ```
*/
declare function soonAll<T extends readonly unknown[] | []>(values: T): SoonAll<T>;
declare function soonAll<T extends readonly unknown[]>(values: T): SoonAll<T>;
interface WithPendingContext<Value> {
get: Getter;
prev: Awaited<Value> | undefined;
pending: PromiseLike<Awaited<Value>>;
}
/**
* Wraps an atom to handle pending states by returning a fallback value while the atom's value is unresolved, optionally using a custom fallback function.
* Intended as an alternative to Jotai's `unwrap`, providing enhanced pending state handling.
*
* @param anAtom The atom to wrap, which may resolve asynchronously.
* @returns An atom that returns the resolved value or undefined while pending.
*
* @example
* ```ts
* import { atom } from 'jotai';
* import { withPending } from 'jotai-eager';
*
* const asyncAtom = atom(Promise.resolve('data'));
* const wrappedAtom = withPending(asyncAtom);
* // wrappedAtom returns 'data' when resolved, or undefined while pending
* ```
*/
declare function withPending<Value, Args extends unknown[], Result>(anAtom: WritableAtom<Value, Args, Result>): WritableAtom<Awaited<Value> | undefined, Args, Result>;
/**
* Wraps an atom to handle pending states by returning a fallback value while the atom's value is unresolved, optionally using a custom fallback function.
* Intended as an alternative to Jotai's `unwrap`, providing enhanced pending state handling.
*
* @param anAtom The atom to wrap, which may resolve asynchronously.
* @param fallback A function that receives context (getter, previous value, pending promise) and returns a value to use while pending.
* @returns An atom that returns the resolved value or the fallback while pending.
*
* @example
* ```ts
* import { atom } from 'jotai';
* import { withPending } from 'jotai-eager';
*
* const asyncAtom = atom(Promise.resolve('data'));
* const wrappedAtom = withPending(asyncAtom, () => 'Loading...');
* // wrappedAtom returns 'data' when resolved, or 'Loading...' while pending
* ```
*/
declare function withPending<Value, Args extends unknown[], Result, PendingValue>(anAtom: WritableAtom<Value, Args, Result>, fallback: (ctx: WithPendingContext<Value>) => PendingValue): WritableAtom<Awaited<Value> | PendingValue, Args, Result>;
/**
* Wraps an atom to handle pending states by returning a fallback value while the atom's value is unresolved, optionally using a custom fallback function.
* Intended as an alternative to Jotai's `unwrap`, providing enhanced pending state handling.
*
* @param anAtom The atom to wrap, which may resolve asynchronously.
* @returns An atom that returns the resolved value or undefined while pending.
*
* @example
* ```ts
* import { atom } from 'jotai';
* import { withPending } from 'jotai-eager';
*
* const asyncAtom = atom(Promise.resolve('data'));
* const wrappedAtom = withPending(asyncAtom);
* // wrappedAtom returns 'data' when resolved, or undefined while pending
* ```
*/
declare function withPending<Value>(anAtom: Atom<Value>): Atom<Awaited<Value> | undefined>;
/**
* Wraps an atom to handle pending states by returning a fallback value while the atom's value is unresolved, optionally using a custom fallback function.
* Intended as an alternative to Jotai's `unwrap`, providing enhanced pending state handling.
*
* @param anAtom The atom to wrap, which may resolve asynchronously.
* @param fallback A function that receives context (getter, previous value, pending promise) and returns a value to use while pending.
* @returns An atom that returns the resolved value or the fallback while pending.
*
* @example
* ```ts
* import { atom } from 'jotai';
* import { withPending } from 'jotai-eager';
*
* const asyncAtom = atom(Promise.resolve('data'));
* const wrappedAtom = withPending(asyncAtom, () => 'Loading...');
* // wrappedAtom returns 'data' when resolved, or 'Loading...' while pending
* ```
*/
declare function withPending<Value, PendingValue>(anAtom: Atom<Value>, fallback: (ctx: WithPendingContext<Value>) => PendingValue): Atom<Awaited<Value> | PendingValue>;
export { derive, eagerAtom, isEagerError, loadable, soon, soonAll, withPending };