{"id":16986043,"url":"https://github.com/chung-leong/react-seq","last_synced_at":"2025-07-31T18:04:37.220Z","repository":{"id":61074554,"uuid":"538652695","full_name":"chung-leong/react-seq","owner":"chung-leong","description":"React hooks for working with async generators and promises","archived":false,"fork":false,"pushed_at":"2023-10-21T12:41:42.000Z","size":5712,"stargazers_count":8,"open_issues_count":8,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-01T17:12:27.778Z","etag":null,"topics":["async","asynchronous-programming","generator","hooks","promise","react"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chung-leong.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-09-19T18:54:17.000Z","updated_at":"2024-11-20T21:08:12.000Z","dependencies_parsed_at":"2024-06-21T15:36:26.357Z","dependency_job_id":null,"html_url":"https://github.com/chung-leong/react-seq","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chung-leong%2Freact-seq","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chung-leong%2Freact-seq/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chung-leong%2Freact-seq/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chung-leong%2Freact-seq/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chung-leong","download_url":"https://codeload.github.com/chung-leong/react-seq/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244227587,"owners_count":20419264,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","asynchronous-programming","generator","hooks","promise","react"],"created_at":"2024-10-14T02:44:43.266Z","updated_at":"2025-03-22T15:30:49.629Z","avatar_url":"https://github.com/chung-leong.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# React-seq ![ci](https://img.shields.io/github/actions/workflow/status/chung-leong/react-seq/node.js.yml?branch=main\u0026label=Node.js%20CI\u0026logo=github) ![nycrc config on GitHub](https://img.shields.io/nycrc/chung-leong/react-seq)\n\nReact-seq is a light-weight library that helps you take full advantage of async functions and generators while\ndeveloping React apps. It provides a set of hooks for managing processes that require time to complete.\nIt's designed for React 18 and above.\n\n## Installation\n\n```sh\nnpm install --save-dev react-seq\n```\n\n## Basic usage\n\nReact only:\n\n```js\nimport { useState, useEffect } from 'react';\n\nexport function Counter() {\n  const [ count, setCount ] = useState(0);\n  useEffect(() =\u003e {\n    const timer = setInterval(() =\u003e {\n      setCount(c =\u003e c + 1);\n    }, 200);\n    return () =\u003e {\n      clearInterval(timer);\n    };\n  }, []);\n  return \u003cdiv\u003e{count} tests passed\u003c/div\u003e;\n}\n```\n\nReact + React-seq:\n\n```js\nimport { useSequentialState, delay } from 'react-seq';\n\nexport function Counter() {\n  const count = useSequentialState(async function*({ initial }) {\n    let count = 0;\n    initial(count++);\n    do {\n      await delay(200);\n      yield count++;\n    } while (true);\n  }, []);\n  return \u003cdiv\u003e{count} tests passed\u003c/div\u003e;\n}\n```\n\nThe generator version is not only easier to understand, it also allows you to utilize the debugger much more effectively.\nYou can step through the code line by line and easily set conditional breakpoints.\n\nThe convinence of React-seq comes at the cost of higher memory usage. It's expected that you would only use it in\nhigher-level components. \n\n## Hooks\n\n* [`useSequential`](./doc/useSequential.md) - Return the last element outputted by an async generator function\n* [`useProgressive`](./doc/useProgressive.md) - Return an element filled with data from multiple async sources\n* [`useSequentialState`](./doc/useSequentialState.md) - Return the last value outputted by an async generator function\n* [`useProgressiveState`](./doc/useProgressiveState.md) - Return an object whose properties are drawn from async sources\n\n## Usage scenarios\n\n* [Loading of remote data](#loading-of-remote-data)\n* [Handling page navigation](#handling-page-navigation)\n* [Adding transition effects](#adding-transition-effects)\n* [Handling authentication](#handling-authentication)\n* [Managing complex states](#managing-complex-states)\n\n## Other topics\n\n* [Error handling](#error-handling)\n* [Server-side rendering](#server-side-rendering)\n* [Logging](#logging)\n* [Unit testing](#unit-testing)\n* [ESLint configuration](#eslint-configuration)\n* [Jest configuration](#jest-configuration)\n* [Browser compatibility](#browser-compatibility)\n\n## API reference\n\n* [Hooks and other functions](./doc/README.md)\n* [Server-side rendering](./doc/server/README.md)\n* [Client-side SSR support](./doc/client/README.md)\n* [Test utilities](./doc/test-utils/README.md)\n\n## List of examples\n\n* [Payment form](./examples/payment/README.md) \u003csup\u003e`useSequential`\u003c/sup\u003e\n* [Star Wars API](./examples/swapi/README.md) \u003csup\u003e`useProgressive`\u003c/sup\u003e\n* [WordPress](./examples/wordpress/README.md) \u003csup\u003e`useProgressive`\u003c/sup\u003e\n* [Nobel Prize API](./examples/nobel/README.md) \u003csup\u003e`useSequentialState`\u003c/sup\u003e\n* [Star Wars API (alternate implementation)](./examples/swapi-hook/README.md) \u003csup\u003e`useSequentialState`\u003c/sup\u003e \u003csup\u003e`useProgressiveState`\u003c/sup\u003e\n* [WordPress (React Native)](./examples/wordpress-react-native/README.md) \u003csup\u003e`useProgressive`\u003c/sup\u003e\n* [Star Wars API (server-side rendering)](./examples/swapi-ssr/README.md) \u003csup\u003e`useProgressive`\u003c/sup\u003e\n* [NPM Search](./examples/npm-input/README.md) \u003csup\u003e`useSequentialState`\u003c/sup\u003e \u003csup\u003e`useProgressiveState`\u003c/sup\u003e\n* [Media capture](./examples/media-cap/README.md) \u003csup\u003e`useSequentialState`\u003c/sup\u003e\n* [Transition](./examples/transition/README.md) \u003csup\u003e`useSequential`\u003c/sup\u003e\n* [Ink CLI](./examples/ink-cli/README.md) \u003csup\u003e`useSequential`\u003c/sup\u003e\n\n## Loading of remote data\n\nRetrieval of data from a remote server is probably the most common async operation in web applications. React-seq\nlets you accomplish this task through different approaches. You can use `useSequential` to construct each part of\na page as data arrives:\n\n```js\nimport { useSequential } from 'react-seq';\n\nfunction ProductPage({ productId }) {\n  return useSequential(async function*({ fallback, defer }) {\n    fallback(\u003cdiv className=\"spinner\"/\u003e);\n    defer(200);\n    const product = await fetchProduct(productId);\n    const { default: ProductDescription } = await import('./ProductDescription.js');\n    yield (\n      \u003cdiv className=\"stage-1\"\u003e\n        \u003cProductDescription product={product} /\u003e\n      \u003c/div\u003e\n    );\n    const related = await fetchRelatedProducts(product);\n    const { default: ProductCarousel } = await import('./ProductCarousel.js');\n    yield (\n      \u003cdiv className=\"stage-2\"\u003e\n        \u003cProductDescription product={product} /\u003e\n        \u003cProductCarousel products={related} /\u003e\n      \u003c/div\u003e\n    );\n    const promoted = await fetchPromotedProducts();\n    yield (\n      \u003cdiv className=\"stage-3\"\u003e\n        \u003cProductDescription product={product} /\u003e\n        \u003cProductCarousel products={related} /\u003e\n        \u003cProductCarousel products={promoted} /\u003e\n      \u003c/div\u003e\n    );\n    /* ... */\n  }, [ productId ]);\n}\n```\n\nYou can periodically update the page with the help of an endless loop:\n\n```js\nfunction ProductPage({ productId }) {\n  return useSequential(async function*({ fallback, defer, manageEvents, flush }) {\n    fallback(\u003cdiv className=\"spinner\"/\u003e);\n    const [ on, eventual ] = manageEvents();\n    for (let i = 0;; i++) {\n      defer(i === 0 ? 200 : Infinity);\n      try {\n        const product = await fetchProduct(productId);\n        const { default: ProductDescription } = await import('./ProductDescription.js');\n        yield (\n          \u003cdiv className=\"stage-1\"\u003e\n            \u003cProductDescription product={product} onUpdate={on.updateRequest} /\u003e\n          \u003c/div\u003e\n        );\n        const related = await fetchRelatedProducts(product);\n        const { default: ProductCarousel } = await import('./ProductCarousel.js');\n        yield (\n          \u003cdiv className=\"stage-2\"\u003e\n            \u003cProductDescription product={product} onUpdate={on.updateRequest} /\u003e\n            \u003cProductCarousel products={related} /\u003e\n          \u003c/div\u003e\n        );\n        const promoted = await fetchPromotedProducts();\n        yield (\n          \u003cdiv className=\"stage-3\"\u003e\n            \u003cProductDescription product={product} onUpdate={on.updateRequest} /\u003e\n            \u003cProductCarousel products={related} /\u003e\n            \u003cProductCarousel products={promoted} /\u003e\n          \u003c/div\u003e\n        );\n      } catch (err) {\n        if (i === 0) {\n          throw err;\n        } else {\n          // abandon partially rendered page\n          flush(false);\n        }\n      } finally {\n        await eventual.updateRequest.for(5).minutes;\n      }\n    }\n    /* ... */\n  }, [ productId ]);\n}\n```\n\nThe example above demonstrates the use of React-seq's [event manager](./doc/managerEvents.md). It's a key component\nof the library. Its automatically generated promises and handlers connect your user interface with\nyour async code. Here, it allows the user to manually trigger an update: Calling `on.updateRequest` causes the\nfulfillment of the `eventual.updateRequest` promise. That releases the generator function from the `await` operation\ninside the finally block.\n\nSince data loading happens so frequently in web applications, React-seq provides a specialized hook:\n[`useProgressive`](./doc/useProgressive.md). It works as a sort of async-to-sync translator, turning promises\nand generators into ordinary objects and arrays.\n\nConsider the following component taken from the [Star Wars API example](./examples/swapi/README.md):\n\n```js\nexport default function Character({ id }) {\n  return useProgressive(async ({ type, defer, suspend, signal }) =\u003e {\n    type(CharacterUI);\n    defer(200);\n    suspend(`character-${id}`);\n    const person = await fetchOne(`people/${id}`, { signal });\n    return {\n      person,\n      films: fetchMultiple(person.films, { signal }),\n      species: fetchMultiple(person.species, { signal }),\n      homeworld: fetchOne(person.homeworld, { signal }),\n      vehicles: fetchMultiple(person.vehicles, { signal }),\n      starships: fetchMultiple(person.starships, { signal }),\n    };\n  }, [ id ]);\n}\n```\n\n(Note the lack of `await` in front of many of the fetch calls.)\n\nThe object returned by the callback would contain this:\n\n```js\n{\n  person: [object],\n  films: [async generator],\n  species: [async generator],\n  homeworld: [promise],\n  vehicles: [async generator],\n  starships: [async generator]\n}\n```\n\nThe component `CharacterUI` would receive this as props:\n\n```js\n{\n  person: [object],\n  films: [array],\n  species: [array],\n  homeworld: [object],\n  vehicles: [array],\n  starships: [array]\n}\n```\n\nThe various arrays would grow over time as data is retrieved from the remote server.\n\nIt's possible to create a generator that pauses and resumes on user action. The\n[WordPress example](./examples/wordpress.md) shows how this can be used to implement an infinite-scrolling article\nlist. The [React Native version of the example](./examples/wordpress-react-native.md) is also worth checking out.\n\nBesides using [`useSequential`](./doc/useSequential.md) and [`useProgressive`](./doc/useProgressive.md), you can\nalso load data with the help of React-seq's state hooks: [`useSequentialState`](./doc/useSequentialState.md)\nand [`useProgressiveState`](./doc/useProgressiveState.md). Instead of React elements, these hooks return simple values\n(usually objects). They are useful for components that handle user input. For example:\n\n```js\nfunction SearchBar() {\n  const categories = useSequentialState(async function*({ signal }) {\n    initial([]);\n    const res = await fetch('/api/categories/', { signal });\n    yield res.json();\n  }, []);\n  const [ query, setQuery ] = useState('');\n  const [ category, setCategory ] = useState('');\n  return (\n    \u003cdiv className=\"SearchBar\"\u003e\n      \u003cinput type=\"text\" value={query} onChange={evt =\u003e setQuery(evt.target.value)} /\u003e\n      \u003cselect value={category} onChange={evt =\u003e setCategory(evt.target.value)} \u003e\n        {categories.map(c =\u003e (\n          \u003coption value={c.id}\u003e{c.name}\u003c/option\u003e\n        ))}\n      \u003c/select\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\nState hooks are also the right ones to use generally when you are creating custom hooks. Consult the\n[alternate implementation of the Star Wars API example](./examples/swapi-hook/README.md) to learn more about\nthe advantages and drawbacks.\n\n## Handling page navigation\n\nYou can use a `useSequential` hook to handle page navigation for you app in the following manner:\n\n```js\nexport default function App() {\n  const [ parts, query, { trap, throw404, isDetour } ] = useSequentialRouter();\n  return useSequential(async function* (methods) {\n    const { reject, manageEvents } = methods;\n    trap('detour', (err) =\u003e {\n      reject(err);\n      return true;\n    });\n    const [ on, eventual ] = manageEvents();\n    for (;;) {\n      try {\n        if (parts[0] === 'products') {\n          if (!parts[1]) {\n            const { default: ProductList } = await import('./ProductList.js');\n            yield \u003cProductList onSelect={on.selection} /\u003e;\n            const { selection } = await eventual.selection;\n            parts[1] = selection;\n          } else {\n            const { default: ProductDetails } = await import('./ProductDetails.js');\n            yield \u003cProductDetails id={parts[1]} onReturn={on.return} /\u003e;\n            await eventual.return;\n            parts.splice(1);\n          }\n        } else if (parts[0] === 'news') {\n          /* ... */\n        } else if (parts[0] === 'notifications') {\n          /* ... */\n        } else {\n          throw404();\n        }\n      } catch (err) {\n        if (isDetour(err)) {\n          err.proceed();\n        } else {\n          yield \u003cErrorPage error={err} onRetry={on.retry} /\u003e;\n          await eventual.retry;\n        }\n      }\n    }\n  }, [ parts, query, trap, isDetour ]);\n}\n```\n\nThe example uses [Array-router](https://github.com/chung-leong/array-router), a companion solution designed\nto work well with React-seq. It's a minimalist \"router\" that turns the browser location into an array of path\nparts and an object containing query variables. Actual routing is done using JavaScript control structures.\n\nThe code above follows the Yield-Await-Promise model. We output some user-interface elements then wait for a\nuser response. We then act upon that response, causing the displaying of a different page.\n\nOur code is in full control of navigation. It alters the route components and interprets them. Clicks on hyperlinks\nand use of the browser's back/forward buttons are treated as exceptions. If a detour request manages to reach\nthe top-level try-catch block, it gets approved.\n\nReact-seq can handle nested generators. `useSequential` will in effect [flatten](./doc/linearize.md) the generator\nprovided by its callback. That allows us to break a long generator functions into more manageable subroutines:\n\n```js\nexport default function App() {\n  const [ parts, query, { trap, throw404, isDetour } ] = useSequentialRouter();\n  return useSequential(async function* (methods) {\n    const { reject, manageEvents } = methods;\n    trap('detour', (err) =\u003e {\n      reject(err);\n      return true;\n    });\n    const [ on, eventual ] = manageEvents();\n    for (;;) {\n      try {\n        if (parts[0] === 'products') {\n          yield handleProductSection(parts, query, methods);\n        } else if (parts[0] === 'news') {\n          yield handleNewsSection(parts, query, methods);\n        } else if (parts[0] === 'notifications') {\n          yield handleNotificationSection(parts, query, methods);\n        } else {\n          throw404();\n        }\n      } catch (err) {\n        if (isDetour(err)) {\n          err.proceed();\n        } else {\n          yield \u003cErrorPage error={err} onRetry={on.retry} /\u003e;\n          await eventual.retry;\n        }\n      }\n    }\n  }, [ parts, query, trap, isDetour ]);\n}\n\nasync function *handleProductSection(parts, query, methods) {\n  const { manageEvents } = methods;\n  const [ on, eventual ] = manageEvents();\n  try {\n    for (;;) {\n      if (!parts[1]) {\n        const { default: ProductList } = await import('./ProductList.js');\n        yield \u003cProductList onSelect={on.selection} /\u003e;\n        const { selection } = await eventual.selection;\n        parts[1] = selection;\n      } else {\n        const { default: ProductDetails } = await import('./ProductDetails.js');\n        yield \u003cProductDetails id={parts[1]} onReturn={on.return} /\u003e;\n        await eventual.return;\n        delete parts[1];\n      }\n    }\n  } finally {\n    // do clean-up here\n  }\n}\n```\n\nSince there is no `break` or `return` inside the loop, the only way to exit the product section is via an exception\n(probably a detour request). When that happens, code in the finally block will run. If the function had made changes\nspecific to this section, it can roll them back here.\n\nPlease note that certain implementation details are left out of the code snippets above for brevity sake.\nConsult the [transition example](./examples/transition/README.md) for something that better represent a\nfully-developed, real-world solution. You'll also find further discussion on the Yield-Await-Promise model.\n\n## Adding transition effects\n\nReact-seq's ability to handle nested async generator lends itself nicely to performing page transition. All we would\nneed is a function that takes an element and returns a generator producing the right sequence. It can output\nanything. The only requirement is that the last item coming from the generator is the next page.\n\nSuppose you want to fill the screen with a fire ball when the user navigates to a different page (because it's a web\nsite for fans of professional wrestling?). The first item of the transition sequence would be the current page\noverlaid with a clip of an explosion. Once the clip has played to some midway point where the screen is fully engulfed\nin flame, the generator outputs the next page overlaid with the same clip. Finally, when the clip has reached its end,\nthe generator outputs only the next page.\n\n![Transition sequence](./doc/img/transition.jpg)\n\nThanks to the syntactic sugar provided by React-seq's [event manager](./doc/managerEvents.md), orchestrating such a sequence is quite easy.\n\nTo see the concept described here in action, check out the [transition example](./examples/transition/README.md).\n\n## Handling authentication\n\nThe ability to insert whole sequence of pages anywhere also makes it rather easy to handle authentication. If a\nparticular section of our app is restricted to logged-in users only, all we would have to do is place a line atop\nthat section:\n\n```js\n  yield handleLogin(methods);\n```\n\n`handleLogin` might look something like this:\n\n```js\nasync function* handleLogin({ manageEvents }) {\n  if (isAuthenticated()) {\n    return;\n  }\n  const [ on, eventual ] = manageEvents();\n  const { default: ScreenLogin } = await import('./screens/ScreenLogin.js');\n  let error;\n  for (;;) {\n    yield \u003cScreenLogin onSubmit={on.credentials} lastError={error} /\u003e;\n    const { credentials } = await eventual.credentials;\n    try {\n      await authenticateUser(credentials);\n      return;\n    } catch (err) {\n      error = err;\n    }\n  }\n}\n```\n\nA call to this function might also happen in the app's catch block, to deal with expired user session:\n\n```js\n    } catch (err) {\n      if (err instanceof HTTPError \u0026\u0026 err.status === 401) {\n        yield handleLogin(methods);\n      } else {\n        yield \u003cScreenError error={err} onRetry={on.retryRequest} /\u003e\n        await eventual.retryRequest;\n      }\n    }\n```\n\nIf the user manages to log in, that means our catch block has successfully resolved the error. The loop sends the user \nback to where he was before (since the route hasn't changed) and whatever being done there would no longer cause an \nerror. Everything works as it should.\n\n## Managing complex states\n\nState management using an async generator function is generally much easier, even when no inherently async operations\nlike data retrieval are involved. First of all, you have a variable scope that persists over time. When you need to\nremember something, just set a local variable. At the same time the number of states that need be tracked are sharply\nreduced thanks to async functions' ability to halt mid-execution. Consider the\n[following example](./examples/konami-code/README.md). It's a hook that listens for a sequence of keystrokes\nmatching the well-known [Konami code](https://en.wikipedia.org/wiki/Konami_Code):\n\n```js\nfunction useKonamiCode() {\n  return useSequentialState(async function*({ initial, mount, manageEvents, signal }) {\n    initial(false);\n    await mount();\n    const [ on, eventual ] = manageEvents();\n    window.addEventListener('keydown', on.key.filter(e =\u003e e.key), { signal });\n    while (!(\n         await eventual.key.value() === 'ArrowUp'\n      \u0026\u0026 await eventual.key.value() === 'ArrowUp'\n      \u0026\u0026 await eventual.key.value() === 'ArrowDown'\n      \u0026\u0026 await eventual.key.value() === 'ArrowDown'\n      \u0026\u0026 await eventual.key.value() === 'ArrowLeft'\n      \u0026\u0026 await eventual.key.value() === 'ArrowRight'\n      \u0026\u0026 await eventual.key.value() === 'ArrowLeft'\n      \u0026\u0026 await eventual.key.value() === 'ArrowRight'\n      \u0026\u0026 await eventual.key.value() === 'b'\n      \u0026\u0026 await eventual.key.value() === 'a'\n    ));\n    yield true;\n  }, []);\n}\n```\n\nNotice how there isn't an array holding the keys that the user has pressed. No number indicating which portion of\nthe sequence has matched thus far either. There are no progress-tracking variables. We don't need them because the\nJavaScript engine is tracking progress for us. It knows where in the code it has stopped.\n\nNote also the use of [`signal`](./doc/signal.md) in the call to\n[`addEventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener). That eliminates\nthe need to call `removeEventListener`.\n\nFor further demonstration of how React-seq can help you manage state, please consult the\n[media capture example](./examples/media-cap/README.md).\n\n## Error handling\n\nErrors encountered by React-seq hooks will trigger component updates and get rethrown during React's rendering cycle,\nallowing them to be handled by an error boundary further up the component tree.\n\nWhen you employ the Yield-Await-Promise model, you can funnel errors through the generator tree with the help of\n[`reject`](./reject.md). Example:\n\n```js\nfunction App() {\n  return useSequential(async function*(methods) {\n    const { manageEvents, wrap, reject } = methods;\n    const [ on, eventual ] = manageEvents();\n    // create error boundary around contents\n    wrap(children =\u003e \u003cErrorBoundary onError={reject}\u003e{children}\u003c/ErrorBoundary\u003e);\n    wrap(children =\u003e \u003cAppFrame\u003e{children}\u003c/AppFrame\u003e);\n    let section = 'news';\n    for (;;) {\n      try {\n        if (section === 'news') {\n          // handle news section in a separate function\n          yield handleNewsSection(methods);\n        } else if (page === 'products') {\n          yield handleProductSection(methods);\n        }\n      } catch (err) {\n        yield \u003cErrorPage error={err} onRetry={on.retryRequest} /\u003e;\n        await eventual.retryRequest;\n      }\n    }\n  }, []);\n}\n\nasync function *handleNewsSection({ wrap, manageEvents }) {\n  const [ on, eventual ] = manageEvents();\n  const unwrap = wrap(children =\u003e \u003cNewsSection\u003e{children}\u003c/NewsSection\u003e);\n  let articleId;\n  try {\n    for (;;) {\n      try {\n        if (!articleId) {\n          try {\n            yield \u003cArticleList onSelect={on.selection} /\u003e;\n            // wait for an article to be selected\n            articleId = await eventual.selection.value();\n          } catch (err) {\n            // handle specific errors from ArticleList\n          }\n        } else {\n          /* ... */\n        }\n      } catch (err) {\n        if (err instanceof CMSError) {\n          // handle error specific to section\n        } else {\n          throw err;\n        }\n      }\n    }\n  } finally {\n    unwrap();\n  }\n}\n\nfunction ArticleList() {\n  return useProgressive(async ({ type, usable, manageEvents, signal }) =\u003e {\n    type(ArticleListUI);\n    usable({ articles: 1 });\n    const [ on, eventual ] = manageEvents();\n    const options = { signal };\n    const articles = fetchAll(() =\u003e eventual.needForMore, options);\n    const authors = fetchAuthors(articles, options);\n    const categories = fetchCategories(articles, options);\n    const tags = fetchTags(articles, options);\n    const media = fetchFeaturedMedia(articles, options);\n    return { articles, authors, categories, tags, media, onBottomReached: on.needForMore };\n  }, []);\n}\n\nclass ErrorBoundary extends Component {\n  constructor(props) {\n    super(props);\n    this.state = { error: null, fresh: false };\n  }\n\n  static getDerivedStateFromProps(props, state) {\n    const { error, fresh } = state;\n    if (fresh) {\n      // render() needs to see this--clear it next time\n      return { error, fresh: false };\n    } else {\n      // clear stale error\n      return { error: null };\n    }\n  }\n\n  static getDerivedStateFromError(error) {\n    return { error, fresh: true };\n  }\n\n  render() {\n    let { error } = this.state;\n    if (error) {\n      // keep rendering if the error was patched up somehow\n      if (this.props.onError(error) === true) {\n        error = null;\n      }\n    }\n    return !error ? this.props.children : null;\n  }\n}\n```\n\nSuppose the call to `fetchTags` in `ArticleList` causes a 404 Not Found error to be thrown. This error will first\nbubble through the React component tree as shown in this diagram (stage 1):\n\n![Error propagation](./doc/img/error-propagation.jpg)\n\nWhen it reaches the error boundary, it teleports to the spot of the current await operation. From there it bubbles\nthrough three try blocks (stage 2). As none of these would stop it, it pops through `handleNewsSection` and\nfinally gets caught by the catch block in `App` (stage 3).\n\nBasically, errors will resurface at (or close to) the decision points that led eventually to their occurrence.\nFrom the perspective of `handleNewsSection`, yielding `ArticleList` is what caused the error. It pops up on the very\nnext line. Action followed by consequence. Intuitively, this is how we expect exception handling would work. And in\ngeneral, the code responsible for triggering an error is in the best position to make decisions on how it should be\nhandled.\n\nOf course, not much can be done about a fatal error like a 404 aside from putting up an error message. In the\n[transition example](./examples/transition/README.md), you'll find a scenario where catching an error in a local\ntry-catch block actual makes sense.\n\n## Server-side rendering\n\nReact-seq has built-in support for a simple kind of server-side rendering (SSR), where server-generated HTML\nis essentially used as an app's fallback screen. Only a single function call is needed:\n\n```js\n  fastify.get('/*', async (req, reply) =\u003e {\n    reply.type('text/html');\n    const location = `${req.protocol}://${req.hostname}/${req.params['*']}`;\n    return renderInChildProc(location, buildPath);\n  });\n```\n\n[`renderInChildProc`](./doc/server/renderInChildProc.md) will generate the page using the app's production build.\nNo changes to your project's build configuration required. You only need to enable \n[hydration](./doc/client/hydrateRoot.md) and [render-to-server](./doc/client/renderToServer.md) in your app's\nboot-strap code:\n\n```js\nimport App from './App.js';\nimport { hydrateRoot, renderToServer } from 'react-seq/client';\n\nif (typeof(window) === 'object') {\n  hydrateRoot(document.getElementById('root'), \u003cApp /\u003e);\n} else {\n  renderToServer(\u003cApp /\u003e);\n}\n```\n\nTo see SSR in action, clone the repository and run the [Star Wars API SSR example](./examples/swapi-ssr/README.md).\n\n## Logging\n\nReact-seq provides a mean for you to examine what happens inside its hooks. When a hook detects the presence of\nan [`InspectorContext`](./doc/InspectorContext.md), it will start reporting events to the given inspector\ninstance.\n\nThe library comes with two built-in inspectors: [`ConsoleLogger`](./doc/ConsoleLogger.md) and\n[`PromiseLogger`](./doc/PromiseLogger.md). You can create you own by extending [`Inspector`](./doc/Inspector.md).\n\n\nThe [Payment form example](./examples/payment/README.md#logging) makes use of `ConsoleLogger`:\n\n```js\nexport default function App() {\n  const logger = useMemo(() =\u003e new ConsoleLogger(), []);\n  return (\n    \u003cdiv className=\"App\"\u003e\n      \u003cheader className=\"App-header\"\u003e\n        \u003cp\u003ePayment Page Example\u003c/p\u003e\n      \u003c/header\u003e\n      \u003cInspectorContext.Provider value={logger}\u003e\n        \u003cPaymentPage /\u003e\n      \u003c/InspectorContext.Provider\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n## Unit testing\n\nFor the purpose of unit testing React-seq provides two functions:\n[`withTestRenderer`](./doc/test-utils/withTestRenderer.md) and [`withRestDOM`](./doc/test-utils/withRestDOM.md).\nOne utilizes [React Test Renderer](https://reactjs.org/docs/test-renderer.html) while the other relies on the DOM. \nThey have the same interface.\n\nThe following is a test case from the [Payment form example](./examples/payment/README.md#unit-testing):\n\n```js\nimport { withTestRenderer } from 'react-seq/test-utils';\nimport { PaymentPage } from './PaymentPage.js';\nimport { PaymentSelectionScreen } from './PaymentSelectionScreen.js';\nimport { PaymentMethodBLIK } from './PaymentMethodBLIK.js';\nimport { PaymentProcessingScreen } from './PaymentProcessingScreen.js';\nimport { PaymentCompleteScreen } from './PaymentCompleteScreen.js';\n\ntest('payment with BLIK', async () =\u003e {\n  await withTestRenderer(\u003cPaymentPage /\u003e, async ({ awaiting, showing, shown, resolve }) =\u003e {\n    expect(showing()).toBe(PaymentSelectionScreen);\n    expect(awaiting()).toBe('selection');\n    await resolve({ selection: { name: 'BLIK', description: 'Payment using BLIK' } });\n    expect(showing()).toBe(PaymentMethodBLIK);\n    expect(awaiting()).toBe('submission.or.cancellation');\n    await resolve({ submission: { number: '123 456' } });\n    expect(shown()).toContain(PaymentProcessingScreen);\n    expect(showing()).toBe(PaymentCompleteScreen);\n    expect(awaiting()).toBe(undefined);\n  });\n});\n```\n\n`withTestRenderer` renders the component and awaits the first stoppage point. A stoppage point is either the\ntermination of a hook's generator or an `await` on a promise of the event manager. When one of the two occurs,\nthe callback is invoked. The test code can then check whether the expected outcome has been achieved then force\nthe component to move to the next stoppage point by manually settling the awaited promise.\n\n## ESLint configuration\n\nAdd \"react-seq\" to your ESLint settings to enable the linting of React-seq hooks:\n\n```json\n  \"eslintConfig\": {\n    \"extends\": [\n      \"react-app\",\n      \"react-app/jest\",\n      \"react-seq\"\n    ]\n  },\n```\n\nYou will find the `eslintConfig` section in your project's `package.json` if it was created using **Create React App**.\n\n## Jest configuration\n\nAdd the following to your project's `package.json` so Jest would transpile the library:\n\n```json\n  \"jest\": {\n    \"transformIgnorePatterns\": [\n      \"!node_modules/react-seq/\"\n    ]\n  },\n```\n\n## Browser compatibility\n\nReact-seq makes use of\n[JavaScript proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). According\nto Mozilla, it is available in the following environment:\n\n![Proxy compatibility](./doc/img/proxy-compatibility.jpg)\n\nSince the functionality in question cannot be polyfilled, React-seq does not work in any version of Internet Explorer\nor Opera Mini.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchung-leong%2Freact-seq","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchung-leong%2Freact-seq","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchung-leong%2Freact-seq/lists"}