Skip to content

React.js hooked up with the magic of JavaScript async iterators: hooks · components · utilities ⚛️ ⛓️ 🧬

License

Notifications You must be signed in to change notification settings

shtaif/react-async-iterators

Repository files navigation

React Async Iterators

Hooks, components and utilities for working with JavaScript async iterator values in React.js.


npm published version semantic-release

Async iterables/iterators are a native language construct in JS that can be viewed as a counterpart to Promise, in the sense that while a promise asynchronously resolves one value - an async iterable is a stream that asynchronously yields any number of values.

Somewhat obvious to say, the React ecosystem features many methods and tools that have to do with integrating promise-based data into your React components; from higher level SDK libraries, state managers - to generic async utilities, which make the different promise states available to the rendering. And just like that - react-async-iterators packs hooks, components and utilities written in TypeScript with the aim to make async iterables into first-class citizens to React as they become gradually more prevalent across the JavaScript platform.

What can react-async-iterators be used for?

  • easily consuming async iterables obtained from any library, web API or composed manually - in a React-friendly declarative fashion.
  • unlocking new ways of expressing data flow in or between components efficiently, constricting redundant re-rendering.

Illustration:

Open in StackBlitz

import { It } from 'react-async-iterators';

const randoms = {
  async *[Symbol.asyncIterator]() {
    while (true) {
      await new Promise((r) => setTimeout(r, 500));
      const x = Math.random();
      yield Math.round(x * 10);
    }
  },
};

// and then:

<It>{randoms}</It>

// renders: '2'... '1'... '3'... etc.

// OR:

<It value={randoms}>
  {next => (
    next.pendingFirst
      ? '⏳ Loading first...'
      : <p>{next.value?.toExponential()}</p>
  )}
</It>

// renders:
//   '⏳ Loading first...'
//   <p>2e+0</p>...
//   <p>1e+0</p>...
//   <p>3e+0</p>...
//   etc.

Highlights

✔️ Fully written in TypeScript with comprehensive inferring typings
✔️ Fully tree-shakeable exports
✔️ Light weight, zero run-time dependencies
✔️ ESM build
✔️ Semver compliant

Table of Contents

Installation

# With npm:
npm i react-async-iterators

# With pnpm:
pnpm i react-async-iterators

# With Yarn:
yarn add react-async-iterators

Can then be imported as follows (TypeScript/ESM style):

import { It, type IterationResult } from 'react-async-iterators';

Walkthrough

Consuming async iterables

Async iterables can be hooked into your components and consumed using <It> and <ItMulti>, or their hook counterparts useAsyncIter and useAsyncIterMulti respectively.

The iteration values and states are expressed via a consistent structure (see exaustive list in this breakdown).
They may be accessed as follows:

const myIter = getSomeIter(); // given some `myIter` async iterable

With <It>:

import { It } from 'react-async-iterators';

<It value={myIter} initialValue="first_value">
  {next => {
    next.pendingFirst; /* -> whether we're still waiting for the first value yielded
                             from `myIter`, analogous to a promise's pending state. */

    next.value; /* -> the most recent value yielded. If `pendingFirst` is `true`,
                      we should see the last value carried over from the previous
                      iterable before `myIter` (otherwise fall back to "first_value"
                      if we've just been mounted) */

    next.done; /* -> whether the iteration of `myIter` has finished (will yield no
                     further values) */
    
    next.error; /* -> if the iterated async iterable threw an error, this will be set
                      to it along with `done` showing `true` */
  }}
</It>

With useAsyncIter:

import { useAsyncIter } from 'react-async-iterators';

const next = useAsyncIter(myIter, 'first_value');

// (Properties are identical to the above...)
next.pendingFirst;
next.value;
next.done;
next.error;

Using the component form may be typically preferrable over the hook form (e.g <It> over useAsyncIter) - Why? because using it, when changes in data occure - the re-rendered UI area within a component tree can be declaratively narrowed to the necessary minimum, saving other React elements that do not depend on its values from re-evaluation. On the the other hand - useAsyncIter, being a hook, must re-render the entirety of the host component's output for every new value.

When segregating different flows of data the components' render code like this, using the component forms - it makes for a more managable code, and might get rid of having to akwardly split components down to smaller parts just to render-optimize them when it otherwise wouldn't "feel right" to do so.

Plain values

All of the consuming hooks and components also accept plain ("non-iterable") values safely, rendering them as-are with very low extra overhead - their inputs may alternate between async iterable and plain values any time.

When rendering a plain value, the iteration state properties behave alternatively like so:

  • .value reflects the plain value as-is
  • .pendingFirst and .done are ALWAYS false
  • .error is ALWAYS empty


ℹ️ When providing a plain value right upon mounting - the initial value, if given, is ignored.


Showing <It> being used with either plain or async iterable values, wrapped as a custom component:

import { It, type MaybeAsyncIterable } from 'react-async-iterators';

function Foo(props: {
  value: MaybeAsyncIterable<string>;
}) {
  return (
    <It value={props.value}>
      {next => (
        /* ... */
      )}
    </It>
  );
}

// Later:

<Foo value="my_value" />
// or:
<Foo value={myStringIter} />


⬆️ Note use of the MaybeAsyncIterable convenience type


One use for this among others is an ability for a certain async-iterable-fed UI piece to be pushed some alternative "placeholder" value at times there isn't an actual async iterable available to feed it with.

Another implication of this conveniency is the possibility to design apps and component libraries that can receive data expressable in both "static" and "changing" fashions - seamlessly. If a certain component has to be given a string value prop, but you happen to (or wish to) only have an async iterable of strings at hand - why shouldn't you be able to pass just that onto the same prop and it would just work as expected - self updating whenever the next string is yielded? Async iterables are standard JavaScript after all.

Iteration lifecycle

When rendering an async iterable with any of the consuming component/hooks, they immediately begin iterating through it value-by-value.

The current active iteration is always associated to the particular value that was given into the consumer component or hook, such that re-rendering the consumer again and again with a reference to the same object will keep the same active iteration running persistingly in a React-like fashion (similar to React.useEffect not re-running until its dependencies are changed).

Whenever the consumer receives a new value to iterate, it will immediately dispose of any current running iteration (calling .return() on its held iterator) and proceed iterating the new value in the same manner described.

Finally, when the consumer is unmounted, any current running iteration is disposed of as well.

Lifecycle phases

The following phases and state properties are reflected via all consumer utilities (with hooks - returned, with components - injected to their given render functions):

Phase Description

1. Initial phase;

a single occurrence of:

{
  pendingFirst: true,
  value: INITIAL_VALUE,
  done: false,
  error: undefined
}

...if input is an async iterable with no current value -
Otherwise, phase is skipped.

⬇️

2. Yields phase;

...zero or more rounds of:

{
  pendingFirst: false,
  value: VALUE,
  done: false,
  error: undefined
},
// ...
// ...
⬇️

3. Ending phase;

{
  pendingFirst: false,
  value: PREVIOUS_RECENT_VALUE,
  done: true,
  error: POSSIBLE_ERROR
}

with error property being non-empty - ending due to source throwing an error
or
with error property being undefined - ending due to completion - source is done.

🔃 Repeat on change of source value 🔃

Async iterables with current values

Throughout the library there is a specially recognized case (or convention) for expressing async iterables with a notion of a "current value". These are simply defined as any regular async iterable object coupled with a readable .value.current property.

When any consumer hook/component from the library detects the presence of a current value (.value.current) on an async iterable, it can render it immediately and skip the isPending: true phase, since it effectively signals there is no need to wait for a first yield - the value is available already.

This rule bridges the gap between async iterables which always yield asynchronously (as their yields are wrapped in promises) and React's component model in which render outputs are strictly synchronous. Due to this discrepency, for example, if the first value for an async iterable is known in advance and yielded as soon as possible - React could only grab the yielded value from it via a subsequent (immediate) run/render of the consumer hook/component (since the promise can resolve only after such initial sync run/render). This issue is therefore solved by async iterables that expose a current value.

For example, the stateful iterable created from the useAsyncIterState hook (see Component state as an async iterable) applies this convention from its design, acting like a "topic" with an always-available current value that's able to signal out future changes, skipping pending phases, so there's no need to set initial starting states.

Formatting values

When building your app with components accepting async iterable data as props, as you render these and have to provide such props - you may commonly see a need to re-format held async iterables' value shapes before they're passed in those props, in order for them to match the expected shape. iterateFormatted is an easy-to-use approach to many cases like this.

For instance, let's say we're trying to use some existing <Select> generic component, which supports being given its option list in async iterable form, so it could update its rendered dropdown in real-time as new sets of options are yielded. It is used like so;

<Select
  options={
    // EXPECTING HERE AN ASYNC ITER YIELDING:
    // {
    //   value: string;
    //   label: string;
    // }
  }
/>

Now, we would like to populate <Select>'s dropdown with some currency options from an async iterable like this one:

const currenciesIter = getAvailableCurrenciesIter();
// THIS YIELDS OBJECTS OF:
// {
//   isoCode: string;
//   name: string;
// }

As apparent, the value types between these two are not compatible (properties are not matching).

By using iterateFormatted, our source iterable can be formatted/transformed to fit like so:

const currenciesIter = getAvailableCurrenciesIter();

function MyComponent() {
  return (
    <div>
      <Select
        options={iterateFormatted(currenciesIter, ({ isoCode, name }) => ({
          value: isoCode,
          label: `${name} (${isoCode})`
        }))}
      />
    </div>
  );
}

Alternatively, such transformation can be also achieved (entirely legitimately) with help from React.useMemo and some generic mapping operator like iter-tools's asyncMap, among the multitude of available operators from such libraries:

import { useMemo } from 'react';
import { execPipe as pipe, asyncMap } from 'iter-tools';

function MyComponent() {
  const formattedCurrenciesIter = useMemo(
    () =>
      pipe(
        getAvailableCurrenciesIter(),
        asyncMap(({ isoCode, name }) => ({
          value: isoCode,
          label: `${name} (${isoCode})`
        }))
      ),
    []
  );

  return (
    <div>
      <Select options={formattedCurrenciesIter} />
    </div>
  );
}


ℹ️ As seen above, unless you require some more elaborate transformation than simply formatting values - it might be more ergonomic to use iterateFormatted vs manual compositions within React.useMemos - especially if you're having to transform multiple iterables.

Every call to iterateFormatted returns a formatted versions of currenciesIter with some transparent metadata, which the library's consumers (like <It>) use to associate every transformed iterable with its original source iterable, and this way existing iteration states can be persisted properly. It's therefore safe to recreate and pass on formatted iterables from repeated calls to iterateFormatted across re-renders (as long the same source is used with it consistently).

Component state as an async iterable

As illustrated throughout this library and docs - when dealing with data in your app that's presented as an async iterable, an interesting pattern emerges; instead of a transition in app state traditionally sending down a cascading re-render through the entire tree of components underneath it to propagate the new state - your async iterable objects can be distributed once when the whole tree is first mounted, and when new data is then communicated through them it directly gets right to the edges of the UI tree that are concerned with it, re-rendering them exclusively and thus skipping all intermediaries.

The packaged useAsyncIterState hook can lend this paradigm to your component state. It's like a React.useState version that returns you an async iterable of the state value instead of the state value, paired with a setter function that causes the stateful iterable to yield the next states.

The stateful iterable may be distributed via props down through any number of component levels the same way you would with classic React state, and used in conjunction with <It> or useAsyncIter, etc. wherever it has to be rendered.

In a glance, it's usage looks like this:

import { useAsyncIterState, It } from 'react-async-iterators';

function MyCounter() {
  const [countIter, setCount] = useAsyncIterState(0);

  function handleIncrement() {
    setCount(count => count + 1);
  }

  return (
    <>
      Current count: <It>{countIter}</It> {/* <- this is the ONLY thing re-rendering here! */}
      <button onClick={handleIncrement}>Increment</button>
    </>
  );
}

The stateful iterable let's you directly access the current state any time via its .value.current property (see Async iterables with current values) so you may read it when you need to get only the current state alone, for example - as part of a certain side effect logic;

// Using the state iterable's `.value.current` property to read the immediate current state:

import { useAsyncIterState, It } from 'react-async-iterators';

function MyForm() {
  const [firstNameIter, setFirstName] = useAsyncIterState('');
  const [lastNameIter, setLastName] = useAsyncIterState('');

  return (
    <form
      onSubmit={() => {
        const firstName = firstNameIter.value.current;
        const lastName = lastNameIter.value.current;
        // submit `firstName` and `lastName`...
      }}
    >
      Greetings, <It>{firstNameIter}</It> <It>{lastNameIter}</It>

      {/* More content... */}
    </form>
  );
}

API

Iteration state properties breakdown

The following iteration state properties are common for all consumer utilities, with hooks - returned, with components - injected to their given render functions.

As detailed below, the types of these properties are affected by particular phases of the iteration process - see Lifecycle phases for more info.

Property Description
.pendingFirst Boolean indicating whether we're still waiting for the first value to yield.
Can be considered analogous to the promise pending state.

** Is always false if source is a plain value instead of an async iterable.
.value The most recent value yielded.
If we've just started consuming the current iterable (while pendingFirst is true), the last value from a prior iterable would be carried over. If there is no prior iterable (the hook/component had just been mounted) - this will be set to the provided initial value (undefined by default).

** If source is otherwise a plain value and not an async iterable - this will be itself.
.done Boolean indicating whether the async iterable's iteration has ended, having no further values to yield.
This means either of:
  1. it has completed (by resolving a { done: true } object, per async iteration protocol)
  2. it had thrown an error (in which case the escorting error property will be set to that error).
When true, the adjacent value property will __still be set__ to the last value seen before the moment of completing/erroring.

** Is always false if source is a plain value instead of an async iterable.
.error Indicates whether the iterated async iterable threw an error, capturing a reference to it.
If error is non-empty, the escorting done property will always be true because the iteration process has effectively ended.

** Is always undefined if source is a plain value instead of an async iterable.

Components

<It>

Alias: <Iterate>

The <It> component (also exported as <Iterate>) is used to format and render an async iterable (or a plain non-iterable value) directly onto a piece of UI.

Essentially, can be seen as a useAsyncIter hook in a component form.

// In "simplified" form:

<It>{myIter}</It>


// In "render function" form:

<It value={myIter}>
  {({ value, pendingFirst, done, error }) =>
    // ...
  }
</It>

Props

  • value: The source value to iterate over - an async iterable or a plain (non async iterable) value. The input value may be changed any time, starting a new iteration in the background, per Iteration lifecycle. If using the "simplified" form, value is ignored and the source value should be provided as children instead.

  • initialValue: An optional starting value, defaults to undefined. Will be the value inserted into the child render function when <It> first renders during mount and while it's pending first yield. You can pass an actual value, or a function that returns a value (which <It> will call once during mounting).

  • children: A render function that is called for each step of the iteration, returning something to render out of it, with the current state object as the argument (see Iteration state properties breakdown). If using the "simplified" form instead - the source value should be directly passed as children and yielded values are rendered just as-are without any formatting on top.

Notes

  • Care should be taken to avoid passing a constantly recreated iterable object across re-renders, e.g; by declaring it outside the component body or by controlling when it should be recreated with React's useMemo.
Additional examples
import { It } from 'react-async-iterators';

function SelfUpdatingTodoList(props) {
  return (
    <div>
      <h2>My TODOs</h2>

      <div>
        Last TODO was completed at: <It>{props.lastCompletedTodoDate}</It>
      </div>

      <ul>
        <It value={props.todosAsyncIter}>
          {({ value: todos }) =>
            todos?.map(todo =>
              <li key={todo.id}>{todo.text}</li>
            )
          }
        </It>
      </ul>
    </div>
  );
}

// With the `initialValue` prop and showing usage of all properties of the iteration object
// within the child render function:

import { It } from 'react-async-iterators';

function SelfUpdatingTodoList(props) {
  return (
    <div>
      <h2>My TODOs</h2>

      <It value={props.todosAsyncIter} initialValue={[]}>
        {todosNext =>
          todosNext.pendingFirst ? (
            <div>Loading first todos...</div>
          ) : (
            <>
              {todosNext.error ? (
                <div>An error was encountered: {todosNext.error.toString()}</div>
              ) : (
                todosNext.done && <div>No additional updates for todos are expected</div>
              )}

              <ul>
                {todosNext.map(todo => (
                  <li key={todo.id}>{todo.text}</li>
                ))}
              </ul>
            </>
          )
        }
      </It>
    </div>
  );
}

<ItMulti>

Alias: <IterateMulti>

The <ItMulti> component (also exported as <IterateMulti>) is used to combine and render any number of async iterables (or plain non-iterable values) directly onto a piece of UI.

It's similar to <It>, only it works with any changeable number of async iterables or plain values instead of a single one. Essentially, can be seen as a useAsyncIterMulti hook in a component form.

<ItMulti values={[myIter, myOtherIter /* ... */]}>
  {([myIterNext, myOtherIterNext]) =>
    // ...
  }
</ItMulti>

Props

  • values: An array of values to iterate over simultaneously, which may include any mix of async iterables or plain (non async iterable) values. Source values may be added, removed or changed at any time and new iterations will be close and started accordingly as per Iteration lifecycle.

  • initialValues: An optional array of initial values or functions that return initial values. The values here will be the starting points for all the async iterables from values (by corresponding array positions) while they are rendered by the children render function for the first time and for each while it is pending its first yield. Async iterables from values that have no initial value corresponding to them will assume undefined as initial value.

  • defaultInitialValue: An optional default starting value for every new async iterable in values if there is no corresponding one for it in the initialValues prop, 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).

  • children: A render function that is called on every progression in any of the running iterations, returning something to render for them. The function is called with an array of the combined iteration state objects of all sources currently given by the values prop (see Iteration state properties breakdown).

Notes

  • Care should be taken to avoid passing constantly recreated async iterables across re-renders, e.g; by declaring iterables outside the component body or by controlling when iterables should be recreated with React's useMemo.
Additional examples
    ```tsx
    import { useMemo } from 'react';
    import { ItMulti } from 'react-async-iterators';
    
    function MyComponent() {
      const numberIter = useMemo(() => createNumberIter(), []);
      const arrayIter = useMemo(() => createArrayIter(), []);
      return (
        <>
          <Header />
          <SideMenu />
          <main>
            <ItMulti values={[numberIter, arrayIter]} initialValues={[0, []]}>
              {([numState, arrState]) => (
                <>
                  <div>
                    {numState.pendingFirst
                      ? '⏳ Loading number...'
                      : `Current number: ${numState.value}`}
                  </div>
                  <div>
                    {arrState.pendingFirst
                      ? '⏳ Loading items...'
                      : arrState.value.map((item, i) => <div key={i}>{item}</div>)}
                  </div>
                </>
              )}
            </ItMulti>
          </main>
        </>
      )
    }
    ```
    
    <br/>
    
    ```tsx
    // Using `<ItMulti>` with a dynamically changing amount of inputs:
    
    import { useState } from 'react';
    import { ItMulti, type MaybeAsyncIterable } from 'react-async-iterators';
    
    function DynamicInputsComponent() {
      const [inputs, setInputs] = useState<MaybeAsyncIterable<string>[]>([]);
    
      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 => [iterableValue, ...prev]);
      };
    
      const addStaticValue = () => {
        const staticValue = `Static ${inputs.length + 1}`;
        setInputs(prev => [staticValue, ...prev]);
      };
    
      return (
        <div>
          <h3>Dynamic Concurrent Async Iteration</h3>
    
          <button onClick={addAsyncIterValue}>🔄 Add Async Iterable</button>
          <button onClick={addStaticValue}>🗿 Add Static Value</button>
    
          <ul>
            <ItMulti values={inputs} defaultInitialValue="">
              {states =>
                states.map((state, i) => (
                  <li key={i}>
                    {state.done
                      ? state.error
                        ? `Error: ${state.error}`
                        : 'Done'
                      : state.pendingFirst
                        ? 'Pending...'
                        : `Value: ${state.value}`}
                  </li>
                ))
              }
            </ItMulti>
          </ul>
        </div>
      );
    }
    ```
    

Hooks

useAsyncIter

useAsyncIter hooks up a single async iterable value to your component and its lifecycle.

const next = useAsyncIter(myIter, 'initial_value');

next.value;
next.pendingFirst;
next.done;
next.error;

Parameters

  • value: The source value to iterate over - an async iterable or a plain (non async iterable) value. The input value may be changed any time, starting a new iteration in the background, per Iteration lifecycle.

  • initialValue: An 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

The iteration state object with properties reflecting the current state of the iterated async iterable or plain value provided via value (see Iteration state properties breakdown).

Notes

  • <It> may be preferable over the useAsyncIter counterpart typically as the UI area it re-renders within a component tree can be confined expressively to the necessary minimum, saving any other unrelated elements from re-evaluation. On the other hand, useAsyncIter being a hook must re-render the entire component's output for every new value.

  • Care should be taken to avoid passing a constantly recreated iterable object across re-renders, e.g; by declaring it outside the component body or by controlling when it should be recreated with React's useMemo.

Additional examples
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>
  );
}

// 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>
      )}
    </>
  );
}

useAsyncIterMulti

useAsyncIterMulti hooks up multiple async iterable (or plain) values to your component and its lifecycle.

It's similar to useAsyncIter, only it works with any number of async iterables or plain values instead of a single one.

const nextStates = useAsyncIterMulti(iters);

// or also:

const [nextNum, nextStr, nextArr] = useAsyncIterMulti([numberIter, stringIter, arrayIter], {
  initialValues: [0, '', []]
});

Parameters

  • values: An array of values to iterate over simultaneously, which may include any mix of async iterables or plain (non async iterable) values. Source values may be added, removed or changed at any time and new iterations will be close and started accordingly as per Iteration lifecycle.

  • opts: An optional object with properties:

    • initialValues: An optional array of initial values or functions that return initial values. The values will be the starting points for all the async iterables from values (by corresponding array positions) for the first time and for each while it is pending its first yield. For async iterables from values that have no corresponding item here the provided opts.defaultInitialValue will be used as fallback.

    • defaultInitialValue: An optional default starting value for every new async iterable in values 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 with up-to-date information about each input's current value, completion status, and more - corresponding to the order by which they appear on values (see Iteration state properties breakdown).

Notes

  • Care should be taken to avoid passing constantly recreated async iterables across re-renders, e.g; by declaring iterables outside the component body or by controlling when iterables should be recreated with React's useMemo.
Additional examples
    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;
      }
    })();

// 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>
  );
}

useAsyncIterState

Basically like React.useState, only that the value is provided back wrapped in an async iterable.

This hook allows a component to declare and manage a piece of state as an async iterable thus letting you easily control what specific places in the app UI tree should be bound to it, re-rendering in reaction to its changes (if used in conjunction with <It> for example).

const [valueIter, setValue] = useAsyncIterState(initialValue);

function handleChange() {
  setValue(valueIter.value.current + 1);
  // or:
  setValue(value => value + 1);
}

<It>{valueIter}</It>

function handleValueSubmit() {
  // use `valueIter.value.current` to get the current state immediately
  // (e.g, as part of some side effect logic)
  submitMyValue({ value: valueIter.value.current });
}

Parameters

  • initialValue: Any optional starting value for the state iterable's .value.current property, 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

A stateful async iterable with accessible current value and a function for yielding an update. The returned async iterable is a shared iterable such that multiple simultaneous consumers (e.g multiple <It>s) all pick up the same yields at the same times. The setter function, likeReact.useState's setter, can be provided the next state directly, or a function that calculates it from the previous state.

Notes


    ℹ️ The returned state iterable also contains a .value.current property which shows the current up to date state value at any time. Use this any time you need to read the immediate current state (e.g as part of side effects). Otherwise, to display its value and future ones to a user simply render it with things like <It>/<ItMulti>, useAsyncIter, etc - more info at Async iterables with current values.


    ℹ️ The returned async iterable and setter function both maintain stable references across re-renders so are effective for use within React's useMemo or useEffect and their dependency lists.

Additional examples
    import { useAsyncIterState, It } from 'react-async-iterators';
    
    function MyForm() {
      const [firstNameIter, setFirstName] = useAsyncIterState('');
      const [lastNameIter, setLastName] = useAsyncIterState('');
      return (
        <div>
          <form>
            <FirstNameInput valueIter={firstNameIter} onChange={setFirstName} />
            <LastNameInput valueIter={lastNameIter} onChange={setLastName} />
          </form>
    
          Greetings, <It>{firstNameIter}</It> <It>{lastNameIter}</It>
        </div>
      );
    }

    // Use the state iterable's `.value.current` property to read the immediate current state:
    
    import { useAsyncIterState } from 'react-async-iterators';
    
    function MyForm() {
      const [firstNameIter, setFirstName] = useAsyncIterState('');
      const [lastNameIter, setLastName] = useAsyncIterState('');
    
      return (
        <form
          onSubmit={() => {
            const firstName = firstNameIter.value.current;
            const lastName = lastNameIter.value.current;
            // submit `firstName` and `lastName`...
          }}
        >
          <>...</>
        </form>
      );
    }

Utils

iterateFormatted

A utility to inline-format an async iterable's values before passed into another consuming component.

Can be thought of as mapping an async iterable before being rendered/passed over in the same way you would commonly .map(...) an array before rendering/passing it over. More details in Formatting values.

iterateFormatted(myIter, (value, idx) => {
  // return a formatted value here...
})
// With some given async-iter-receiving `<Select>` component:

<Select
  optionsIter={iterateFormatted(currenciesIter, ({ isoCode, name }) => ({
    value: isoCode,
    label: `${name} (${isoCode})`
  }))}
  onChange={...}
/>

Parameters

  • source: Any async iterable or plain value.

  • formatFn: Function that performs formatting/mapping logic for each value of source.

Returns

A transformed async iterable emitting every value of source after formatting. If source is a plain value and not an async iterable, it will be passed into the given formatFn and returned on the spot.

Notes

  • iterateFormatted acts by returning a new transformed version of the source async iterable object, attaching it with some special metadata telling consumers like <It> and useAsyncIter that the original base object is what the iteration process should be bound to instead of the given object. This way, the resulting formatted iterable may be recreated repeatedly without concerns of restarting the iteration process (as long as source is passed the same base iterable consistently).

  • If source has a current value property at .value.current (see Async iterables with current values), it will be formatted via formatFn as well.

  • If source is a plain value and not an async iterable, it will be passed into the given formatFn and returned on the spot.

Additional examples
    import { iterateFormatted } from 'iter-tools';
    
    const currenciesIter = getAvailableCurrenciesIter();
    
    // This:
    
    function MyComponent() {
      return (
        <div>
          <Select
            options={iterateFormatted(currenciesIter, ({ isoCode, name }) => ({
              value: isoCode,
              label: `${name} (${isoCode})`
            }))}
          />
        </div>
      );
    }
    
    // instead of this:
    
    import { useMemo } from 'react';
    import { execPipe as pipe, asyncMap } from 'iter-tools';
    
    function MyComponent() {
      const formattedCurrenciesIter = useMemo(
        () =>
          pipe(
            currenciesIter,
            asyncMap(({ isoCode, name }) => ({
              value: isoCode,
              label: `${name} (${isoCode})`
            }))
          ),
        []
      );
    
      return (
        <div>
          <Select options={formattedCurrenciesIter} />
        </div>
      );
    }

License

Free and licensed under the MIT License (c) 2024