UNPKG

react-async-iterators

Version:

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

158 lines (157 loc) 6.9 kB
"use strict"; 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