├── .circleci └── config.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── CNAME ├── Details.md ├── Examples.md ├── GettingStarted.md ├── README.md ├── _config.yml ├── _layouts │ └── default.html └── assets │ └── highlight.js ├── examples ├── README.md ├── code-splitting │ ├── README.md │ ├── index.html │ ├── index.js │ ├── modules │ │ ├── Placeholder.js │ │ ├── PokemonAPI.js │ │ ├── PokemonInfo.js │ │ ├── PokemonList.js │ │ └── Routes.js │ ├── package.json │ └── webpack.config.js ├── search-form │ ├── .babelrc │ ├── README.md │ ├── modules │ │ ├── SearchAPI.js │ │ ├── SearchForm.js │ │ └── index.js │ ├── package.json │ └── public │ │ └── index.html ├── timing-motion │ ├── .babelrc │ ├── README.md │ ├── modules │ │ ├── App.js │ │ └── index.js │ ├── package.json │ └── public │ │ └── index.html └── unidirectional-dataflow │ ├── .babelrc │ ├── README.md │ ├── modules │ ├── App.js │ ├── AppContext.js │ ├── AppStateRecord.js │ ├── Counter.js │ └── index.js │ ├── package.json │ └── public │ └── index.html ├── modules ├── Coroutine.js └── __tests__ │ └── Coroutine-test.js ├── package.json └── scripts ├── integration.sh └── rollup.config.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:8 6 | 7 | steps: 8 | - checkout 9 | 10 | - run: 11 | name: Install Dependencies 12 | command: yarn install --ignore-scripts 13 | 14 | - run: 15 | name: Run Tests 16 | command: yarn ci 17 | environment: 18 | JEST_JUNIT_OUTPUT: "reports/junit/js-test-results.xml" 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | build 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## react-coroutine v2.0.2 2 | 3 | * Prevent usage of deprecated React lifecycle methods 4 | * Switch to `PureComponent` for small optimizations 5 | * Minor internal changes to improve iterators cancellation 6 | * Fix exceptions throw in coroutines based on sync generators 7 | * Update peer dependency range to support React versions higher than `16.2.0` 8 | 9 | ## react-coroutine v2.0.1 10 | 11 | * Prevent rejected promises swallowing in sync generators 12 | * Remove `shallowequal` dependency, decreasing the lib size 13 | * Use `babel-preset-env` instead of `babel-preset-es2015` for building the lib 14 | 15 | ## react-coroutine v2.0.0 16 | 17 | * Remove `context` argument from coroutine signature (recommended to switch to the new Context API) 18 | * `Coroutine` is only an object with `create()` factory method (breaking change for `v2.0.0-alpha.2`) 19 | * Use `pkg.module` instead of `jsnext:main` 20 | * Build correct ES Module artifact 21 | * Move `react` to `peerDependencies` and update the range to `~16.2.0` 22 | * Allow the use of sync generators that can yield possible Promise values 23 | 24 | ## react-coroutine v2.0.0-alpha.2 25 | 26 | * Remove `getVariables` mechanism 27 | * Remove unused `Coroutine.render` component 28 | * Introduce `Coroutine` as a base React component 29 | * Fix the issue with stale props after receiving new 30 | 31 | ## react-coroutine v1.0.6 32 | 33 | * Use latest `shallowequal` without `lodash` dependency (–3kb of minified code) 34 | 35 | ## react-coroutine v1.0.5 36 | 37 | * Fix the issue with redundant updates due to late props comparison 38 | * Fix broken reference to the current props state 39 | 40 | ## react-coroutine v1.0.3 41 | 42 | * Use `jsnext:main` instead of `pkg.module` because of Webpack 2 issue 43 | * Use `shallowequal` instead of React's internal tool 44 | 45 | ## react-coroutine v1.0.2 46 | 47 | * Fix compatibility issue with polyfilled Promises 48 | 49 | ## react-coroutine v1.0.1 50 | 51 | * Fix the usage of `contextTypes` for async functions 52 | 53 | ## react-coroutine v1.0.0 54 | 55 | * Use Rollup to build smaller bundle 56 | * Provide `pkg.module` property for bundling original sources 57 | * Prevent calls of `setState()` for unmounted components 58 | 59 | ## react-coroutine v0.6.1 60 | 61 | * Fix `.npmignore` due to lost modules after previous release 62 | 63 | ## react-coroutine v0.6.0 64 | 65 | * Provide `Coroutine.render` component to render async functions without wrapping them 66 | * Add an ability to provide custom component for the initial (empty) state 67 | * Drop outdated promises if coroutine was updated before they are resolved 68 | * Use `null` as default initial body and allow returning it from coroutines 69 | 70 | ## react-coroutine v0.5.1 71 | 72 | * Fix `return` statement usage in async generators 73 | 74 | ## react-coroutine v0.5.0 75 | 76 | * Use a better approach to check if component is mounted 77 | 78 | ## react-coroutine v0.4.1 79 | 80 | * Fixed weird publish issue 81 | 82 | ## react-coroutine v0.4.0 83 | 84 | * Drop outdated body before fetching new one 85 | 86 | ## react-coroutine v0.3.2 87 | 88 | * Fixed incorrect props comparison 89 | * Fixed re-render logic when new props are received 90 | 91 | ## react-coroutine v0.3.1 92 | 93 | * Cancel async iterator when a component was unmounted or received new props 94 | 95 | ## react-coroutine v0.3.0 96 | 97 | * Removed `invariant` dependency 98 | * Call `getVariables()` with `props` and `context` passed in 99 | * Allowed the usage of async generators as components 100 | 101 | ## react-coroutine v0.2.0 102 | 103 | * Set correct and transpiled main file 104 | * Reduced amount of files on package install 105 | 106 | ## react-coroutine v0.1.0 107 | 108 | Initial public version. 109 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at oleksii.raspopov@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | Greetings! I'm glad that you are interested in contributing to this project. 4 | 5 | Before submitting your contribution though, please take a moment and read 6 | through the following guidelines. 7 | 8 | ## Issue Reporting Guidelines 9 | 10 | * You are free to open an issue with any question you have. This helps us to 11 | improve the docs and make the project more developers-friendly. 12 | * Make sure you question has not been answered before in other issues or in 13 | the docs. 14 | * Please provide an environment or list of steps to reproduce the bug you've 15 | found. You can attach a link to a repo or gist that has all the sources needed 16 | for reproducing. 17 | 18 | ## Pull Request Guidelines 19 | 20 | * Feel free to open pull requests against `master` branch. 21 | * Provide descriptive explanation of the things you want to fix, improve, or 22 | change. 23 | * Create new automated tests for bug fixes, to ensure the effect of introduced 24 | changes and ability to avoid regressions. 25 | * Keep git history clear and readable. No "ugh linter again" commits. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-present Alexey Raspopov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Coroutine 2 | 3 | npm install react-coroutine 4 | 5 | > **Coroutines** are computer program components that generalize subroutines for nonpreemptive multitasking, by allowing multiple entry points for suspending and resuming execution at certain locations. Coroutines are well-suited for implementing more familiar program components such as cooperative tasks, exceptions, event loop, iterators, infinite lists and pipes. 6 | > — _[Wikipedia](https://en.wikipedia.org/wiki/Coroutine)_ 7 | 8 | Describe complex async state flows in your React components using only language 9 | features like [generators][1], [async functions][2], and [async generators][3]. 10 | 11 | No API or new abstractions to learn, only JavaScript code as it intended to be. 12 | 13 | ## Motivation 14 | 15 | React Coroutine attempts to use basic and known language features for the sake 16 | of solving problems that are usually solved with APIs and new abstractions that 17 | require particular knowledge about them or, sometimes, about internal processes. 18 | 19 | ## Examples 20 | 21 | ```javascript 22 | import React from 'react'; 23 | import Coroutine from 'react-coroutine'; 24 | ``` 25 | 26 | ```javascript 27 | async function UserListContainer() { 28 | try { 29 | // Wait for async data and render it in the same way as plain components 30 | let users = await Users.retrieve(); 31 | return ; 32 | } catch (error) { 33 | // Handle failures in place with just JavaScript tools 34 | return ; 35 | } 36 | } 37 | 38 | export default Coroutine.create(UserListContainer); 39 | ``` 40 | 41 | ```javascript 42 | async function* PokemonInfoPage({ pokemonId, pokemonName }) { 43 | // Use generators to provide multiple render points of your async component 44 | yield

Loading {pokemonName} info...

; 45 | 46 | // Easily import components asynchronously and render them on demand 47 | let { default: PokemonInfo } = await import('./PokemonInfo.react'); 48 | let data = await PokemonAPI.retrieve(pokemonId); 49 | 50 | return ; 51 | } 52 | 53 | export default Coroutine.create(PokemonInfoPage); 54 | ``` 55 | 56 | ```javascript 57 | function* MovieInfoLoader({ movieId }) { 58 | // Assuming cache.read() return a value from cache or Promise 59 | let movieData = yield movieCache.read(movieId); 60 | return ; 61 | } 62 | 63 | export default Coroutine.create(MovieInfoLoader); 64 | ``` 65 | 66 | ## Documentation 67 | 68 | See [details page](https://react-coroutine.js.org/Details.html) for more. 69 | 70 | ## Installation 71 | 72 | React Coroutine project is available as the `react-coroutine` package on NPM. 73 | Installed package includes precompiled code (ECMAScript 5), ES Modules-friendly 74 | artifact, [LICENSE](./LICENSE), and [the changelog](./CHANGELOG.md). 75 | 76 | ## Contributing 77 | 78 | Current project has adopted a [Code of Conduct](./CODE_OF_CONDUCT.md) which is 79 | expected to be adhered by project participants. Please also visit [the document 80 | website](https://www.contributor-covenant.org/) to learn more. 81 | 82 | Please read [the contributing guide](./CONTRIBUTING.md) to learn how to propose 83 | bug fixes and improvements, and how to build and test your changes. 84 | 85 | [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function* 86 | [2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function 87 | [3]: https://github.com/tc39/proposal-async-iteration 88 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | react-coroutine.js.org 2 | -------------------------------------------------------------------------------- /docs/Details.md: -------------------------------------------------------------------------------- 1 | In the world full of APIs, we starting to forget the power of plain JavaScript 2 | and how essential patterns eliminate the need in providing new abstractions. 3 | 4 | The power of coroutines allows to write code in synchronous style and be able 5 | to pause it or partially postpone its execution. This essential idea brought us 6 | [Async Functions](https://github.com/tc39/ecmascript-asyncawait) which are 7 | already included in the language. Async functions allow us to get rid of 8 | complex Promises API and use plain JavaScript instructions. 9 | 10 | async function doSomethingComplex(data) { 11 | try { 12 | let response = await postData(data); 13 | let status = await getStatus(response.headers.Location); 14 | return status; 15 | } catch (error) { 16 | notify('Unable to perform the action', error); 17 | } 18 | } 19 | 20 | Since React allows us to treat the UI as a first-class citizen, we can mix both 21 | things for the sake of solving problems that are the same for React and for 22 | just JavaScript code. 23 | 24 | This project tends to use the simplicity of functional React components and the 25 | essential mechanism of coroutines to create stateful components with data 26 | fetching colocation. 27 | 28 | The problem of existent solutions in colocating data fetching is an initial 29 | complexity of their APIs. These APIs usually tend to provide convenient way for 30 | handling one particular use case. This often means possible future issues with 31 | handling exceptions or dealing with pending state. However, that's something 32 | that can be easily described in terms of the language and may be different 33 | based on your opinion or particular task. 34 | 35 | ## How it works: async functions 36 | 37 | When an async component is mounted, async function is executed. Initially, 38 | mounted component will render nothing, since async function hasn't been 39 | resolved or rejected yet. You can set your `placeholder` for the pending state, 40 | check Dependency Injection docs below. Once async function is resolved, the 41 | thing it returned will be rendered instead of placeholder. Whenever you pass 42 | new props to an async component it will switch to pending state and execute 43 | async function again. 44 | 45 | ## How it works: async generators 46 | 47 | In the same way as async functions work, async generators are executed when a 48 | component is mounted. In addition, you can produce content more than once by 49 | using `yield` keyword. You can find a good example of `yield` keyword usage on 50 | [examples page](/Examples.html). 51 | 52 | async function* MultipleStepsRender() { 53 | yield

Loading...

; 54 | 55 | let firstPart = await fetchSomeData(); 56 | yield ; 57 | 58 | let secondPart = await fetchMoreData(); 59 | return ; 60 | } 61 | 62 | Worth mentioning, `for..await` also can be used for producing content over time. 63 | 64 | async function* EventMonitor({ stream }) { 65 | for await (let event of stream) 66 | yield ; 67 | } 68 | -------------------------------------------------------------------------------- /docs/Examples.md: -------------------------------------------------------------------------------- 1 | Next list of small code examples attempts to show the advantages of React Coroutine in solving different tasks. 2 | 3 | ## [Code Splitting](https://github.com/alexeyraspopov/react-coroutine/blob/master/examples/code-splitting) 4 | 5 | An example of coroutines usage for async data fetching and dynamic imports. 6 | 7 | import React from 'react'; 8 | import Coroutine from 'react-coroutine'; 9 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 10 | import Pokemons from './PokemonAPI'; 11 | 12 | export default ( 13 | 14 |
15 |

Pokemons

16 | 17 | 18 |
19 |
20 | ); 21 | 22 | async function PokemonListLoader() { 23 | let { default: PokemonList } = await import('./PokemonList'); 24 | return ; 25 | } 26 | 27 | async function* PokemonInfoLoader({ match }) { 28 | let module = import('./PokemonInfo'); 29 | let pokemonInfo = Pokemons.retrieve(match.params.pokemonId); 30 | yield

Loading...

; 31 | let [{ default: PokemonInfo }, data] = await Promise.all([module, pokemonInfo]); 32 | return ; 33 | } 34 | 35 | ## [Search Form](https://github.com/alexeyraspopov/react-coroutine/blob/master/examples/search-form) 36 | 37 | An example of progressive rendering with loading spinner and requests debouncing. 38 | 39 | import React from 'react'; 40 | import Coroutine from 'react-coroutine'; 41 | import SearchAPI from './SearchAPI'; 42 | import SearchResults from './SearchResults'; 43 | import ErrorMessage from './ErrorMessage'; 44 | 45 | export default Coroutine.create(SearchForm); 46 | 47 | async function* SearchForm({ query }) { 48 | if (query.length === 0) return null; 49 | 50 | yield

Searching {query}...

; 51 | 52 | try { 53 | let { results } = await SearchAPI.retrieve(query); 54 | return ; 55 | } catch (error) { 56 | return ; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /docs/GettingStarted.md: -------------------------------------------------------------------------------- 1 | To install React Coroutine in your project, use [NPM][1] or [Yarn][2]. The lib 2 | is available as [`react-coroutine`][3]: 3 | 4 | # using npm 5 | npm install --save react-coroutine 6 | 7 | # or using yarn 8 | yarn add react-coroutine 9 | 10 | The library has React defined as a [peer dependency][4]. This means, you need 11 | to install React separately. React Coroutine follows [semantic versioning][5] 12 | so no breaking changes should be expected between minor or patch releases. 13 | 14 | Whenever you want to define React component using generator, async generator, 15 | or async function, you need to import the factory function that is used in the 16 | same way as you use other [higher-order components][6]. No options or specific 17 | arguments required, just a coroutine that defines the workflow and outputs JSX. 18 | 19 | // UserProfilePage.js 20 | import React from 'react'; 21 | import Coroutine from 'react-coroutine'; 22 | 23 | export default Coroutine.create(UserProfilePage); 24 | 25 | async function UserProfilePage({ userId }) { 26 | let userInfo = await retrieveUserInfo(userId); 27 | let { default: UserProfile } = await import('./UserProfile'); 28 | return ; 29 | } 30 | 31 | Once defined, the component can be used in the same way as stateless components 32 | are used. For example, the use of component above as a root of a route 33 | definition (using [React Router][7]). 34 | 35 | // App.js 36 | import React from 'react'; 37 | import { BrowserRouter, Route } from 'react-router-dom'; 38 | import UserProfilePage from './UserProfilePage'; 39 | 40 | export default function App() { 41 | return ( 42 | 43 | 44 | {match => } 45 | 46 | 47 | ); 48 | } 49 | 50 | [1]: https://www.npmjs.com/ 51 | [2]: https://yarnpkg.com/ 52 | [3]: https://www.npmjs.com/package/react-coroutine 53 | [4]: https://nodejs.org/en/blog/npm/peer-dependencies/ 54 | [5]: https://semver.org/ 55 | [6]: https://reactjs.org/docs/higher-order-components.html 56 | [7]: https://reacttraining.com/react-router/ 57 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | React Coroutine is a small library which leverages the power of modern 2 | JavaScript to provide seamless way in creating stateful components with 3 | different purposes. 4 | 5 | This library just use a coroutine as a React component. No API to learn and 6 | keep up to date, no additional workflows or blackboxes to worry about. 7 | 8 | ## Install 9 | 10 | Use [NPM](https://www.npmjs.com/) or [Yarn](https://yarnpkg.com/) to install 11 | the library from NPM [registry](https://www.npmjs.com/package/react-coroutine). 12 | 13 | npm install react-coroutine 14 | 15 | ## Usage 16 | 17 | import React from 'react'; 18 | import Coroutine from 'react-coroutine'; 19 | import Posts from 'PostAPI'; 20 | import PostList from 'PostList.react'; 21 | 22 | async function PostListCo() { 23 | try { 24 | const posts = await Posts.retrieve(); 25 | return ; 26 | } catch (error) { 27 | return

Unable to fetch posts.

; 28 | } 29 | } 30 | 31 | export default Coroutine.create(PostListCo); 32 | 33 | ## Requirements 34 | 35 | Using latest `babel-preset-env` you're able to use React Coroutine with async 36 | functions. This also covers current version of [Create React App][cra]. You may 37 | need to add `babel-preset-stage-3` to your setup to be able to use async 38 | generators. 39 | 40 | ## License 41 | 42 | React Coroutine licensed under [the MIT][mit]. 43 | 44 | The MIT License places almost no restrictions on what you can do with this lib. 45 | You are free to use it in commercial projects as long as the copyright is left 46 | intact. 47 | 48 | ## Credits 49 | 50 | Amazing [awsm.css](https://igoradamenko.github.io/awsm.css) built by 51 | [Igor Adamenko](https://igoradamenko.com/) was used for making the website. 52 | 53 | Code examples use [FiraCode](https://github.com/tonsky/FiraCode) font family. 54 | 55 | [cra]: https://github.com/facebook/create-react-app 56 | [mit]: https://github.com/alexeyraspopov/react-coroutine/blob/master/LICENSE 57 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: React Coroutine 2 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ page.title | default: site.title }} 8 | 9 | 10 | 58 | 59 | 60 |
61 |

React Coroutine

62 |

Async components made easy. No API, just language features.

63 |
64 | 65 | 74 | 75 |
{{ content }}
76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /docs/assets/highlight.js: -------------------------------------------------------------------------------- 1 | const codeSnippets = document.querySelectorAll('pre'); 2 | 3 | const whiteListIdentifiers = ['create', 'retrieve', 'update', 'destroy', 'render']; 4 | const reservedWords = ['class', 'extends', 'return', 'throw', 'yield', 'new', 'function', 'async', 'await', 5 | 'for', 'if', 'of', 'switch', 'case', 'default', 'this', 'const', 'let', 6 | 'var', 'true', 'false', 'try', 'catch', 'finally', 'static', 'import', 7 | 'from', 'export', 'default']; 8 | 9 | for (const snippet of codeSnippets) { 10 | for (const textNode of getTextNodes(snippet)) { 11 | transform(textNode); 12 | } 13 | } 14 | 15 | function transform(textNode) { 16 | const words = textNode.textContent.split(/\b/); 17 | const fragment = document.createDocumentFragment(); 18 | 19 | const transformed = words.map(word => { 20 | if (whiteListIdentifiers.includes(word)) { 21 | return wrapTextNode(word, 'primary'); 22 | } 23 | 24 | if (reservedWords.includes(word)) { 25 | return wrapTextNode(word, 'secondary'); 26 | } 27 | 28 | return document.createTextNode(word); 29 | }); 30 | 31 | transformed.forEach(node => fragment.appendChild(node)); 32 | requestAnimationFrame(() => textNode.parentNode.replaceChild(fragment, textNode)); 33 | } 34 | 35 | function* getTextNodes(root) { 36 | const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode }); 37 | let node; 38 | 39 | while (node = walker.nextNode()) { 40 | yield node; 41 | } 42 | } 43 | 44 | function acceptNode(node) { 45 | if (node.textContent.trim().length > 0) { 46 | return NodeFilter.FILTER_ACCEPT; 47 | } 48 | 49 | return NodeFilter.FILTER_REJECT; 50 | } 51 | 52 | function wrapTextNode(text, type) { 53 | const wrapper = document.createElement('span'); 54 | 55 | wrapper.setAttribute('class', `${type}-accent`); 56 | wrapper.appendChild(document.createTextNode(text)); 57 | 58 | return wrapper; 59 | } 60 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # React Coroutine / Examples 2 | 3 | This folder contains several examples of react-coroutine applied to different and non-trivial cases. 4 | 5 | * [Code Splitting](./code-splitting) — an example of coroutines usage for async data fetching and dynamic imports. 6 | * [Search Form](./search-form) — an example of progressive rendering with loading spinner and requests debouncing. 7 | * [Timing Motion](./timing-motion) — an example of using React Coroutine and React Motion 8 | * [Unidirectional Dataflow](./unidirectional-dataflow) — an example of state management solution and unidirectional dataflow described without additional libraries. 9 | -------------------------------------------------------------------------------- /examples/code-splitting/README.md: -------------------------------------------------------------------------------- 1 | # code-splitting 2 | 3 | React Coroutine used for code splitting. 4 | 5 | ## How to start 6 | 7 | git clone --depth=1 git@github.com:alexeyraspopov/react-coroutine.git 8 | cd react-coroutine/examples/code-splitting 9 | npm install 10 | npm start 11 | 12 | ## Code 13 | 14 | Please read [`modules/Routes.js`](./modules/Routes.js) for the explanation of this example. 15 | -------------------------------------------------------------------------------- /examples/code-splitting/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code Splitting Example 5 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/code-splitting/index.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import Routes from './modules/Routes'; 3 | 4 | ReactDOM.render(Routes, document.querySelector('main')); 5 | -------------------------------------------------------------------------------- /examples/code-splitting/modules/Placeholder.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Coroutine from 'react-coroutine'; 3 | 4 | export default Coroutine.create(Placeholder); 5 | 6 | async function Placeholder({ delay, children }) { 7 | await new Promise(resolve => setTimeout(resolve, delay)); 8 | return children; 9 | } 10 | -------------------------------------------------------------------------------- /examples/code-splitting/modules/PokemonAPI.js: -------------------------------------------------------------------------------- 1 | class Pokemons { 2 | async retrieve(id) { 3 | let response = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}/`); 4 | return response.json(); 5 | } 6 | } 7 | 8 | export default new Pokemons(); 9 | -------------------------------------------------------------------------------- /examples/code-splitting/modules/PokemonInfo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | export default function PokemonInfo({ data }) { 5 | return ( 6 |
7 | ← Back 8 |

{data.name}

9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/code-splitting/modules/PokemonList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const pokemons = [ 5 | { id: 1, name: "Bulbasaur" }, 6 | { id: 2, name: "Ivysaur" }, 7 | { id: 3, name: "Venusaur" }, 8 | { id: 4, name: "Charmander" }, 9 | { id: 5, name: "Charmeleon" }, 10 | { id: 6, name: "Charizard" }, 11 | { id: 7, name: "Squirtle" }, 12 | { id: 8, name: "Wartortle" }, 13 | { id: 9, name: "Blastoise" }, 14 | ]; 15 | 16 | export default function PokemonList() { 17 | return ( 18 |
19 |
    20 | {pokemons.map(pokemon => ( 21 |
  • 22 | {pokemon.name} 23 |
  • 24 | ))} 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /examples/code-splitting/modules/Routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Coroutine from 'react-coroutine'; 3 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 4 | import Pokemons from './PokemonAPI'; 5 | import Placeholder from './Placeholder'; 6 | 7 | /* Routes are using wrapped by Coroutine components for the sake of 8 | async functions and generators usage. */ 9 | export default ( 10 | 11 |
12 |

Pokemons

13 | 14 | 15 |
16 |
17 | ); 18 | 19 | /* Async function that is used as a component and provides 20 | actual `PokemonList` once it becomes imported via async `import()`. */ 21 | async function PokemonListLoader() { 22 | /* Module is an object that keeps all exports from particular file. 23 | You can think about the result as `import * as module from '...'`.*/ 24 | let { default: PokemonList } = await import('./PokemonList'); 25 | return ; 26 | } 27 | 28 | /* Async generator that is used as a component for /:pokemonId page. 29 | It imports `PokemonInfo` component and fetches particular pokemon data 30 | using API. */ 31 | async function* PokemonInfoLoader({ match }) { 32 | /* This component is rendered every time the user opens a pokemon profile. 33 | However, `PokemonInfo` component will be loaded only once. After first 34 | usage `import('./PokemonInfo')` just returns resolved promise with module. */ 35 | let module = import('./PokemonInfo'); 36 | /* This request can also be cached but that's API's implementation detail. 37 | For the example purpose, it just does new request all the time. */ 38 | let pokemonInfo = Pokemons.retrieve(match.params.pokemonId); 39 | /* Since API request takes time sometimes, we show a pending message 40 | and then wait for requests resolving. */ 41 | yield ( 42 | 43 |

Loading...

44 |
45 | ); 46 | /* Promise.all is used pretty much for example purpose. However, it's 47 | efficient way to make concurrent requests. */ 48 | let [{ default: PokemonInfo }, data] = await Promise.all([module, pokemonInfo]); 49 | return ; 50 | } 51 | -------------------------------------------------------------------------------- /examples/code-splitting/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-splitting", 3 | "version": "0.0.1", 4 | "description": "Code splitting example with React Coroutine", 5 | "main": "index.js", 6 | "dependencies": { 7 | "react": "~16.2.0", 8 | "react-coroutine": "~2.0.0", 9 | "react-dom": "~16.2.0", 10 | "react-router-dom": "~4.2.2" 11 | }, 12 | "devDependencies": { 13 | "babel-core": "~6.24.0", 14 | "babel-loader": "~6.4.1", 15 | "babel-preset-es2017": "~6.22.0", 16 | "babel-preset-react": "~6.23.0", 17 | "babel-preset-stage-2": "~6.22.0", 18 | "http-server": "~0.9.0", 19 | "uglify-js": "github:mishoo/uglifyjs2#harmony", 20 | "webpack": "~2.3.1", 21 | "webpack-dev-server": "~2.4.1" 22 | }, 23 | "babel": { 24 | "presets": [ 25 | "react", 26 | "es2017", 27 | "stage-2" 28 | ] 29 | }, 30 | "scripts": { 31 | "build": "webpack index.js bundle.js -p", 32 | "serve": "http-server . -p 8080", 33 | "start": "webpack-dev-server . -d" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/code-splitting/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | module: { 3 | rules: [ 4 | { test: /js$/, exclude: /node_modules/, 5 | use: [{ loader: 'babel-loader' }] } 6 | ] 7 | }, 8 | devServer: { 9 | historyApiFallback: true, 10 | publicPath: '/' 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /examples/search-form/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "stage-3", 5 | "react" 6 | ], 7 | "plugins": [ 8 | "transform-runtime" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /examples/search-form/README.md: -------------------------------------------------------------------------------- 1 | # search-form 2 | 3 | React Coroutine used for search functionality. 4 | 5 | ## How to start 6 | 7 | git clone --depth=1 git@github.com:alexeyraspopov/react-coroutine.git 8 | cd react-coroutine/examples/search-form 9 | npm install 10 | npm start 11 | 12 | ## Code 13 | 14 | Please read [`modules/SearchForm.js`](./modules/SearchForm.js) and [`modules/SearchAPI.js`](./modules/SearchAPI.js) for the explanation of this example. 15 | -------------------------------------------------------------------------------- /examples/search-form/modules/SearchAPI.js: -------------------------------------------------------------------------------- 1 | class Search { 2 | constructor() { 3 | /* The method is debounced for the sake of not doing unnecessary HTTP requests. */ 4 | this.retrieve = debounce(this.retrieve.bind(this)); 5 | } 6 | 7 | async retrieve(query) { 8 | /* npms.io search API is used in this example. Good stuff.*/ 9 | let response = await fetch(`https://api.npms.io/v2/search?from=0&size=25&q=${query}`); 10 | 11 | if (response.ok) { 12 | return response.json(); 13 | } 14 | 15 | /* If API returns some weird stuff and not 2xx, convert it to error and show 16 | on the screen. */ 17 | throw new Error(await response.text()); 18 | } 19 | } 20 | 21 | function debounce(fn, delay = 400) { 22 | let timer = null; 23 | let resolver = null; 24 | 25 | function resolveFn(args) { 26 | resolver(fn(...args)); 27 | timer = null; 28 | resolver = null; 29 | } 30 | 31 | return function(...args) { 32 | return new Promise(resolve => { 33 | if (timer) { 34 | clearTimeout(timer); 35 | timer = null; 36 | resolver = null; 37 | } 38 | 39 | timer = setTimeout(resolveFn, delay, args); 40 | resolver = resolve; 41 | }); 42 | }; 43 | } 44 | 45 | export default new Search(); 46 | -------------------------------------------------------------------------------- /examples/search-form/modules/SearchForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Coroutine from 'react-coroutine'; 3 | import SearchAPI from './SearchAPI'; 4 | 5 | /* A coroutine becomes a React component via this wrapper. */ 6 | export default Coroutine.create(SearchForm); 7 | 8 | /* Async generator is used as a component that represents a stateful component 9 | with search results. The same rules are applicable as to functional component. 10 | The main difference comparing to plain functional component is that async generator 11 | yields particular UI state and then performs additional actions (awaitg for data) 12 | to create and yield new UI state. */ 13 | /* If you don't know what the thing is async generator, check the TC39 proposal: 14 | https://github.com/tc39/proposal-async-iteration#async-generator-functions */ 15 | async function* SearchForm({ query }) { 16 | /* Not really important. There is nothing to show if query is empty. */ 17 | if (query.length === 0) return null; 18 | 19 | /* This call does not finish the execution of the component. It just provides a 20 | state of UI and then doing another stuff. */ 21 | yield

Searching {query}...

; 22 | 23 | try { 24 | /* This piece is the same as with async functions. Some data is fetched and 25 | used with another plain functional component. */ 26 | let { results } = await SearchAPI.retrieve(query); 27 | return ; 28 | } catch (error) { 29 | return ; 30 | } 31 | } 32 | 33 | function SearchResults({ results }) { 34 | return results.length === 0 ? ( 35 |

No results

36 | ) : ( 37 |
    38 | {results.map((result) => ( 39 |
  • 40 |

    {result.package.name} ({result.package.version})

    41 |

    {result.package.description}

    42 |
  • 43 | ))} 44 |
45 | ); 46 | } 47 | 48 | function ErrorMessage({ error }) { 49 | return ( 50 |
51 | Something went wrong! 52 |

{error.message}

53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /examples/search-form/modules/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import SearchForm from './SearchForm'; 4 | 5 | export default class App extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { query: '' }; 9 | this.handleEvent = this.handleEvent.bind(this); 10 | } 11 | 12 | handleEvent(event) { 13 | let query = event.target.value; 14 | this.setState(() => ({ query })); 15 | } 16 | 17 | render() { 18 | return ( 19 |
20 |

NPM Search

21 | 25 | 26 |
27 | ); 28 | } 29 | } 30 | 31 | ReactDOM.render(, document.querySelector('main')); 32 | -------------------------------------------------------------------------------- /examples/search-form/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "description": "Search functionality example with React Coroutine", 4 | "license": "MIT", 5 | "dependencies": { 6 | "react": "~16.3.0", 7 | "react-coroutine": "~2.0.2", 8 | "react-dom": "~16.3.0" 9 | }, 10 | "devDependencies": { 11 | "babel-plugin-transform-runtime": "^6.23.0", 12 | "babel-preset-env": "^1.7.0", 13 | "babel-preset-react": "^6.24.1", 14 | "babel-preset-stage-3": "^6.24.1", 15 | "parcel-bundler": "^1.9.7" 16 | }, 17 | "scripts": { 18 | "start": "parcel public/index.html" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/search-form/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Search Form Example 5 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/timing-motion/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "stage-3" 5 | ], 6 | "plugins": [ 7 | "transform-runtime" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /examples/timing-motion/README.md: -------------------------------------------------------------------------------- 1 | # timing-motion 2 | 3 | React Coroutine used for search functionality. 4 | 5 | ## How to start 6 | 7 | git clone --depth=1 git@github.com:alexeyraspopov/react-coroutine.git 8 | cd react-coroutine/examples/unidirectional-dataflow 9 | npm install 10 | npm start 11 | 12 | ## Code 13 | 14 | Please read [`index.js`](./index.js), and [`modules/App.js`](./modules/App.js) 15 | for the explanation of this example. 16 | -------------------------------------------------------------------------------- /examples/timing-motion/modules/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Coroutine from 'react-coroutine'; 3 | import { Motion, spring } from 'react-motion'; 4 | import { AsyncQueue } from 'async-structure'; 5 | 6 | export default Coroutine.create(AppCoroutine); 7 | 8 | async function* AppCoroutine() { 9 | let values = new AsyncQueue(); 10 | let onChange = value => values.enqueue(value); 11 | 12 | // enqueue initial value 13 | values.enqueue(0); 14 | 15 | for await (let counter of values) { 16 | yield ; 17 | } 18 | } 19 | 20 | function CounterView({ counter, onCounterChange }) { 21 | let onChange = event => onCounterChange(parseFloat(event.target.value)); 22 | 23 | return ( 24 |
25 | 26 | 27 | {({ x }) =>

Selected value: {x.toFixed(0)}

} 28 |
29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /examples/timing-motion/modules/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.querySelector('main')); 6 | -------------------------------------------------------------------------------- /examples/timing-motion/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timing-motion", 3 | "version": "0.0.1", 4 | "description": "Some stuff with React Motion", 5 | "license": "MIT", 6 | "main": "index.js", 7 | "dependencies": { 8 | "async-structure": "~0.1.0", 9 | "react": "~16.3.0", 10 | "react-coroutine": "~2.0.2", 11 | "react-dom": "~16.3.0", 12 | "react-motion": "~0.5.2" 13 | }, 14 | "devDependencies": { 15 | "babel-plugin-transform-runtime": "^6.23.0", 16 | "babel-preset-react": "^6.24.1", 17 | "babel-preset-stage-3": "^6.24.1", 18 | "parcel-bundler": "^1.7.0" 19 | }, 20 | "scripts": { 21 | "start": "parcel public/index.html" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/timing-motion/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Unidirectional Dataflow Example 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/unidirectional-dataflow/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "stage-3" 5 | ], 6 | "plugins": [ 7 | "transform-runtime", 8 | "transform-class-properties" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /examples/unidirectional-dataflow/README.md: -------------------------------------------------------------------------------- 1 | # unidirectional-dataflow 2 | 3 | React Coroutine used for search functionality. 4 | 5 | ## How to start 6 | 7 | git clone --depth=1 git@github.com:alexeyraspopov/react-coroutine.git 8 | cd react-coroutine/examples/unidirectional-dataflow 9 | npm install 10 | npm start 11 | 12 | ## Code 13 | 14 | Please read [`index.js`](./index.js), [`modules/App.js`](./modules/App.js), and [`modules/Counter.js`](./modules/Counter.js) for the explanation of this example. 15 | -------------------------------------------------------------------------------- /examples/unidirectional-dataflow/modules/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Coroutine from 'react-coroutine'; 3 | import AppContext from './AppContext'; 4 | 5 | export default Coroutine.create(App); 6 | 7 | async function* App({ state, actions, reduce, children }) { 8 | // Rendering initial state, immediately 9 | yield ( 10 | 11 | {children} 12 | 13 | ); 14 | // Awaiting for the next action, updating state, re-rendering 15 | for await (let action of actions) { 16 | state = reduce(state, action); 17 | yield ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/unidirectional-dataflow/modules/AppContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export default createContext(); 4 | -------------------------------------------------------------------------------- /examples/unidirectional-dataflow/modules/AppStateRecord.js: -------------------------------------------------------------------------------- 1 | import Record from 'dataclass'; 2 | 3 | export default class AppStateRecord extends Record { 4 | counter = 0; 5 | } 6 | -------------------------------------------------------------------------------- /examples/unidirectional-dataflow/modules/Counter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AppContext from './AppContext'; 3 | 4 | export default function Counter() { 5 | return ( 6 | 7 | {({ state, actions }) => ( 8 |
9 |

{ state.counter }

10 | 11 |
12 | )} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/unidirectional-dataflow/modules/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Coroutine from 'react-coroutine'; 4 | import { AsyncQueue } from 'async-structure'; 5 | import AppContext from './AppContext'; 6 | import AppStateRecord from './AppStateRecord'; 7 | import App from './App'; 8 | import Counter from './Counter'; 9 | 10 | let state = new AppStateRecord(); 11 | let actions = new AsyncQueue(); 12 | 13 | function reduce(state, action) { 14 | switch (action) { 15 | case 'INCREMENT': 16 | return state.copy({ counter: state.counter + 1 }); 17 | default: 18 | return state; 19 | } 20 | } 21 | 22 | ReactDOM.render( 23 | 24 | 25 | , 26 | document.querySelector('main') 27 | ); 28 | -------------------------------------------------------------------------------- /examples/unidirectional-dataflow/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unidirectional-dataflow", 3 | "version": "0.0.1", 4 | "description": "Unidirectional Dataflow pattern example with React Coroutine", 5 | "license": "MIT", 6 | "main": "index.js", 7 | "dependencies": { 8 | "async-structure": "~0.1.0", 9 | "dataclass": "~1.1.1", 10 | "react": "~16.3.1", 11 | "react-coroutine": "~2.0.2", 12 | "react-dom": "~16.3.1" 13 | }, 14 | "devDependencies": { 15 | "babel-plugin-transform-class-properties": "~6.24.1", 16 | "babel-plugin-transform-runtime": "~6.23.0", 17 | "babel-preset-react": "~6.24.1", 18 | "babel-preset-stage-3": "~6.24.1", 19 | "parcel-bundler": "~1.7.0" 20 | }, 21 | "scripts": { 22 | "start": "parcel public/index.html" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/unidirectional-dataflow/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Unidirectional Dataflow Example 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /modules/Coroutine.js: -------------------------------------------------------------------------------- 1 | import { PureComponent } from 'react'; 2 | 3 | export default { create }; 4 | 5 | function create(coroutine) { 6 | class Coroutine extends PureComponent { 7 | constructor(props) { 8 | super(props); 9 | this.state = { view: null }; 10 | this.iterator = null; 11 | } 12 | 13 | iterate(props) { 14 | let target = coroutine(props); 15 | 16 | this.iterator = target; 17 | 18 | let shouldStop = () => this.iterator !== target; 19 | let updateView = view => this.setState({ view }); 20 | 21 | if (target && typeof target.then === 'function') { 22 | // coroutine is Async Function, awaiting for the final result 23 | return target.then(value => shouldStop() || updateView(value)); 24 | } else { 25 | // coroutine is Async Generator, rendering every time it yields 26 | return resolveAsyncIterator(this.iterator, updateView, shouldStop); 27 | } 28 | } 29 | 30 | componentDidUpdate(prevProps) { 31 | return arePropsEqual(this.props, prevProps) || this.iterate(this.props); 32 | } 33 | 34 | componentWillUnmount() { 35 | this.iterator = null; 36 | } 37 | 38 | render() { 39 | if (this.iterator == null) { 40 | this.iterate(this.props); 41 | } 42 | 43 | return this.state.view; 44 | } 45 | } 46 | 47 | Coroutine.displayName = coroutine.name || coroutine.displayName || 'Anonymous'; 48 | 49 | return Coroutine; 50 | } 51 | 52 | function resolveAsyncIterator(iterator, done, shouldStop) { 53 | return iterator.next().then(data => { 54 | if (shouldStop()) { 55 | return iterator.return(); 56 | } 57 | 58 | if (data.value !== undefined) { 59 | done(data.value); 60 | } 61 | 62 | return !data.done && resolveAsyncIterator(iterator, done, shouldStop); 63 | }); 64 | } 65 | 66 | function arePropsEqual(a, b) { 67 | return keysEqual(a, b) || keysEqual(b, a); 68 | } 69 | 70 | function keysEqual(a, b) { 71 | for (let k in a) { 72 | if (a.hasOwnProperty(k)) { 73 | if (!b.hasOwnProperty(k) || a[k] !== b[k]) { 74 | return false; 75 | } 76 | } 77 | } 78 | 79 | return true; 80 | } 81 | -------------------------------------------------------------------------------- /modules/__tests__/Coroutine-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Renderer from 'react-test-renderer'; 3 | import Coroutine from '../Coroutine'; 4 | 5 | describe('Coroutine', () => { 6 | it('should render empty body until coroutine is resolved', async () => { 7 | async function render() { 8 | return

test

; 9 | } 10 | 11 | let TestComponent = Coroutine.create(render); 12 | let tree = Renderer.create(); 13 | 14 | expect(tree.toJSON()).toEqual(null); 15 | 16 | let success = await Renderer.create(

test

); 17 | expect(tree.toJSON()).toEqual(success.toJSON()); 18 | }); 19 | 20 | it('should render each step of async iterator', async () => { 21 | async function* render() { 22 | yield

Loading...

; 23 | await Promise.resolve(); 24 | return

Done!

; 25 | } 26 | 27 | let TestComponent = Coroutine.create(render); 28 | let tree = await Renderer.create(); 29 | 30 | let first = await Renderer.create(

Loading...

); 31 | expect(tree.toJSON()).toEqual(first.toJSON()); 32 | 33 | let second = await Renderer.create(

Done!

); 34 | expect(tree.toJSON()).toEqual(second.toJSON()); 35 | }); 36 | 37 | it('should rethrow exceptions back to async generator', async () => { 38 | async function* render() { 39 | try { 40 | await Promise.reject(new Error('Boom')); 41 | return

Hello

; 42 | } catch (error) { 43 | return

{error.message}

; 44 | } 45 | } 46 | 47 | let TestComponent = Coroutine.create(render); 48 | let tree = await Renderer.create(); 49 | 50 | let result = await Renderer.create(

Boom

); 51 | expect(tree.toJSON()).toEqual(result.toJSON()); 52 | }); 53 | 54 | it('should restart coroutine on new props', async () => { 55 | let getData = jest.fn(n => Promise.resolve(n * 2)); 56 | let trap = jest.fn(); 57 | 58 | async function* render({ number }) { 59 | let data = await getData(number); 60 | yield

{data}

; 61 | await Promise.resolve(); 62 | yield

Another

; 63 | trap(); 64 | return

Done

; 65 | } 66 | 67 | let TestComponent = Coroutine.create(render); 68 | let tree = await Renderer.create(); 69 | 70 | let first = await Renderer.create(

26

); 71 | expect(tree.toJSON()).toEqual(first.toJSON()); 72 | expect(getData).toHaveBeenCalledWith(13); 73 | 74 | await tree.update(); 75 | 76 | let second = await Renderer.create(

40

); 77 | expect(tree.toJSON()).toEqual(second.toJSON()); 78 | expect(getData).toHaveBeenCalledWith(20); 79 | expect(trap).not.toHaveBeenCalled(); 80 | }); 81 | 82 | it('should do nothing about the same props', async () => { 83 | let trap = jest.fn(); 84 | 85 | async function render({ number }) { 86 | trap(); 87 | return

Test

; 88 | } 89 | 90 | let TestComponent = Coroutine.create(render); 91 | let tree = await Renderer.create(); 92 | 93 | tree.update(); 94 | 95 | expect(trap).toHaveBeenCalledTimes(1); 96 | }); 97 | 98 | it('should cancel async iterator on unmount', async () => { 99 | let getData = jest.fn(n => Promise.resolve(n * 2)); 100 | let trap = jest.fn(); 101 | 102 | async function* render({ number }) { 103 | let data = await getData(number); 104 | yield

{data}

; 105 | await Promise.resolve(); 106 | yield

Another {trap()}

; 107 | return

Done

; 108 | } 109 | 110 | let TestComponent = Coroutine.create(render); 111 | let tree = await Renderer.create(); 112 | 113 | let first = await Renderer.create(

26

); 114 | expect(tree.toJSON()).toEqual(first.toJSON()); 115 | expect(getData).toHaveBeenCalledWith(13); 116 | 117 | tree.unmount(); 118 | expect(trap).not.toHaveBeenCalled(); 119 | }); 120 | }); 121 | 122 | // describe('Placeholder', () => { 123 | // 124 | // }); 125 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-coroutine", 3 | "version": "2.0.2", 4 | "description": "React Components as Coroutines", 5 | "author": "Alexey Raspopov", 6 | "license": "MIT", 7 | "homepage": "https://react-coroutine.js.org/", 8 | "repository": "alexeyraspopov/react-coroutine", 9 | "main": "build/react-coroutine.js", 10 | "module": "build/react-coroutine.module.js", 11 | "files": [ 12 | "build/", 13 | "LICENSE", 14 | "CHANGELOG.md" 15 | ], 16 | "keywords": [ 17 | "async", 18 | "react", 19 | "component", 20 | "container", 21 | "coroutine", 22 | "generator", 23 | "stateful", 24 | "iterator" 25 | ], 26 | "scripts": { 27 | "test": "jest --env=node --coverage", 28 | "ci": "sh scripts/integration.sh", 29 | "prepublish": "rollup --config scripts/rollup.config.js" 30 | }, 31 | "peerDependencies": { 32 | "react": "^16.2.0" 33 | }, 34 | "devDependencies": { 35 | "babel-core": "~6.26.0", 36 | "babel-jest": "~22.4.1", 37 | "babel-plugin-external-helpers": "~6.22.0", 38 | "babel-preset-env": "~1.6.1", 39 | "babel-preset-react": "~6.24.1", 40 | "babel-preset-stage-3": "~6.24.1", 41 | "jest": "~22.4.2", 42 | "jest-junit": "~3.6.0", 43 | "react": "~16.3.0", 44 | "react-test-renderer": "~16.3.0", 45 | "rollup": "~0.56.5", 46 | "rollup-plugin-babel": "~3.0.3", 47 | "size-limit": "~0.16.1" 48 | }, 49 | "babel": { 50 | "presets": [ 51 | [ 52 | "env", 53 | { 54 | "targets": { 55 | "node": "8.9.4" 56 | } 57 | } 58 | ], 59 | "react", 60 | "stage-3" 61 | ] 62 | }, 63 | "size-limit": [ 64 | { 65 | "limit": "783 B", 66 | "path": "build/react-coroutine.js" 67 | }, 68 | { 69 | "limit": "784 B", 70 | "path": "build/react-coroutine.module.js" 71 | } 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /scripts/integration.sh: -------------------------------------------------------------------------------- 1 | # Run acceptance tests 2 | jest --ci --testResultsProcessor "jest-junit" 3 | 4 | # Build lib files to ensure it's working 5 | yarn run prepublish 6 | 7 | # Check lib files size limit 8 | yarn size-limit 9 | -------------------------------------------------------------------------------- /scripts/rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | 3 | let config = { 4 | babelrc: false, 5 | presets: [ 6 | ['env', { modules: false, loose: true }], 7 | ], 8 | plugins: ['external-helpers'] 9 | }; 10 | 11 | export default [ 12 | { 13 | input: 'modules/Coroutine.js', 14 | output: { 15 | file: 'build/react-coroutine.js', 16 | format: 'cjs', 17 | }, 18 | plugins: [babel(config)], 19 | external: ['react'], 20 | }, 21 | { 22 | input: 'modules/Coroutine.js', 23 | output: { 24 | file: 'build/react-coroutine.module.js', 25 | format: 'es', 26 | }, 27 | plugins: [babel(config)], 28 | external: ['react'], 29 | }, 30 | ]; 31 | --------------------------------------------------------------------------------