react-async-iterators
Version:
The magic of JavaScript async iterators in React ⛓️ 🧬 🔃
290 lines • 12.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.useAsyncIterMulti = useAsyncIterMulti;
const react_1 = require("react");
const useSimpleRerender_js_1 = require("../common/hooks/useSimpleRerender.js");
const useRefWithInitialValue_js_1 = require("../common/hooks/useRefWithInitialValue.js");
const isAsyncIter_js_1 = require("../common/isAsyncIter.js");
const callOrReturn_js_1 = require("../common/callOrReturn.js");
const asyncIterSyncMap_js_1 = require("../common/asyncIterSyncMap.js");
const ReactAsyncIterable_js_1 = require("../common/ReactAsyncIterable.js");
const iterateAsyncIterWithCallbacks_js_1 = require("../common/iterateAsyncIterWithCallbacks.js");
// TODO: The initial values should be able to be given in function/s form, with consideration for iterable sources that could be added in dynamically.
/**
* `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>
* );
* }
* ```
*/
function useAsyncIterMulti(inputs, opts) {
const update = (0, useSimpleRerender_js_1.useSimpleRerender)();
const ref = (0, useRefWithInitialValue_js_1.useRefWithInitialValue)(() => ({
currDiffCompId: 0,
prevResults: [],
activeItersMap: new Map(),
}));
const { prevResults, activeItersMap } = ref.current;
(0, react_1.useEffect)(() => {
return () => {
for (const it of activeItersMap.values()) {
it.destroy();
}
};
}, []);
const optsNormed = {
initialValues: opts?.initialValues ?? [],
defaultInitialValue: opts?.defaultInitialValue,
};
const nextDiffCompId = (ref.current.currDiffCompId = ref.current.currDiffCompId === 0 ? 1 : 0);
let numOfPrevRunItersPreserved = 0;
const numOfPrevRunIters = activeItersMap.size;
const nextResults = inputs.map((input, i) => {
if (!(0, isAsyncIter_js_1.isAsyncIter)(input)) {
return {
value: input,
pendingFirst: false,
done: false,
error: undefined,
};
}
const { baseIter, formatFn } = (0, ReactAsyncIterable_js_1.parseReactAsyncIterable)(input);
const existingIterState = activeItersMap.get(baseIter);
if (existingIterState) {
numOfPrevRunItersPreserved++;
existingIterState.diffCompId = nextDiffCompId;
existingIterState.formatFn = formatFn;
return existingIterState.currState;
}
const formattedIter = (() => {
let iterationIdx = 0;
return (0, asyncIterSyncMap_js_1.asyncIterSyncMap)(baseIter, value => newIterState.formatFn(value, iterationIdx++));
})();
const inputWithMaybeCurrentValue = input;
let startingValue;
let pendingFirst;
if (inputWithMaybeCurrentValue.value) {
startingValue = inputWithMaybeCurrentValue.value.current;
pendingFirst = false;
}
else {
startingValue =
i < prevResults.length
? prevResults[i].value
: (0, callOrReturn_js_1.callOrReturn)(i < optsNormed.initialValues.length
? optsNormed.initialValues[i]
: optsNormed.defaultInitialValue);
pendingFirst = true;
}
const destroyFn = (0, iterateAsyncIterWithCallbacks_js_1.iterateAsyncIterWithCallbacks)(formattedIter, startingValue, next => {
newIterState.currState = { pendingFirst: false, ...next };
update();
});
const newIterState = {
diffCompId: nextDiffCompId,
destroy: destroyFn,
formatFn,
currState: {
value: startingValue,
pendingFirst,
done: false,
error: undefined,
},
};
activeItersMap.set(baseIter, newIterState);
return newIterState.currState;
});
const numOfPrevRunItersDisappeared = numOfPrevRunIters - numOfPrevRunItersPreserved;
if (numOfPrevRunItersDisappeared > 0) {
let i = 0;
for (const { 0: iter, 1: state } of activeItersMap) {
if (state.diffCompId !== nextDiffCompId) {
activeItersMap.delete(iter);
state.destroy();
if (++i === numOfPrevRunItersDisappeared) {
break;
}
}
}
}
ref.current.prevResults = nextResults;
return nextResults;
}
//# sourceMappingURL=index.js.map