react-async-iterators
Version:
The magic of JavaScript async iterators in React ⛓️ 🧬 🔃
194 lines (193 loc) • 9.46 kB
TypeScript
import { type ReactNode } from 'react';
import { type Writeable } from '../common/Writeable.js';
import { type IterationResultSet } from '../useAsyncIterMulti/index.js';
import { type MaybeFunction } from '../common/MaybeFunction.js';
export { IterateMulti, type IterateMultiProps };
/**
* The `<IterateMulti>` component (also exported as `<ItMulti>`) is used to combine and render
* any number of async iterables (or plain non-iterable values) directly onto a piece of UI.
*
* It's similar to `<Iterate>`, only it works with any changeable number of async iterables or plain values
* instead of a single one. Essentially, can be seen as a {@link useAsyncIterMulti `useAsyncIterMulti`}
* hook in a component form, conveniently.
*
* ---
*
* _Illustration:_
*
* @example
* ```tsx
* import { useMemo } from 'react';
* import { ItMulti } from 'react-async-iterators';
*
* function MyComponent() {
* const numberIter = useMemo(() => createNumberIter(), []);
* const arrayIter = useMemo(() => createArrayIter(), []);
* return (
* <main>
* <Header />
* <SideMenu />
* <ItMulti values={[numberIter, arrayIter]} initialValues={[0, []]}>
* {([numState, arrState]) => (
* <>
* <div>
* {numState.pendingFirst
* ? '⏳ Loading number...'
* : `Current number: ${numState.value}`}
* </div>
* <div>
* {arrState.pendingFirst
* ? '⏳ Loading items...'
* : arrState.value.map((item, i) => <div key={i}>{item}</div>)}
* </div>
* </>
* )}
* </ItMulti>
* </main>
* )
* }
* ```
*
* `<IterateMulti>` may be preferable over the {@link useAsyncIterMulti `useAsyncIterMulti`} counterpart
* typically as the UI area it re-renders within a component tree can be expressly confined to the
* necessary minimum, saving any other unrelated elements from re-evaluation - while on the other hand,
* {@link useAsyncIterMulti `useAsyncIterMulti`} being a hook it has to re-render an entire component's
* output for every new value.
*
* Given an array of source async iterables (or plain values) as the `values` prop, this hook will start
* iterating each of them concurrently, re-rendering every time any source yields a value, completes, or
* errors outs. This will run the render function provided as children to `<IterateMulti>` with an array
* argument that includes all latest individual states of current sources.
*
* `values` may also be mixed with plain (non async iterable) values, in which case they'll simply be
* returned as-are, coinciding along current values of other async iterables.
* This can enable components that can work seamlessly with either _"static"_ and _"changing"_ values
* and props.
*
* Semantics are similar with `<Iterate>` - the component maintains its iteration process with each async
* iterable object as long as that same object remains present in the arrays passed to the `values` prop
* across subsequent updates. Changing the position of such object in the array on a consequent call will
* __not__ close its current running iteration - it will only change the position its result appears at
* in the array argument passed into the render function.
* Care should be taken therefore to not unintentionally recreate the given iterables on every render,
* by e.g; declaring an iterable outside the component body, controling __when__ it should be recreated
* with React's [`useMemo`](https://react.dev/reference/react/useMemo) or preferably use the library's
* {@link iterateFormatted `iterateFormatted`} util for formatting an iterable's values while preserving
* its identity.
*
* Whenever `<IterateMulti>` is updated with a new inputs array and detects that one or more async
* iterables from the previously given array are no longer present, it will close their iteration
* processes. On component unmount, the hook will ensure closing all active iterated async iterables
* entirely.
*
* ---
*
* @template TVals The type of the input set of async iterable or plain values as an array/tuple.
* @template TInitVals The type of all initial values for each of the input values as an array/tuple, corresponding by order.
* @template TDefaultInitVal The type of the default initial value (the fallback from `TValues`). `undefined` by default.
*
* @param props Props for `<IterateMulti>`. See {@link IterateMultiProps `IterateMultiProps`}.
*
* @returns A React node that re-renders itself whenever any of the source inputs change's state, and formatted by the child render function passed into `children`.
*
* @see {@link IterationResultSet `IterationResultSet`}
*
* @example
* ```tsx
* // Using `<ItMulti>` with a dynamically changing amount of inputs:
*
* import { useState } from 'react';
* import { ItMulti, type MaybeAsyncIterable } from 'react-async-iterators';
*
* function DynamicInputsComponent() {
* const [inputs, setInputs] = useState<MaybeAsyncIterable<string>[]>([]);
*
* const addAsyncIterValue = () => {
* const iterableValue = (async function* () {
* for (let i = 0; i < 10; i++) {
* await new Promise(resolve => setTimeout(resolve, 500));
* yield `Item ${i}`;
* }
* })();
* setInputs(prev => [...prev, iterableValue]);
* };
*
* const addStaticValue = () => {
* const staticValue = `Static ${inputs.length + 1}`;
* setInputs(prev => [...prev, staticValue]);
* };
*
* return (
* <div>
* <h3>Dynamic Concurrent Async Iteration</h3>
*
* <button onClick={addAsyncIterValue}>🔄 Add Async Iterable</button>
* <button onClick={addStaticValue}>🗿 Add Static Value</button>
*
* <ul>
* <ItMulti values={inputs} defaultInitialValue="">
* {states =>
* states.map((state, i) => (
* <li key={i}>
* {state.done
* ? state.error
* ? `Error: ${state.error}`
* : 'Done'
* : state.pendingFirst
* ? 'Pending...'
* : `Value: ${state.value}`}
* </li>
* ))
* }
* </ItMulti>
* </ul>
* </div>
* );
* }
* ```
*/
declare function IterateMulti<const TVals extends readonly unknown[], const TInitVals extends readonly unknown[] = readonly [], const TDefaultInitVal = undefined>(props: IterateMultiProps<TVals, MaybeFunctions<TInitVals>, TDefaultInitVal>): ReactNode;
/**
* Props for the {@link IterateMulti `<IterateMulti>`} component.
*
* @template TVals The type of the input set of async iterable or plain values as an array/tuple.
* @template TInitVals The type of initial values for each of the input values as an array/tuple, corresponding by order. For input values which don't have a corrsponding initial value, the default is `undefined`.
* @template TDefaultInitVal The type of the default initial value (the fallback from `TVals`). `undefined` by default.
*/
type IterateMultiProps<TVals extends readonly unknown[], TInitVals extends readonly unknown[] = readonly [], TDefaultInitVal = undefined> = {
/**
* An array of values to iterate over simultaneously, which may include any mix of async iterables or
* plain (non async iterable) values.
*/
values: TVals;
/**
* An _optional_ array of initial values or functions that return initial values. These values
* will be the starting points for all the async iterables from `values` (correspondingly by
* matching array positions) when they are rendered by the `children` render function for the
* first time and for each while it is pending its first yield. Async iterables from `values`
* that have no corresponding item in this array, will fall back to the
* {@link IterateMultiProps.defaultInitialValue `defaultInitialValue`} prop as the initial value.
*/
initialValues?: TInitVals;
/**
* An _optional_ default starting value for every new async iterable in `values` if there is no
* corresponding one for it in the `initialValues` prop, defaults to `undefined`. You can pass
* an actual value, or a function that returns a value (which the hook will call for every new
* iterable added).
*/
defaultInitialValue?: TDefaultInitVal;
/**
* A render function that is called on every progression in any of the running iterations, returning
* something to render for them.
*
* @param iterationStates - An array of the combined iteration state objects of all sources given with the `values` prop, which includes each source's last yielded value, whether completed, any associated error, etc. Each object is corresponding to the source on the `values` array in the same the position (array index). (see {@link IterationResultSet `IterationResultSet`}).
* @returns The content to render for the current iteration state.
*
* @see {@link IterateMultiProps `IterateMultiProps`}
* @see {@link IterationResultSet `IterationResultSet`}
*/
children: (iterationStates: IterationResultSet<Writeable<TVals>, Writeable<TInitVals>, TDefaultInitVal>) => ReactNode;
};
type MaybeFunctions<T extends readonly unknown[]> = {
[I in keyof T]: T[I] extends MaybeFunction<infer J> ? J : T[I];
};