@@ -3,6 +3,105 @@ import { useAsyncIter, type IterationResult } from '../useAsyncIter/index.js';
33
44export { Iterate , type IterateProps } ;
55
6+ /**
7+ * The `<Iterate>` helper component is used to format and render an async iterable (or a plain non-iterable value)
8+ * directly onto a piece of UI.
9+ *
10+ * Essentially wraps a single {@link useAsyncIter `useAsyncIter`} hook call into a component
11+ * conveniently.
12+ *
13+ * _Illustration:_
14+ *
15+ * ```tsx
16+ * import { Iterate } from 'react-async-iterators';
17+ *
18+ * function SelfUpdatingTodoList(props) {
19+ * return (
20+ * <div>
21+ * <h2>My TODOs</h2>
22+ *
23+ * <div>
24+ * Last TODO was completed at: <Iterate>{props.lastCompletedTodoDate}</Iterate>
25+ * </div>
26+ *
27+ * <ul>
28+ * <Iterate value={props.todosAsyncIter}>
29+ * {({ value: todos }) =>
30+ * todos?.map(todo =>
31+ * <li key={todo.id}>{todo.text}</li>
32+ * )
33+ * }
34+ * </Iterate>
35+ * </ul>
36+ * </div>
37+ * );
38+ * }
39+ * ```
40+ *
41+ * `<Iterate>` may be preferable over {@link useAsyncIter `useAsyncIter`} typically as the UI area it
42+ * controls the rendering for is constrainable down to the essential, saving some React elements from
43+ * unnecessary re-renderings while placable clearly and elegantly within some larger component's UI
44+ * output. In regard to {@link useAsyncIter `useAsyncIter`} being a hook though, it has to
45+ * re-render the entire component output for every new value.
46+ *
47+ * Given an async iterable as the `value` prop, this component will iterate it and render each new
48+ * value that becomes available together with any possible completion or error it may run into.
49+ * If `value` is a plain (non async iterable) value, it will simply be rendered over as-is.
50+ *
51+ * Whenever given `value` is changed from the previous one seen, `<Iterate>` will close the previous
52+ * if it was async iterable before proceeding to iterate the new `value`. Care should be taken to
53+ * avoid passing a constantly recreated iterable object across re-renders, e.g; by declaring it outside the component body or control __when__ it
54+ * should be recreated with React's [`useMemo`](https://react.dev/reference/react/useMemo).
55+ * `<Iterate>` will automatically close its iterated iterable as soon as it gets unmounted.
56+ *
57+ * @template TVal The type of values yielded by the passed iterable or otherwise type of the passed plain value itself.
58+ * @template TInitialVal The type of the initial value, defaults to `undefined`.
59+ *
60+ * @param props Props for `<Iterate>`. See {@link IterateProps `IterateProps`}.
61+ *
62+ * @returns A renderable output that's re-rendered as consequent values become available and
63+ * formatted by the function passed as `children` (or otherwise the resolved values as-are).
64+ *
65+ * @see {@link IterationResult }
66+ *
67+ * @example
68+ * ```tsx
69+ * // With the `initialValue` prop and showing usage of all properties of the iteration object
70+ * // within the child render function:
71+ *
72+ * import { Iterate } from 'react-async-iterators';
73+ *
74+ * function SelfUpdatingTodoList(props) {
75+ * return (
76+ * <div>
77+ * <h2>My TODOs</h2>
78+ *
79+ * <Iterate initialValue={[]} value={props.todosAsyncIter}>
80+ * {todosNext =>
81+ * todosNext.pendingFirst ? (
82+ * <div>Loading first todos...</div>
83+ * ) : (
84+ * <>
85+ * {todosNext.error ? (
86+ * <div>An error was encountered: {todosNext.error.toString()}</div>
87+ * ) : (
88+ * todosNext.done && <div>No additional updates for todos are expected</div>
89+ * )}
90+ *
91+ * <ul>
92+ * {todosNext.map(todo => (
93+ * <li key={todo.id}>{todo.text}</li>
94+ * ))}
95+ * </ul>
96+ * </>
97+ * )
98+ * }
99+ * </Iterate>
100+ * </div>
101+ * );
102+ * }
103+ * ```
104+ */
6105function Iterate < TVal , TInitialVal = undefined > ( props : IterateProps < TVal , TInitialVal > ) : ReactNode {
7106 const renderOutput =
8107 typeof props . children === 'function'
@@ -12,26 +111,62 @@ function Iterate<TVal, TInitialVal = undefined>(props: IterateProps<TVal, TIniti
12111 return propsBetterTyped . children ( next ) ;
13112 } ) ( )
14113 : ( ( ) => {
15- const propsBetterTyped = props as IteratePropsWithIterableAsChildren ;
114+ const propsBetterTyped = props as IteratePropsWithNoRenderFunction ;
16115 const next = useAsyncIter ( propsBetterTyped . children , propsBetterTyped . initialValue ) ;
17116 return next . value ;
18117 } ) ( ) ;
19118
20119 return renderOutput ;
21120}
22121
122+ /**
123+ * Props for the {@link Iterate `<Iterate>`} component.
124+ * The component accepts its props in two variants:
125+ *
126+ * 1. Providing a render function as `children` to dynamically format each state of the iteration.
127+ * 2. Providing an async iterable as `children` to render the values of the async iterable (or plain value) directly as are.
128+ *
129+ * @template TVal The type of values yielded by the passed iterable or otherwise type of the passed plain value itself.
130+ * @template TInitialVal The type of the initial value, defaults to `undefined`.
131+ */
23132type IterateProps < TVal , TInitialVal = undefined > =
24133 | IteratePropsWithRenderFunction < TVal , TInitialVal >
25- | IteratePropsWithIterableAsChildren ;
134+ | IteratePropsWithNoRenderFunction ;
26135
27136type IteratePropsWithRenderFunction < TVal , TInitialVal = undefined > = {
28- initialValue ?: TInitialVal ;
137+ /**
138+ * The source value to iterate over, an async iterable or a plain (non async iterable) value.
139+ */
29140 value : TVal ;
141+ /**
142+ * An optional initial value, defaults to `undefined`.
143+ */
144+ initialValue ?: TInitialVal ;
145+ /**
146+ * A render function that is called for each iteration state and returns something to render
147+ * out of it.
148+ *
149+ * @param nextIterationState - The current state of the iteration, including the yielded value, whether iteration is complete, any associated error, etc. (see {@link IterationResult `IterationResult`})
150+ * @returns The content to render for the current iteration state.
151+ *
152+ * @see {@link IterateProps `IterateProps` }
153+ * @see {@link IterationResult `IterationResult` }
154+ */
30155 children : ( nextIterationState : IterationResult < TVal , TInitialVal > ) => ReactNode ;
31156} ;
32157
33- type IteratePropsWithIterableAsChildren = {
34- initialValue ?: ReactNode ;
158+ type IteratePropsWithNoRenderFunction = {
159+ /**
160+ * The `value` prop source value should not be provided for this variant since it is already
161+ * passed via `children` (see {@link IterateProps `IterateProps`}).
162+ */
35163 value ?: undefined ;
164+ /**
165+ * An optional initial value, defaults to `undefined`.
166+ */
167+ initialValue ?: ReactNode ;
168+ /**
169+ * The source value to render from, either an async iterable to iterate over of a plain value.
170+ */
36171 children : ReactNode | AsyncIterable < ReactNode > ;
37172} ;
0 commit comments