├── .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 |
;
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 |
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 |