react-async-iterators
Version:
The magic of JavaScript async iterators in React ⛓️ 🧬 🔃
158 lines (157 loc) • 6.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.useAsyncIter = void 0;
const react_1 = require("react");
const useLatest_js_1 = require("../common/hooks/useLatest.js");
const isAsyncIter_js_1 = require("../common/isAsyncIter.js");
const useSimpleRerender_js_1 = require("../common/hooks/useSimpleRerender.js");
const useRefWithInitialValue_js_1 = require("../common/hooks/useRefWithInitialValue.js");
const ReactAsyncIterable_js_1 = require("../common/ReactAsyncIterable.js");
const iterateAsyncIterWithCallbacks_js_1 = require("../common/iterateAsyncIterWithCallbacks.js");
const callOrReturn_js_1 = require("../common/callOrReturn.js");
/**
* `useAsyncIter` hooks up a single async iterable value to your component and its lifecycle.
*
* @example
* ```tsx
* import { useAsyncIter } from 'react-async-iterators';
*
* function SelfUpdatingTodoList(props) {
* const { value: todos } = useAsyncIter(props.todosAsyncIter);
* return (
* <ul>
* {todos?.map(todo => (
* <li key={todo.id}>{todo.text}</li>
* ))}
* </ul>
* );
* }
* ```
*
* ---
*
* Given an async iterable `input`, this hook will iterate it value-by-value and update (re-render) the
* host component upon each yielded value, along with any possible completion or error it may run into.
* `input` may also be given a plain (non async iterable) value, in which case it will simply be used
* to render once and immediately, thus enabling components that can handle _"static"_ as well as
* _"changing"_ values and props seamlessly.
*
* The hook initializes and maintains its iteration process with its given async iterable `input`
* across component updates as long as `input` keeps getting passed the same object reference every
* time (similar to the behavior of a `useEffect(() => {...}, [input])`), therefore care should be taken
* to avoid constantly recreating the iterable on every render, by e.g; declaring it outside the component
* body, controlling __when__ it should be recreated with React's
* [`useMemo`](https://react.dev/reference/react/useMemo) or alternatively use the library's
* {@link iterateFormatted `iterateFormatted`} util for a formatted version of an iterable which
* preserves its identity.
* Whenever `useAsyncIter` detects a different `input` value, it automatically closes the previous
* active async iterable and proceeds to start iteration with the new `input` async iterable. On
* component unmount, the hook will also ensure closing any currently iterated `input`.
*
* The object returned from `useAsyncIter` holds all the state from the most recent iteration
* of `input` (most recent value, whether is completed or still running, etc. - see
* {@link IterationResult `IterationResult`}).
* In case `input` is given a plain value, it will be delivered as-is within the returned
* result object's `value` property.
*
* ---
*
* @template TVal The type of values yielded by the passed iterable or type of plain value if otherwise passed.
* @template TInitVal The type of the initial value, defaults to `undefined`.
*
* @param input Any async iterable or plain value.
* @param initialVal Any optional starting value for the hook to return prior to the ___first yield___ of the ___first given___ async iterable, defaults to `undefined`. You can pass an actual value, or a function that returns a value (which the hook will call once during mounting).
*
* @returns An object with properties reflecting the current state of the iterated async iterable or plain value provided via `input` (see {@link IterationResult `IterationResult`}).
*
* @see {@link IterationResult `IterationResult`}
*
* @example
* ```tsx
* // With an `initialVal` and showing usage of all properties of the returned iteration object:
*
* import { useAsyncIter } from 'react-async-iterators';
* function SelfUpdatingTodoList(props) {
* const todosNext = useAsyncIter(props.todosAsyncIter, []);
* return (
* <>
* {todosNext.error ? (
* <div>An error was encountered: {todosNext.error.toString()}</div>
* ) : todosNext.done && (
* <div>No additional updates for todos are expected</div>
* )}
*
* {todosNext.pendingFirst ? (
* <div>Loading first todos...</div>
* ) : (
* <ul>
* {todosNext.map(todo => (
* <li key={todo.id}>{todo.text}</li>
* ))}
* </ul>
* )}
* </>
* );
* }
* ```
*/
const useAsyncIter = (input, initialVal) => {
const rerender = (0, useSimpleRerender_js_1.useSimpleRerender)();
const stateRef = (0, useRefWithInitialValue_js_1.useRefWithInitialValue)(() => ({
value: (0, callOrReturn_js_1.callOrReturn)(initialVal),
pendingFirst: true,
done: false,
error: undefined,
}));
const latestInputRef = (0, useLatest_js_1.useLatest)(input);
if (!(0, isAsyncIter_js_1.isAsyncIter)(latestInputRef.current)) {
(0, react_1.useMemo)(() => { }, [undefined]);
(0, react_1.useEffect)(() => { }, [undefined]);
stateRef.current = {
value: latestInputRef.current,
pendingFirst: false,
done: false,
error: undefined,
};
return stateRef.current;
}
const iterSourceRefToUse = latestInputRef.current[ReactAsyncIterable_js_1.reactAsyncIterSpecialInfoSymbol]?.origSource ?? latestInputRef.current;
(0, react_1.useMemo)(() => {
const latestInputRefCurrent = latestInputRef.current;
let value;
let pendingFirst;
if (latestInputRefCurrent.value) {
value = latestInputRefCurrent.value.current;
pendingFirst = false;
}
else {
const prevSourceLastestVal = stateRef.current.value;
value = prevSourceLastestVal;
pendingFirst = true;
}
stateRef.current = {
value,
pendingFirst,
done: false,
error: undefined,
};
}, [iterSourceRefToUse]);
(0, react_1.useEffect)(() => {
let iterationIdx = 0;
return (0, iterateAsyncIterWithCallbacks_js_1.iterateAsyncIterWithCallbacks)(iterSourceRefToUse, stateRef.current.value, next => {
const possibleGivenFormatFn = latestInputRef.current?.[ReactAsyncIterable_js_1.reactAsyncIterSpecialInfoSymbol]?.formatFn;
const formattedValue = possibleGivenFormatFn
? possibleGivenFormatFn(next.value, iterationIdx++)
: next.value;
stateRef.current = {
...next,
pendingFirst: false,
value: formattedValue,
};
rerender();
});
}, [iterSourceRefToUse]);
return stateRef.current;
};
exports.useAsyncIter = useAsyncIter;
//# sourceMappingURL=index.js.map