UNPKG

react-async-iterators

Version:

The magic of JavaScript async iterators in React ⛓️ 🧬 🔃

198 lines (197 loc) 8.6 kB
import { type IterationResult } from '../useAsyncIter/index.js'; import { type MaybeFunction } from '../common/MaybeFunction.js'; export { useAsyncIterMulti, type IterationResult, type IterationResultSet }; /** * `useAsyncIterMulti` hooks up multiple async iterables to your component and its lifecycle, letting * additional async iterables be added or removed on the go. * * It's similar to `useAsyncIter`, only it works with any changeable number of async iterables or * plain values instead of a single one. * * --- * * _Illustration:_ * * @example * ```tsx * import { useAsyncIterMulti } from 'react-async-iterators'; * * function MyComponent() { * const [nextNum, nextStr, nextArr] = useAsyncIterMulti([numberIter, stringIter, arrayIter], { * initialValues: [0, '', []] * }); * * nextNum.value; // Current value of `numberIter` * nextStr.value; // Current value of `stringIter` * nextArr.value; // Current value of `arrayIter` * nextNum.done; // Whether iteration of `numberIter` ended * nextStr.done; // Whether iteration of `stringIter` ended * nextArr.done; // Whether iteration of `arrayIter` ended * * // ... * } * ``` * * Given an array of async iterables for `inputs`, this hook will iterate over all of them concurrently, * updating (re-rendering) the host component whenever any yields a value, completes, or errors outs - * each time returning an array combining all their current individual states, in correspondence to * their original positions they were given in on `inputs`. * * `inputs` 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. * * The hook initializes and maintains its iteration process with each async iterable object as long as * that same object remains present in `inputs` arrays 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 returned array. * 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 `useAsyncIterMulti` detects that one or more previously present async iterables have * disappeared from the `inputs` array, it will close their iteration processes. * On component unmount, the hook will ensure closing all active iterated async iterables entirely. * * The array returned from `useAsyncIterMulti` contains all the individual most recent states of all * actively iterated objects and/or plain values from the current `inputs` (including each's most recent * value, who's completed, etc. - see {@link IterationResultSet `IterationResultSet`}). * * --- * * @template TValues The array/tuple type of the input set of async iterable or plain values. * @template TInitValues The type of all initial values corresponding to types of `TValues`. * @template TDefaultInitValue The type of the default initial value (the fallback from `TValues`). `undefined` by default. * * @param inputs An array of zero or more async iterable or plain values (mixable). * @param {object} opts An _optional_ object with options. * @param opts.initialValues An _optional_ array of initial values or functions that return initial values, each item of which is a starting value for the async iterable from `inputs` on the same array position. For every async iterable that has no corresponding item here, the provided `opts.defaultInitialValue` will be used as fallback. * @param opts.defaultInitialValue An _optional_ default starting value for every new async iterable in `inputs` if there is no corresponding one for it in `opts.initialValues`, 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). * * @returns An array of objects that provide up-to-date information about each input's current value, completion status, whether it's still waiting for its first value and so on, correspondingly with the order in which they appear on `inputs` (see {@link IterationResultSet `IterationResultSet`}). * * @see {@link IterationResultSet `IterationResultSet`} * * @example * ```tsx * import { useAsyncIterMulti } from 'react-async-iterators'; * * function MyDemo() { * const [currentWords, currentFruits] = useAsyncIterMulti( * [wordGen, fruitGen], * { initialValues: ['', []] } * ); * * return ( * <div> * Current word: * <h2> * {currentWords.pendingFirst * ? 'Loading words...' * : currentWords.error * ? `Error: ${currentWords.error}` * : currentWords.done * ? `Done (last value: ${currentWords.value})` * : `Value: ${currentWords.value}`} * </h2> * * Fruits: * <ul> * {currentFruits.pendingFirst * ? 'Loading fruits...' * : currentFruits.value.map(fruit => ( * <li key={fruit.icon}>{fruit.icon}</li> * ))} * </ul> * </div> * ); * } * * const wordGen = (async function* () { * const words = ['Hello', 'React', 'Async', 'Iterators']; * for (const word of words) { * await new Promise(resolve => setTimeout(resolve, 1250)); * yield word; * } * })(); * * const fruitGen = (async function* () { * const sets = [ * [{ icon: '🍑' }, { icon: '🥭' }, { icon: '🍊' }], * [{ icon: '🍏' }, { icon: '🍐' }, { icon: '🍋' }], * [{ icon: '🍉' }, { icon: '🥝' }, { icon: '🍇' }], * ]; * for (const fruits of sets) { * await new Promise(resolve => setTimeout(resolve, 2000)); * yield fruits; * } * })(); * ``` * * --- * * @example * ```tsx * // Using `useAsyncIterMulti` with a dynamically changing amount of inputs: * * import { useState } from 'react'; * import { useAsyncIterMulti, type MaybeAsyncIterable } from 'react-async-iterators'; * * function DynamicInputsComponent() { * const [inputs, setInputs] = useState<MaybeAsyncIterable<string>[]>([]); * * const states = useAsyncIterMulti(inputs, { defaultInitialValue: '' }); * * 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> * {states.map((state, i) => ( * <li key={i}> * {state.done * ? state.error * ? `Error: ${state.error}` * : 'Done' * : state.pendingFirst * ? 'Pending...' * : `Value: ${state.value}`} * </li> * ))} * </ul> * </div> * ); * } * ``` */ declare function useAsyncIterMulti<const TValues extends readonly unknown[], const TInitValues extends readonly unknown[] = readonly [], const TDefaultInitValue = undefined>(inputs: TValues, opts?: { initialValues?: TInitValues; defaultInitialValue?: TDefaultInitValue; }): IterationResultSet<TValues, MaybeFunctions<TInitValues>, TDefaultInitValue>; type IterationResultSet<TValues extends readonly unknown[], TInitValues extends readonly unknown[] = readonly [], TDefaultInitValue = undefined> = { [I in keyof TValues]: IterationResult<TValues[I], I extends keyof TInitValues ? TInitValues[I] : TDefaultInitValue>; }; type MaybeFunctions<T extends readonly unknown[]> = { [I in keyof T]: T[I] extends MaybeFunction<infer J> ? J : T[I]; };