├── .all-contributorsrc ├── .circleci └── config.yml ├── .codesandbox └── ci.json ├── .eslintrc ├── .gitbook.yaml ├── .github ├── pull_request_template.md └── workflows │ └── examples.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── .storybook ├── config.js └── webpack.config.js ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── brand ├── icon-circle-bg.png ├── icon-circle-fit.png ├── icon.png ├── logo-outlined.png ├── logo-outlined@2x.png ├── logo-social.png ├── logo.png └── logo@2x.png ├── codemods ├── README.md ├── v6.js └── v8.js ├── docs ├── _summary.md ├── api │ ├── helpers.md │ ├── interfaces.md │ ├── options.md │ └── state.md ├── contributing │ ├── development.md │ ├── introduction.md │ ├── releasing.md │ ├── setting-up.md │ └── testing.md ├── getting-started │ ├── devtools.md │ ├── installation.md │ ├── upgrading.md │ └── usage.md ├── guide │ ├── async-actions.md │ ├── async-components.md │ ├── optimistic-updates.md │ ├── separating-view-logic.md │ └── server-side-rendering.md └── introduction.md ├── examples ├── .eslintrc ├── basic-fetch │ ├── .env │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ └── src │ │ ├── index.css │ │ ├── index.js │ │ └── index.test.js ├── basic-hook │ ├── .env │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ └── src │ │ ├── index.css │ │ ├── index.js │ │ └── index.test.js ├── custom-instance │ ├── .env │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ └── src │ │ ├── index.css │ │ ├── index.js │ │ └── index.test.js ├── movie-app │ ├── .env │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── api.js │ │ ├── index.css │ │ ├── index.js │ │ └── serviceWorker.js ├── with-abortcontroller │ ├── .env │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ └── src │ │ ├── index.css │ │ ├── index.js │ │ └── index.test.js ├── with-graphql │ ├── .env │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ └── src │ │ ├── index.css │ │ ├── index.js │ │ └── index.test.js ├── with-nextjs │ ├── .gitignore │ ├── README.md │ ├── next.config.js │ ├── package.json │ └── pages │ │ └── examples │ │ └── with-nextjs.js ├── with-react-native │ ├── .gitignore │ ├── .watchmanconfig │ ├── App.js │ ├── app.json │ ├── assets │ │ ├── icon.png │ │ └── splash.png │ ├── babel.config.js │ ├── package-lock.json │ └── package.json ├── with-react-router │ ├── .babelrc │ ├── README.md │ ├── index.html │ ├── index.js │ ├── js │ │ ├── AsyncRoute.js │ │ ├── Contributors.js │ │ └── Repositories.js │ └── package.json ├── with-suspense │ ├── .env │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ └── src │ │ ├── index.css │ │ ├── index.js │ │ └── index.test.js └── with-typescript │ ├── .env │ ├── README.md │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── FetchHookExample.tsx │ ├── index.css │ ├── index.tsx │ └── react-app-env.d.ts │ └── tsconfig.json ├── jest.config.js ├── jest.setup.js ├── lerna.json ├── package.json ├── packages ├── react-async-devtools │ ├── .babelrc.js │ ├── package.json │ └── src │ │ ├── components.js │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.spec.js └── react-async │ ├── .babelrc.js │ ├── package.json │ ├── src │ ├── Async.spec.js │ ├── Async.tsx │ ├── globalScope.spec.js │ ├── globalScope.ts │ ├── helpers.spec.js │ ├── helpers.tsx │ ├── index.ts │ ├── propTypes.ts │ ├── reducer.ts │ ├── specs.js │ ├── status.spec.js │ ├── status.ts │ ├── types.ts │ ├── useAsync.spec.js │ └── useAsync.tsx │ └── tsconfig.json ├── react-async.png ├── renovate.json ├── stories ├── .eslintrc ├── index.stories.js └── photos.css └── vercel.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "badgeTemplate": "/6d60e6\" alt=\"All Contributors\">", 6 | "imageSize": 75, 7 | "commit": false, 8 | "contributors": [ 9 | { 10 | "login": "ghengeveld", 11 | "name": "Gert Hengeveld", 12 | "avatar_url": "https://avatars1.githubusercontent.com/u/321738?v=4", 13 | "profile": "https://medium.com/@ghengeveld", 14 | "contributions": [ 15 | "code", 16 | "review", 17 | "question" 18 | ] 19 | }, 20 | { 21 | "login": "Khartir", 22 | "name": "Khartir", 23 | "avatar_url": "https://avatars3.githubusercontent.com/u/5592420?v=4", 24 | "profile": "https://github.com/Khartir", 25 | "contributions": [ 26 | "code", 27 | "platform" 28 | ] 29 | }, 30 | { 31 | "login": "phryneas", 32 | "name": "Lenz Weber", 33 | "avatar_url": "https://avatars1.githubusercontent.com/u/4282439?v=4", 34 | "profile": "https://twitter.com/phry", 35 | "contributions": [ 36 | "code", 37 | "platform", 38 | "ideas" 39 | ] 40 | }, 41 | { 42 | "login": "Avi98", 43 | "name": "Avinash", 44 | "avatar_url": "https://avatars1.githubusercontent.com/u/26133749?v=4", 45 | "profile": "https://github.com/Avi98", 46 | "contributions": [ 47 | "review", 48 | "doc" 49 | ] 50 | }, 51 | { 52 | "login": "FredKSchott", 53 | "name": "Fred K. Schott", 54 | "avatar_url": "https://avatars1.githubusercontent.com/u/622227?v=4", 55 | "profile": "http://www.fredkschott.com", 56 | "contributions": [ 57 | "tool" 58 | ] 59 | }, 60 | { 61 | "login": "byCedric", 62 | "name": "Cedric van Putten", 63 | "avatar_url": "https://avatars2.githubusercontent.com/u/1203991?v=4", 64 | "profile": "https://bycedric.com", 65 | "contributions": [ 66 | "code" 67 | ] 68 | }, 69 | { 70 | "login": "tomshane", 71 | "name": "Tom Shane", 72 | "avatar_url": "https://avatars1.githubusercontent.com/u/11005356?v=4", 73 | "profile": "https://github.com/tomshane", 74 | "contributions": [ 75 | "review" 76 | ] 77 | }, 78 | { 79 | "login": "philip-peterson", 80 | "name": "Philip Peterson", 81 | "avatar_url": "https://avatars1.githubusercontent.com/u/1326208?v=4", 82 | "profile": "http://philippeterson.com/", 83 | "contributions": [ 84 | "code" 85 | ] 86 | }, 87 | { 88 | "login": "sibelius", 89 | "name": "Sibelius Seraphini", 90 | "avatar_url": "https://avatars3.githubusercontent.com/u/2005841?v=4", 91 | "profile": "https://twitter.com/sseraphini", 92 | "contributions": [ 93 | "review" 94 | ] 95 | }, 96 | { 97 | "login": "jimthedev", 98 | "name": "Jim Cummins", 99 | "avatar_url": "https://avatars0.githubusercontent.com/u/108938?v=4", 100 | "profile": "https://jimthedev.com", 101 | "contributions": [ 102 | "review" 103 | ] 104 | }, 105 | { 106 | "login": "msokk", 107 | "name": "Mihkel Sokk", 108 | "avatar_url": "https://avatars3.githubusercontent.com/u/231978?v=4", 109 | "profile": "http://mihkel.sokk.ee", 110 | "contributions": [ 111 | "review" 112 | ] 113 | }, 114 | { 115 | "login": "brabeji", 116 | "name": "Jiří Brabec", 117 | "avatar_url": "https://avatars3.githubusercontent.com/u/2237954?v=4", 118 | "profile": "https://github.com/brabeji", 119 | "contributions": [ 120 | "code" 121 | ] 122 | }, 123 | { 124 | "login": "unorsk", 125 | "name": "Andrii U", 126 | "avatar_url": "https://avatars0.githubusercontent.com/u/25188?v=4", 127 | "profile": "https://github.com/unorsk", 128 | "contributions": [ 129 | "example" 130 | ] 131 | }, 132 | { 133 | "login": "matthisk", 134 | "name": "Matthisk Heimensen", 135 | "avatar_url": "https://avatars0.githubusercontent.com/u/602837?v=4", 136 | "profile": "http://matthisk.nl", 137 | "contributions": [ 138 | "code" 139 | ] 140 | }, 141 | { 142 | "login": "dhurlburtusa", 143 | "name": "Danny Hurlburt", 144 | "avatar_url": "https://avatars3.githubusercontent.com/u/4006431?v=4", 145 | "profile": "https://github.com/dhurlburtusa", 146 | "contributions": [ 147 | "ideas", 148 | "doc" 149 | ] 150 | }, 151 | { 152 | "login": "noelyoo", 153 | "name": "Noel Yoo", 154 | "avatar_url": "https://avatars2.githubusercontent.com/u/25740248?v=4", 155 | "profile": "https://noelyoo.github.io/resume", 156 | "contributions": [ 157 | "test", 158 | "code", 159 | "ideas" 160 | ] 161 | }, 162 | { 163 | "login": "aratcliffe", 164 | "name": "Adam Ratcliffe", 165 | "avatar_url": "https://avatars3.githubusercontent.com/u/491126?v=4", 166 | "profile": "https://github.com/aratcliffe", 167 | "contributions": [ 168 | "code" 169 | ] 170 | }, 171 | { 172 | "login": "kentcdodds", 173 | "name": "Kent C. Dodds", 174 | "avatar_url": "https://avatars0.githubusercontent.com/u/1500684?v=4", 175 | "profile": "https://kentcdodds.com", 176 | "contributions": [ 177 | "code" 178 | ] 179 | }, 180 | { 181 | "login": "walter-ind", 182 | "name": "walter-ind", 183 | "avatar_url": "https://avatars2.githubusercontent.com/u/52423075?v=4", 184 | "profile": "https://github.com/walter-ind", 185 | "contributions": [ 186 | "doc" 187 | ] 188 | }, 189 | { 190 | "login": "artdent", 191 | "name": "Jacob Lee", 192 | "avatar_url": "https://avatars3.githubusercontent.com/u/80536?v=4", 193 | "profile": "https://twitter.com/arthurdenture", 194 | "contributions": [ 195 | "code" 196 | ] 197 | }, 198 | { 199 | "login": "rokoroku", 200 | "name": "Youngrok Kim", 201 | "avatar_url": "https://avatars1.githubusercontent.com/u/5208632?v=4", 202 | "profile": "http://rokoroku.github.io", 203 | "contributions": [ 204 | "code" 205 | ] 206 | }, 207 | { 208 | "login": "elsangedy", 209 | "name": "Munir Ahmed Elsangedy", 210 | "avatar_url": "https://avatars3.githubusercontent.com/u/5339664?v=4", 211 | "profile": "https://munir.dev", 212 | "contributions": [ 213 | "ideas" 214 | ] 215 | }, 216 | { 217 | "login": "AlixWang", 218 | "name": "AlixWang", 219 | "avatar_url": "https://avatars0.githubusercontent.com/u/5417459?v=4", 220 | "profile": "https://github.com/AlixWang", 221 | "contributions": [ 222 | "doc" 223 | ] 224 | }, 225 | { 226 | "login": "salolivares", 227 | "name": "Sal Olivares", 228 | "avatar_url": "https://avatars0.githubusercontent.com/u/1812749?v=4", 229 | "profile": "http://salolivares.com", 230 | "contributions": [ 231 | "code", 232 | "bug" 233 | ] 234 | }, 235 | { 236 | "login": "nealeu", 237 | "name": "Neale Upstone", 238 | "avatar_url": "https://avatars1.githubusercontent.com/u/264594?v=4", 239 | "profile": "https://github.com/nealeu", 240 | "contributions": [ 241 | "code" 242 | ] 243 | }, 244 | { 245 | "login": "mbark", 246 | "name": "Martin Barksten", 247 | "avatar_url": "https://avatars1.githubusercontent.com/u/1579384?v=4", 248 | "profile": "https://github.com/mbark", 249 | "contributions": [ 250 | "doc" 251 | ] 252 | } 253 | ], 254 | "contributorsPerLine": 7, 255 | "projectName": "react-async", 256 | "projectOwner": "async-library", 257 | "repoType": "github", 258 | "repoHost": "https://github.com", 259 | "skipCi": true 260 | } 261 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | 7 | aliases: 8 | - &defaults 9 | working_directory: /tmp/react-async 10 | docker: 11 | - image: circleci/node:10 12 | 13 | jobs: 14 | build: 15 | <<: *defaults 16 | steps: 17 | - checkout 18 | - restore_cache: 19 | name: Restore root dependencies from cache 20 | keys: 21 | - root-dependencies-v1-{{ checksum "package.json" }} 22 | - run: 23 | name: Install dependencies 24 | command: yarn install 25 | - run: 26 | name: Bootstrap 27 | command: yarn bootstrap 28 | - save_cache: 29 | name: Cache root dependencies 30 | key: root-dependencies-v1-{{ checksum "package.json" }} 31 | paths: 32 | - ~/.cache/yarn 33 | - run: yarn build:packages 34 | - persist_to_workspace: 35 | root: . 36 | paths: 37 | - node_modules 38 | - packages 39 | lint: 40 | <<: *defaults 41 | steps: 42 | - checkout 43 | - attach_workspace: 44 | at: . 45 | - run: yarn lint 46 | test: 47 | <<: *defaults 48 | steps: 49 | - checkout 50 | - attach_workspace: 51 | at: . 52 | - run: 53 | name: Test with compatibility checks 54 | command: yarn test:compat 55 | - persist_to_workspace: 56 | root: . 57 | paths: 58 | - coverage 59 | coverage: 60 | <<: *defaults 61 | steps: 62 | - checkout 63 | - attach_workspace: 64 | at: . 65 | - run: bash <(curl -s https://codecov.io/bash) 66 | chromatic: 67 | <<: *defaults 68 | steps: 69 | - checkout 70 | - attach_workspace: 71 | at: . 72 | - run: | 73 | if [[ "${CIRCLE_BRANCH}" == renovate/* ]]; then 74 | echo "Skipping Chromatic" 75 | elif [ "${CIRCLE_BRANCH}" == "master" ]; then 76 | yarn test:chromatic --auto-accept-changes 77 | else 78 | yarn test:chromatic 79 | fi 80 | 81 | workflows: 82 | version: 2 83 | build_test_deploy: 84 | jobs: 85 | - build 86 | - lint: 87 | requires: 88 | - build 89 | - test: 90 | requires: 91 | - build 92 | - coverage: 93 | requires: 94 | - test 95 | - chromatic: 96 | requires: 97 | - build 98 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "build:packages", 3 | "packages": ["packages/react-async", "packages/react-async-devtools"], 4 | "publishDirectory": { 5 | "react-async": "packages/react-async/pkg", 6 | "react-async-devtools": "packages/react-async-devtools/pkg" 7 | }, 8 | "sandboxes": ["new"] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:prettier/recommended", 4 | "plugin:promise/recommended", 5 | "plugin:react/recommended" 6 | ], 7 | "parser": "babel-eslint", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | } 12 | }, 13 | "plugins": ["jest", "promise", "react", "react-hooks"], 14 | "rules": { 15 | "react-hooks/rules-of-hooks": "error", 16 | "react-hooks/exhaustive-deps": "warn", 17 | "no-console": "error" 18 | }, 19 | "settings": { 20 | "react": { 21 | "version": "detect" 22 | } 23 | }, 24 | "overrides": [{ 25 | "files": "packages/**/*.{ts,tsx}", 26 | "parser": "@typescript-eslint/parser", 27 | "plugins": ["@typescript-eslint"], 28 | "parserOptions": { 29 | "ecmaVersion": 6, 30 | "sourceType": "module", 31 | "ecmaFeatures": { 32 | "modules": true 33 | } 34 | } 35 | }] 36 | } -------------------------------------------------------------------------------- /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./docs/ 2 | 3 | structure: 4 | readme: ./introduction.md 5 | summary: ./_summary.md 6 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Describe the feature / improvement / bugfix you did. This will be in the release notes. 4 | Please refer to an issue number if there is one. 5 | 6 | ## Breaking changes 7 | 8 | Does this include any (potentially) breaking API changes? 9 | 10 | # Checklist 11 | 12 | Make sure you check all the boxes. You can omit items that are not applicable. 13 | 14 | - [ ] Implementation for both `` and `useAsync()` 15 | - [ ] Added / updated the unit tests 16 | - [ ] Added / updated the documentation 17 | - [ ] Updated the PropTypes 18 | - [ ] Updated the TypeScript type definitions 19 | -------------------------------------------------------------------------------- /.github/workflows/examples.yml: -------------------------------------------------------------------------------- 1 | name: Build, test and deploy examples 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | name: Build on node ${{ matrix.node_version }} and ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest] 12 | node: [10] 13 | steps: 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: "10.x" 17 | - uses: actions/checkout@v1 18 | - name: Cache node modules 19 | id: cache-modules 20 | uses: actions/cache@v1 21 | with: 22 | path: node_modules 23 | key: ${{ runner.OS }}-build-${{ hashFiles('**/package.json') }} 24 | restore-keys: | 25 | ${{ runner.OS }}-build-${{ env.cache-name }}- 26 | ${{ runner.OS }}-build- 27 | ${{ runner.OS }}- 28 | - name: Install 29 | if: steps.cache-modules.outputs.cache-hit != 'true' 30 | run: yarn install 31 | - name: Bootstrap 32 | run: yarn bootstrap 33 | - name: Build examples 34 | run: yarn build:examples 35 | - name: Test examples 36 | run: yarn test:examples --maxWorkers=2 37 | - name: Deploy examples 38 | if: github.ref == 'master' 39 | run: yarn deploy:examples 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | coverage/ 3 | dist/ 4 | storybook/ 5 | node_modules/ 6 | pkg/ 7 | .cache 8 | 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # these cause more harm than good 15 | # when working with contributors 16 | package-lock.json 17 | yarn.lock 18 | 19 | .vercel 20 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "semi": false, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from "@storybook/react" 2 | 3 | const req = require.context("../stories", true, /\.stories\.js$/) 4 | configure(() => req.keys().forEach(filename => req(filename)), module) 5 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = async ({ config }) => { 2 | delete config.module.rules[0].include 3 | config.module.rules.push({ 4 | test: /\.(ts|tsx)$/, 5 | loader: require.resolve('babel-loader'), 6 | options: { 7 | presets: [['react-app', { flow: false, typescript: true }]], 8 | }, 9 | }); 10 | config.resolve.extensions.push('.ts', '.tsx'); 11 | return config 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | cache: 5 | yarn: true 6 | directories: 7 | - node_modules 8 | script: yarn && yarn ci 9 | after_success: 10 | - bash <(curl -s https://codecov.io/bash) -e TRAVIS_NODE_VERSION 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to React Async 2 | 3 | Thanks for your interest in improving React Async! Contributions of any kind are welcome. Please refer to this guide before opening an issue or pull request. 4 | 5 | This repo relies on Yarn workspaces, so you should [install](https://yarnpkg.com/en/docs/install) and use `yarn@1.3.2` or higher as the package manager for this project. 6 | 7 | ## Development guide 8 | 9 | Please have the **_latest_** stable versions of the following on your machine 10 | 11 | - node 12 | - yarn 13 | 14 | ### Initial setup 15 | 16 | To start working on React Async, clone the repo and bootstrap the project: 17 | 18 | ```sh 19 | git clone https://github.com/async-library/react-async.git 20 | cd react-async 21 | yarn && yarn bootstrap && yarn test 22 | ``` 23 | 24 | Note that all work is done against the `next` branch, we only merge to `master` when doing a release. 25 | 26 | ### Working with Storybook 27 | 28 | We use Storybook as a development environment, particularly for the DevTools. Spin it up using: 29 | 30 | ```sh 31 | yarn start:storybook 32 | ``` 33 | 34 | This should open up Storybook in a browser at http://localhost:6006/ 35 | Run it side-by-side with `yarn test --watch` during development. See [Testing](#testing). 36 | 37 | ### Linting 38 | 39 | Use `yarn lint` to verify your code style before committing. It's highly recommended to install the Prettier and ESLint plugins for your IDE. Travis CI will fail your build on lint errors. Configure VS Code with the following settings: 40 | 41 | ```plaintext 42 | "eslint.autoFixOnSave": true, 43 | "eslint.packageManager": "yarn", 44 | "eslint.options": { 45 | "cache": true, 46 | "cacheLocation": ".cache/eslint", 47 | "extensions": [".js", ".jsx", ".mjs", ".json", ".ts", ".tsx"] 48 | }, 49 | "eslint.validate": [ 50 | "javascript", 51 | "javascriptreact", 52 | {"language": "typescript", "autoFix": true }, 53 | {"language": "typescriptreact", "autoFix": true } 54 | ], 55 | "eslint.alwaysShowStatus": true 56 | ``` 57 | 58 | This should enable auto-fix for all source files, and give linting warnings and errors within your editor. 59 | 60 | ### Testing 61 | 62 | Use the following command to test all packages in watch mode. Refer to the [Jest CLI options](https://jestjs.io/docs/en/cli#options) for details. 63 | 64 | ```sh 65 | yarn test:watch 66 | ``` 67 | 68 | In general, this is sufficient during development. Travis CI will apply a more rigorous set of tests. 69 | 70 | #### Testing for compatibility 71 | 72 | ```sh 73 | yarn test:compat 74 | ``` 75 | 76 | This runs all tests using various versions of `react` and `react-dom`, to check for compatibility with older/newer versions of React. This is what CircleCI and Travis run. 77 | 78 | ### Working with the examples 79 | 80 | In the `examples` folder, you will find sample React applications that use React Async in various ways with various other libraries. Please add a new example when introducing a major new feature. Make sure to add it to `now.json` so it is automatically deployed when merged to `master`. 81 | 82 | To run sample examples on your local environments 83 | 84 | ```sh 85 | yarn build:examples 86 | yarn test:examples 87 | yarn start:examples 88 | ``` 89 | 90 | ### Resolving issues 91 | 92 | Sometimes your dependencies might end up in a weird state, causing random issues, especially when working with the examples. In this case it often helps to run `yarn clean -y && yarn bootstrap`. This will delete `node_modules` from all packages/examples and do a clean install. 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2019 Gert Hengeveld 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@babel/preset-react"], 3 | plugins: ["@babel/plugin-proposal-object-rest-spread", "@babel/plugin-proposal-class-properties"], 4 | 5 | env: { 6 | test: { 7 | presets: ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"], 8 | plugins: ["@babel/plugin-transform-runtime", "@babel/plugin-proposal-class-properties"], 9 | }, 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /brand/icon-circle-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/brand/icon-circle-bg.png -------------------------------------------------------------------------------- /brand/icon-circle-fit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/brand/icon-circle-fit.png -------------------------------------------------------------------------------- /brand/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/brand/icon.png -------------------------------------------------------------------------------- /brand/logo-outlined.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/brand/logo-outlined.png -------------------------------------------------------------------------------- /brand/logo-outlined@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/brand/logo-outlined@2x.png -------------------------------------------------------------------------------- /brand/logo-social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/brand/logo-social.png -------------------------------------------------------------------------------- /brand/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/brand/logo.png -------------------------------------------------------------------------------- /brand/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/brand/logo@2x.png -------------------------------------------------------------------------------- /codemods/README.md: -------------------------------------------------------------------------------- 1 | # React Async codemods 2 | 3 | These codemods enable you to automatically upgrade your codebase to handle breaking changes in 4 | React Async's API. 5 | 6 | ## Warning 7 | 8 | Be aware: **codemods transform your source code in place**. Make sure that your files are in 9 | version control before running a codemod. 10 | 11 | These codemods come without warranty. They will work fine most of the time, but you should always 12 | verify their output. Also, **do not run a codemod more than once.** 13 | 14 | ## Running a codemod 15 | 16 | These codemods are based on [jscodeshift](https://github.com/facebook/jscodeshift). Refer to their 17 | docs for specifics. 18 | 19 | ```bash 20 | npx jscodeshift -t 21 | ``` 22 | 23 | Where `` should be replaced with the path to your project's source directory and 24 | `` should be replaced by the URL of the codemod. 25 | 26 | For example: 27 | 28 | ```bash 29 | npx jscodeshift . -t https://raw.githubusercontent.com/async-library/react-async/master/codemods/v6.js 30 | ``` 31 | 32 | This will apply the codemod for [v6](https://github.com/async-library/react-async/blob/master/codemods/v6.js) 33 | to the current working directory (`.`). 34 | -------------------------------------------------------------------------------- /codemods/v6.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This renames: 3 | * - to 4 | * - to 5 | * - to 6 | * 7 | * This includes any custom instances created with createInstance(). 8 | */ 9 | 10 | module.exports = function transform({ path, source }, api) { 11 | if (path.includes("node_modules/")) return 12 | 13 | const j = api.jscodeshift 14 | const root = j(source) 15 | 16 | const renameJsxMembers = parentName => { 17 | root 18 | .find(j.JSXMemberExpression, { object: { name: parentName }, property: { name: "Pending" } }) 19 | .forEach(node => (node.value.property.name = "Initial")) 20 | root 21 | .find(j.JSXMemberExpression, { object: { name: parentName }, property: { name: "Loading" } }) 22 | .forEach(node => (node.value.property.name = "Pending")) 23 | root 24 | .find(j.JSXMemberExpression, { object: { name: parentName }, property: { name: "Resolved" } }) 25 | .forEach(node => (node.value.property.name = "Fulfilled")) 26 | } 27 | 28 | // Rename instances using default import 29 | root 30 | .find(j.ImportDeclaration, { source: { value: "react-async" } }) 31 | .find(j.ImportDefaultSpecifier) 32 | .forEach(node => renameJsxMembers(node.value.local.name)) 33 | 34 | // Rename instances using named `Async` import 35 | root 36 | .find(j.ImportDeclaration, { source: { value: "react-async" } }) 37 | .find(j.ImportSpecifier, { imported: { name: "Async" } }) 38 | .forEach(node => renameJsxMembers(node.value.local.name)) 39 | 40 | // Rename instances created with `createInstance` 41 | root 42 | .find(j.ImportDeclaration, { source: { value: "react-async" } }) 43 | .find(j.ImportSpecifier, { imported: { name: "createInstance" } }) 44 | .forEach(node => { 45 | const createInstance = node.value.local.name 46 | root 47 | .find(j.VariableDeclarator) 48 | .filter(node => node.value.init.type === "CallExpression") 49 | .filter(node => node.value.init.callee.name === createInstance) 50 | .forEach(node => renameJsxMembers(node.value.id.name)) 51 | }) 52 | 53 | return root.toSource() 54 | } 55 | -------------------------------------------------------------------------------- /codemods/v8.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This renames the standalone helper components: 3 | * - to 4 | * - to 5 | * - to 6 | * - to 7 | * - to 8 | */ 9 | 10 | const helperNames = ["Initial", "Pending", "Fulfilled", "Rejected", "Settled"] 11 | 12 | module.exports = function transform({ path, source }, api) { 13 | if (path.includes("node_modules/")) return 14 | 15 | const j = api.jscodeshift 16 | const root = j(source) 17 | 18 | // Rename imports 19 | root 20 | .find(j.ImportDeclaration, { source: { value: "react-async" } }) 21 | .find(j.ImportSpecifier) 22 | .filter(node => helperNames.includes(node.value.imported.name)) 23 | .forEach(node => (node.value.imported.name = `If${node.value.imported.name}`)) 24 | 25 | // Rename JSX elements 26 | root 27 | .find(j.JSXIdentifier) 28 | .filter(node => helperNames.includes(node.value.name)) 29 | .filter(node => node.parentPath.value.type !== "JSXMemberExpression") 30 | .forEach(node => (node.value.name = `If${node.value.name}`)) 31 | 32 | return root.toSource() 33 | } 34 | -------------------------------------------------------------------------------- /docs/_summary.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | - [Introduction](introduction.md) 4 | 5 | ## Getting started 6 | 7 | - [Installation](getting-started/installation.md) 8 | - [Upgrading](getting-started/upgrading.md) 9 | - [Usage](getting-started/usage.md) 10 | - [DevTools](getting-started/devtools.md) 11 | 12 | ## API 13 | 14 | - [Interfaces](api/interfaces.md) 15 | - [Configuration options](api/options.md) 16 | - [State properties](api/state.md) 17 | - [Helper components](api/helpers.md) 18 | 19 | ## Guide 20 | 21 | - [Async components](guide/async-components.md) 22 | - [Separating view and logic](guide/separating-view-logic.md) 23 | - [Async actions](guide/async-actions.md) 24 | - [Optimistic updates](guide/optimistic-updates.md) 25 | - [Server-side rendering](guide/server-side-rendering.md) 26 | 27 | ## Contributing 28 | 29 | - [Introduction](contributing/introduction.md) 30 | - [Setting up](contributing/setting-up.md) 31 | - [Development](contributing/development.md) 32 | - [Testing](contributing/testing.md) 33 | - [Releasing](contributing/releasing.md) 34 | -------------------------------------------------------------------------------- /docs/api/helpers.md: -------------------------------------------------------------------------------- 1 | # Helper components 2 | 3 | React Async provides several helper components that make your JSX more declarative and less cluttered. They don't have to be direct children of `` and you can use the same component several times. 4 | 5 | ## `` / `` 6 | 7 | Renders only while the deferred promise is still waiting to be run, or you have not provided any promise. 8 | 9 | ### Props 10 | 11 | - `children` `function(state: Object): Node | Node` Render function or React Node. 12 | - `state` `object` Async state object \(return value of `useAsync()`\). 13 | - `persist` `boolean` Show until we have data, even while loading or when an error occurred. By default it hides as soon 14 | 15 | as the promise starts loading. 16 | 17 | ### Examples 18 | 19 | ```jsx 20 | const state = useAsync(...) 21 | return ( 22 | 23 |

This text is only rendered while `run` has not yet been invoked on `deferFn`.

24 |
25 | ) 26 | ``` 27 | 28 | ```jsx 29 | 30 | 31 |

This text is only rendered while `run` has not yet been invoked on `deferFn`.

32 |
33 |
34 | ``` 35 | 36 | ```jsx 37 | 38 | {({ error, isPending, run }) => ( 39 |
40 |

This text is only rendered while the promise has not fulfilled yet.

41 | 44 | {error &&

{error.message}

} 45 |
46 | )} 47 |
48 | ``` 49 | 50 | ## `` / `` 51 | 52 | This component renders only while the promise is pending \(loading / unsettled\). 53 | 54 | Alias: `` 55 | 56 | ### Props 57 | 58 | - `children` `function(state: Object): Node | Node` Render function or React Node. 59 | - `state` `object` Async state object \(return value of `useAsync()`\). 60 | - `initial` `boolean` Show only on initial load \(when `data` is `undefined`\). 61 | 62 | ### Examples 63 | 64 | ```jsx 65 | const state = useAsync(...) 66 | return ( 67 | 68 |

This text is only rendered while performing the initial load.

69 |
70 | ) 71 | ``` 72 | 73 | ```jsx 74 | 75 |

This text is only rendered while performing the initial load.

76 |
77 | ``` 78 | 79 | ```jsx 80 | {({ startedAt }) => `Loading since ${startedAt.toISOString()}`} 81 | ``` 82 | 83 | ## `` / `` 84 | 85 | This component renders only when the promise is fulfilled \(resolved to a value, could be `undefined`\). 86 | 87 | Alias: `` 88 | 89 | ### Props 90 | 91 | - `children` `function(data: any, state: Object): Node | Node` Render function or React Node. 92 | - `state` `object` Async state object \(return value of `useAsync()`\). 93 | - `persist` `boolean` Show old data while loading new data. By default it hides as soon as a new promise starts. 94 | 95 | ### Examples 96 | 97 | ```jsx 98 | const state = useAsync(...) 99 | return ( 100 | 101 | {data =>
{JSON.stringify(data)}
} 102 |
103 | ) 104 | ``` 105 | 106 | ```jsx 107 | {data =>
{JSON.stringify(data)}
}
108 | ``` 109 | 110 | ```jsx 111 | 112 | {(data, { finishedAt }) => `Last updated ${finishedAt.toISOString()}`} 113 | 114 | ``` 115 | 116 | ## `` / `` 117 | 118 | This component renders only when the promise is rejected. 119 | 120 | ### Props 121 | 122 | - `children` `function(error: Error, state: Object): Node | Node` Render function or React Node. 123 | - `state` `object` Async state object \(return value of `useAsync()`\). 124 | - `persist` `boolean` Show old error while loading new data. By default it hides as soon as a new promise starts. 125 | -------------------------------------------------------------------------------- /docs/api/interfaces.md: -------------------------------------------------------------------------------- 1 | # Interfaces 2 | 3 | React Async provides several ways to use it. The classic interface is through the `` component, which is 4 | backwards compatible to React v16.3. More recent React applications will be using hooks, of which two are provided: 5 | `useAsync` and `useFetch`. Functionally, `` and `useAsync` are equivalent. `useFetch` is a special version of 6 | `useAsync` which is tied to the native `fetch` API. 7 | 8 | React Async accepts a wide range of [configuration options](options.md) and returns a set of [state props](state.md). 9 | The way you use these differs slightly between the `useAsync` and `useFetch` hooks, and the `` component. 10 | 11 | ## `Async` component 12 | 13 | ```jsx 14 | {state => ...} 15 | ``` 16 | 17 | - [`options`](options.md) Configuration options 18 | - [`state`](state.md) State object 19 | 20 | > We recommend that you pass the options individually, rather than using JSX [spread attributes]. React Async uses 21 | > [render props] to return its state back to you, so it can be used by other components further down the tree. 22 | 23 | [spread attributes]: https://reactjs.org/docs/jsx-in-depth.html#spread-attributes 24 | [render props]: https://reactjs.org/docs/render-props.html 25 | 26 | ## `useAsync` hook 27 | 28 | ```js 29 | const state = useAsync(options) 30 | ``` 31 | 32 | - [`state`](state.md) State object 33 | - [`options`](options.md) Configuration options 34 | 35 | > We recommend that you pass `options` as an inline object literal, and that you [destructure] the `state` object to 36 | > extract the properties you need, unless you have multiple instances in the same component. 37 | 38 | [destructure]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring 39 | 40 | ## `useFetch` hook 41 | 42 | ```js 43 | const state = useFetch(resource, init, options) 44 | ``` 45 | 46 | - [`state`](state.md) State object 47 | - [`resource`][fetch api] The resource you want to fetch 48 | - [`init`][fetch api] Custom request options 49 | - [`options`](options.md) Configuration options 50 | 51 | [fetch api]: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Syntax 52 | 53 | ## `createInstance` 54 | 55 | Besides using the `Async` component directly, you can also create your own instance of it. This allows you to preload it 56 | with options, e.g. to enable global error handling. 57 | 58 | ```js 59 | const CustomAsync = createInstance(defaultOptions, displayName) 60 | ``` 61 | 62 | - [`defaultOptions`](options.md) Default configuration options 63 | - `displayName` Name for this instance, used by React DevTools 64 | -------------------------------------------------------------------------------- /docs/api/options.md: -------------------------------------------------------------------------------- 1 | # Configuration options 2 | 3 | These can be passed in an object to `useAsync(options)`, or as props to `` and custom instances. 4 | 5 | - [`promise`](#promise) An already started Promise instance. 6 | - [`promiseFn`](#promisefn) Function that returns a Promise, automatically invoked. 7 | - [`deferFn`](#deferfn) Function that returns a Promise, manually invoked with `run`. 8 | - [`watch`](#watch) Watch a value and automatically reload when it changes. 9 | - [`watchFn`](#watchfn) Watch this function and automatically reload when it returns truthy. 10 | - [`initialValue`](#initialvalue) Provide initial data or error for server-side rendering. 11 | - [`onResolve`](#onresolve) Callback invoked when Promise resolves. 12 | - [`onReject`](#onreject) Callback invoked when Promise rejects. 13 | - [`onCancel`](#oncancel) Callback invoked when a Promise is cancelled. 14 | - [`reducer`](#reducer) State reducer to control internal state updates. 15 | - [`dispatcher`](#dispatcher) Action dispatcher to control internal action dispatching. 16 | - [`debugLabel`](#debuglabel) Unique label used in DevTools. 17 | - [`suspense`](#suspense) Enable **experimental** Suspense integration. 18 | 19 | `useFetch` additionally takes these options: 20 | 21 | - [`defer`](#defer) Force the use of `deferFn` or `promiseFn`. 22 | - [`json`](#json) Enable JSON parsing of the response. 23 | 24 | ## `promise` 25 | 26 | > `Promise` 27 | 28 | A Promise instance which has already started. It will simply add the necessary resolve/reject callbacks and set `startedAt` to the time `promise` was first provided. Changing the value of `promise` will cancel any pending promise and listen to the new one. If `promise` is initially undefined, the React Async state will be `pending`. 29 | 30 | > Note that `reload` will not do anything when using `promise`. Use `promiseFn` instead. 31 | 32 | ## `promiseFn` 33 | 34 | > `function(props: Object, controller: AbortController): Promise` 35 | 36 | A function that returns a promise. It is automatically invoked in `componentDidMount` and `componentDidUpdate`. The function receives all component props \(or options\) and an AbortController instance as arguments. 37 | 38 | > Be aware that updating `promiseFn` will trigger it to cancel any pending promise and load the new promise. Passing an inline (arrow) function will cause it to change and reload on every render of the parent component. You can avoid this by defining the `promiseFn` value **outside** of the render method. If you need to pass variables to the `promiseFn`, pass them as additional props to ``, as `promiseFn` will be invoked with these props. Alternatively you can use `useCallback` or [memoize-one](https://github.com/alexreardon/memoize-one) to avoid unnecessary updates. 39 | 40 | ## `deferFn` 41 | 42 | > `function(args: any[], props: Object, controller: AbortController): Promise` 43 | 44 | A function that returns a promise. This is invoked only by manually calling `run(...args)`. Any arguments to `run` are passed-through as an array via `args`, so you can pass data through either `args` or `props`, as needed. The `deferFn` is commonly used to send data to the server following a user action, such as submitting a form. You can use this in conjunction with `promiseFn` to fill the form with existing data, then updating it on submit with `deferFn`. 45 | 46 | > Be aware that when using both `promiseFn` and `deferFn`, the shape of their fulfilled value should match, because they both update the same `data`. 47 | 48 | ## `watch` 49 | 50 | > `any` 51 | 52 | Watches this property through `componentDidUpdate` and re-runs the `promiseFn` when the value changes, using a simple reference check \(`oldValue !== newValue`\). If you need a more complex update check, use `watchFn` instead. 53 | 54 | ## `watchFn` 55 | 56 | > `function(props: Object, prevProps: Object): boolean | any` 57 | 58 | Re-runs the `promiseFn` when this callback returns truthy \(called on every update\). Any default props specified by `createInstance` are available too. 59 | 60 | ## `initialValue` 61 | 62 | > `any | Error` 63 | 64 | Initial state for `data` or `error` \(if instance of Error\); useful for server-side rendering. When an `initialValue` is provided, the `promiseFn` will not be invoked on first render. Instead, `status` will be immediately set to `fulfilled` or `rejected` and your components will render accordingly. If you want to trigger the `promiseFn` regardless, you can call `reload()` or use the `watch` or `watchFn` option. 65 | 66 | > Note that `onResolve` or `onReject` is not invoked in this case and no `promise` prop will be created. 67 | 68 | ## `onResolve` 69 | 70 | > `function(data: any): void` 71 | 72 | Callback function invoked when a promise resolves, receives data as argument. 73 | 74 | ## `onReject` 75 | 76 | > `function(reason: Error): void` 77 | 78 | Callback function invoked when a promise rejects, receives rejection reason \(error\) as argument. 79 | 80 | ## `onCancel` 81 | 82 | > `function(): void` 83 | 84 | Callback function invoked when a promise is cancelled, either manually using `cancel()` or automatically due to props changes or unmounting. 85 | 86 | ## `reducer` 87 | 88 | > `function(state: any, action: Object, internalReducer: function(state: any, action: Object))` 89 | 90 | State reducer to take full control over state updates by wrapping the [internal reducer](https://github.com/async-library/react-async/blob/master/packages/react-async/src/reducer.ts). It receives the current state, the dispatched action and the internal reducer. You probably want to invoke the internal reducer at some point. 91 | 92 | > This is a power feature which loosely follows the [state reducer pattern](https://kentcdodds.com/blog/the-state-reducer-pattern). It allows you to control state changes by intercepting actions before they are handled, or by overriding or enhancing the reducer itself. 93 | 94 | ## `dispatcher` 95 | 96 | > `function(action: Object, internalDispatch: function(action: Object), props: Object)` 97 | 98 | Action dispatcher to take full control over action dispatching by wrapping the internal dispatcher. It receives the original action, the internal dispatcher and all component props \(or options\). You probably want to invoke the internal dispatcher at some point. 99 | 100 | > This is a power feature similar to the [state reducer pattern](https://kentcdodds.com/blog/the-state-reducer-pattern). It allows you to control state changes by intercepting actions before they are dispatched, to dispatch additional actions, possibly later in time. 101 | 102 | ## `debugLabel` 103 | 104 | > `string` 105 | 106 | A unique label to describe this React Async instance, used in React DevTools \(through `useDebugValue`\) and React Async DevTools. 107 | 108 | ## `suspense` 109 | 110 | > `boolean` 111 | 112 | Enables **experimental** Suspense integration. This will make React Async throw a promise while loading, so you can use Suspense to render a fallback UI, instead of using ``. Suspense differs in 2 main ways: 113 | 114 | - `` should be an ancestor of your Async component, instead of a descendant. It can be anywhere up in the 115 | 116 | component hierarchy. 117 | 118 | - You can have a single `` wrap multiple Async components, in which case it will render the fallback UI until 119 | 120 | all promises are settled. 121 | 122 | > Note that the way Suspense is integrated right now may change. Until Suspense for data fetching is officially released, we may make breaking changes to its integration in React Async in a minor or patch release. Among other things, we'll probably add a cache of sorts. 123 | 124 | ## `defer` 125 | 126 | > `boolean` 127 | 128 | Enables the use of `deferFn` if `true`, or enables the use of `promiseFn` if `false`. By default this is automatically chosen based on the request method \(`deferFn` for POST / PUT / PATCH / DELETE, `promiseFn` otherwise\). 129 | 130 | ## `json` 131 | 132 | > `boolean` 133 | 134 | Enables or disables JSON parsing of the response body. By default this is automatically enabled if the `Accept` header is set to `"application/json"`. 135 | -------------------------------------------------------------------------------- /docs/api/state.md: -------------------------------------------------------------------------------- 1 | # State properties 2 | 3 | These are returned in an object by `useAsync()` or provided by `` as render props to the `children` function: 4 | 5 | - [`data`](#data) Last resolved promise value, maintained when new error arrives. 6 | - [`error`](#error) Rejected promise reason, cleared when new data arrives. 7 | - [`value`](#value) The value of `data` or `error`, whichever was last updated. 8 | - [`initialValue`](#initialvalue) The data or error that was provided through the `initialValue` prop. 9 | - [`startedAt`](#startedat) When the current/last promise was started. 10 | - [`finishedAt`](#finishedat) When the last promise was fulfilled or rejected. 11 | - [`status`](#status) One of: `initial`, `pending`, `fulfilled`, `rejected`. 12 | - [`isInitial`](#isinitial) true when no promise has ever started, or one started but was cancelled. 13 | - [`isPending`](#ispending) true when a promise is currently awaiting settlement. Alias: `isLoading` 14 | - [`isFulfilled`](#isfulfilled) true when the last promise was fulfilled. Alias: `isResolved` 15 | - [`isRejected`](#isrejected) true when the last promise was rejected. 16 | - [`isSettled`](#issettled) true when the last promise was fulfilled or rejected \(not initial or pending\). 17 | - [`counter`](#counter) The number of times a promise was started. 18 | - [`promise`](#promise) A reference to the internal wrapper promise, which can be chained on. 19 | - [`run`](#run) Invokes the `deferFn`. 20 | - [`reload`](#reload) Re-runs the promise when invoked, using any previous arguments. 21 | - [`cancel`](#cancel) Cancel any pending promise. 22 | - [`setData`](#setdata) Sets `data` to the passed value, unsets `error` and cancels any pending promise. 23 | - [`setError`](#seterror) Sets `error` to the passed value and cancels any pending promise. 24 | 25 | ## `data` 26 | 27 | > `any` 28 | 29 | Last resolved promise value, maintained when new error arrives. 30 | 31 | ## `error` 32 | 33 | > `Error` 34 | 35 | Rejected promise reason, cleared when new data arrives. 36 | 37 | ## `value` 38 | 39 | > `any | Error` 40 | 41 | The data or error that was last provided \(either through `initialValue` or by settling a promise\). 42 | 43 | ## `initialValue` 44 | 45 | > `any | Error` 46 | 47 | The data or error that was originally provided through the `initialValue` prop. 48 | 49 | ## `startedAt` 50 | 51 | > `Date` 52 | 53 | Tracks when the current/last promise was started. 54 | 55 | ## `finishedAt` 56 | 57 | > `Date` 58 | 59 | Tracks when the last promise was resolved or rejected. 60 | 61 | ## `status` 62 | 63 | > `string` 64 | 65 | One of: `initial`, `pending`, `fulfilled`, `rejected`. These are available for import as `statusTypes`. 66 | 67 | ## `isInitial` 68 | 69 | > `boolean` 70 | 71 | `true` while no promise has started yet, or one was started but cancelled. 72 | 73 | ## `isPending` 74 | 75 | > `boolean` 76 | 77 | `true` while a promise is pending \(loading\), `false` otherwise. 78 | 79 | Alias: `isLoading` 80 | 81 | ## `isFulfilled` 82 | 83 | > `boolean` 84 | 85 | `true` when the last promise was fulfilled \(resolved to a value\). 86 | 87 | Alias: `isResolved` 88 | 89 | ## `isRejected` 90 | 91 | > `boolean` 92 | 93 | `true` when the last promise was rejected. 94 | 95 | ## `isSettled` 96 | 97 | > `boolean` 98 | 99 | `true` when the last promise was either fulfilled or rejected \(i.e. not initial or pending\) 100 | 101 | ## `counter` 102 | 103 | > `number` 104 | 105 | The number of times a promise was started. 106 | 107 | ## `promise` 108 | 109 | > `Promise` 110 | 111 | A reference to the internal wrapper promise created when starting a new promise \(either automatically or by invoking 112 | `run` / `reload`\). It fulfills or rejects along with the provided `promise` / `promiseFn` / `deferFn`. Useful as a 113 | chainable alternative to the `onResolve` / `onReject` callbacks. 114 | 115 | Warning! If you chain on `promise`, you MUST provide a rejection handler \(e.g. `.catch(...)`\). Otherwise React will 116 | throw an exception and crash if the promise rejects. 117 | 118 | ## `run` 119 | 120 | > `function(...args: any[]): void` 121 | 122 | Runs the `deferFn`, passing any arguments provided as an array. 123 | 124 | When used with `useFetch`, `run` has several overloaded signatures: 125 | 126 | > `function(override: OverrideParams | (params: OverrideParams) => OverrideParams): void` 127 | > 128 | > `function(event: SyntheticEvent | Event): void` 129 | > 130 | > `function(): void` 131 | 132 | Where `type OverrideParams = { resource?: RequestInfo } & Partial`. 133 | 134 | This way you can run the `fetch` request with custom `resource` and `init`. If `override` is an object it will be spread 135 | over the default `resource` and `init` for `fetch`. If it's a function it will be invoked with the params defined with 136 | `useFetch`, and should return an `override` object. This way you can either extend or override the value of `resource` 137 | and `init`, for example to change the URL or set custom request headers. 138 | 139 | ## `reload` 140 | 141 | > `function(): void` 142 | 143 | Re-runs the promise when invoked, using the previous arguments. 144 | 145 | ## `cancel` 146 | 147 | > `function(): void` 148 | 149 | Cancels the currently pending promise by ignoring its result and calls `abort()` on the AbortController. 150 | 151 | ## `setData` 152 | 153 | > `function(data: any, callback?: () => void): any` 154 | 155 | Function that sets `data` to the passed value, unsets `error` and cancels any pending promise. Takes an optional callback which is invoked after the state update is completed. Returns the data to enable chaining. 156 | 157 | ## `setError` 158 | 159 | > `function(error: Error, callback?: () => void): Error` 160 | 161 | Function that sets `error` to the passed value and cancels any pending promise. Takes an optional callback which is invoked after the state update is completed. Returns the error to enable chaining. 162 | -------------------------------------------------------------------------------- /docs/contributing/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | React Async is a library without visual parts. Only the DevTools have a user interface you can spin up in a browser. 4 | Therefore the development workflow for the core library might be different from what you're used to. Generally, we use a 5 | TDD approach: 6 | 7 | - Write a unit test for the new feature or bug you want to fix. Sometimes you can just extend an existing test. 8 | - Fix the test by implementing the feature or bugfix. Now all tests should pass. 9 | - Optionally refactor the code for performance, readability and style. Probably this will come up during PR review. 10 | 11 | We use the GitHub pull request workflow. In practice this means your workflow looks like this: 12 | 13 | - Fork the repo (or pull the latest upstream) under your own account. 14 | - Make your changes, commit and push them. We don't enforce any commit message format. 15 | - Open a pull request on the main repository against the `next` branch. Make sure to follow the template. 16 | - We'll review your PR and will probably ask for some changes. 17 | - Once ready, we'll merge your PR. 18 | - Your changes will be in the next release. 19 | 20 | ## Working with Storybook 21 | 22 | We use Storybook as a development environment for the DevTools. Spin it up using: 23 | 24 | ```sh 25 | yarn start:storybook 26 | ``` 27 | 28 | This should open up Storybook in a browser at http://localhost:6006/ 29 | Run it side-by-side with `yarn test --watch` during development. See [Testing](#testing). 30 | 31 | ## Working with the examples 32 | 33 | In the `examples` folder, you will find sample React applications that use React Async in various ways with various other libraries. Please add a new example when introducing a major new feature. Make sure to add it to `now.json` so it is automatically deployed when merged to `master`. 34 | 35 | To run sample examples on your local environments 36 | 37 | ```sh 38 | yarn build:examples 39 | yarn test:examples 40 | yarn start:examples 41 | ``` 42 | 43 | ## Resolving issues 44 | 45 | Sometimes your dependencies might end up in a weird state, causing random issues, especially when working with the examples. In this case it often helps to run `yarn clean -y && yarn bootstrap`. This will delete `node_modules` from all packages/examples and do a clean install. 46 | -------------------------------------------------------------------------------- /docs/contributing/introduction.md: -------------------------------------------------------------------------------- 1 | # Contributing to React Async 2 | 3 | Thanks for your interest in improving React Async! Contributions of any kind are welcome. Please refer to this guide before opening an issue or pull request. 4 | 5 | This repo relies on Yarn workspaces, so you should [install](https://yarnpkg.com/en/docs/install) and use `yarn@1.3.2` or higher as the package manager for this project. 6 | 7 | ## Development guide 8 | 9 | Please have the **_latest_** stable versions of the following on your machine 10 | 11 | - node 12 | - yarn 13 | 14 | ### Initial setup 15 | 16 | To start working on React Async, clone the repo and bootstrap the project: 17 | 18 | ```sh 19 | git clone https://github.com/async-library/react-async.git 20 | cd react-async 21 | yarn && yarn bootstrap && yarn test 22 | ``` 23 | 24 | Note that all work is done against the `next` branch, we only merge to `master` when doing a release. 25 | 26 | ### Working with Storybook 27 | 28 | We use Storybook as a development environment, particularly for the DevTools. Spin it up using: 29 | 30 | ```sh 31 | yarn start:storybook 32 | ``` 33 | 34 | This should open up Storybook in a browser at http://localhost:6006/ 35 | Run it side-by-side with `yarn test --watch` during development. See [Testing](#testing). 36 | 37 | ### Linting 38 | 39 | Use `yarn lint` to verify your code style before committing. It's highly recommended to install the Prettier and ESLint plugins for your IDE. Travis CI will fail your build on lint errors. Configure VS Code with the following settings: 40 | 41 | ```plaintext 42 | "eslint.autoFixOnSave": true, 43 | "eslint.packageManager": "yarn", 44 | "eslint.options": { 45 | "cache": true, 46 | "cacheLocation": ".cache/eslint", 47 | "extensions": [".js", ".jsx", ".mjs", ".json", ".ts", ".tsx"] 48 | }, 49 | "eslint.validate": [ 50 | "javascript", 51 | "javascriptreact", 52 | {"language": "typescript", "autoFix": true }, 53 | {"language": "typescriptreact", "autoFix": true } 54 | ], 55 | "eslint.alwaysShowStatus": true 56 | ``` 57 | 58 | This should enable auto-fix for all source files, and give linting warnings and errors within your editor. 59 | 60 | ### Testing 61 | 62 | Use the following command to test all packages in watch mode. Refer to the [Jest CLI options](https://jestjs.io/docs/en/cli#options) for details. 63 | 64 | ```sh 65 | yarn test:watch 66 | ``` 67 | 68 | In general, this is sufficient during development. Travis CI will apply a more rigorous set of tests. 69 | 70 | #### Testing for compatibility 71 | 72 | ```sh 73 | yarn test:compat 74 | ``` 75 | 76 | This runs all tests using various versions of `react` and `react-dom`, to check for compatibility with older/newer versions of React. This is what CircleCI and Travis run. 77 | 78 | ### Working with the examples 79 | 80 | In the `examples` folder, you will find sample React applications that use React Async in various ways with various other libraries. Please add a new example when introducing a major new feature. Make sure to add it to `now.json` so it is automatically deployed when merged to `master`. 81 | 82 | To run sample examples on your local environments 83 | 84 | ```sh 85 | yarn build:examples 86 | yarn test:examples 87 | yarn start:examples 88 | ``` 89 | 90 | ### Resolving issues 91 | 92 | Sometimes your dependencies might end up in a weird state, causing random issues, especially when working with the examples. In this case it often helps to run `yarn clean -y && yarn bootstrap`. This will delete `node_modules` from all packages/examples and do a clean install. 93 | -------------------------------------------------------------------------------- /docs/contributing/releasing.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | All ongoing development is done on the `next` branch. When preparing for a release, we'll create a `release` branch 4 | which will eventually be merged into `master`. This way, what's on `master` is always what's published on `npm`. 5 | 6 | Release management is currently a manual process, to be performed by core team members only. Here's the process: 7 | 8 | 1. Create a `release` branch, usually based on `next`. 9 | 2. Open a pull request for `release` -> `master` 10 | 3. Write the release notes in the PR description. 11 | 4. Decide on the version number, taking care to follow semver. Do a pre-release before doing the actual release. 12 | 5. Run `yarn bump` to increment the version number in all `package.json` files as well as `lerna.json`. 13 | 6. Commit the version change as "Release vX.X.X" (using the correct version number). 14 | 7. Tag the release commit with `git tag vX.X.X` (using the correct version number). 15 | 8. Push the release commit AND tag: `git push --follow-tags` 16 | 9. Publish each package (in `./packages`) to npm using the script below. 17 | 10. Create a new release on GitHub and copy the release notes there. 18 | 19 | ``` 20 | yarn build:packages 21 | cd packages/react-async 22 | npm publish pkg 23 | cd ../react-async-devtools 24 | npm publish pkg 25 | ``` 26 | 27 | Take care to publish the `pkg` directory! 28 | -------------------------------------------------------------------------------- /docs/contributing/setting-up.md: -------------------------------------------------------------------------------- 1 | # Setting up your development environment 2 | 3 | ## Prerequisites 4 | 5 | In order to develop React Async on your local machine, you'll need `git`, `node` and `yarn`. 6 | 7 | ### Git 8 | 9 | To clone the repository, commit your changes and push them upstream, you'll need to have `git` [installed][install git]. 10 | 11 | [install git]: https://www.atlassian.com/git/tutorials/install-git 12 | 13 | ### Node.js 14 | 15 | As a JavaScript project, we rely heavily on Node.js. It's recommended to use a version manager such as [fnm] for Mac / 16 | Linux or [nvm-windows] for Windows to install the latest Node.js with. 17 | 18 | [fnm]: https://github.com/Schniz/fnm 19 | [nvm-windows]: https://github.com/coreybutler/nvm-windows 20 | 21 | ### Yarn 22 | 23 | This repo relies on Yarn workspaces, so you should [install][install yarn] and use `yarn@1.3.2` or higher as the package 24 | manager for this project. 25 | 26 | [install yarn]: https://yarnpkg.com/en/docs/install 27 | 28 | ## Project setup 29 | 30 | To start working on React Async, clone the repository and bootstrap the project by running the following commands 31 | one-by-one: 32 | 33 | ```sh 34 | git clone https://github.com/async-library/react-async.git 35 | cd react-async 36 | yarn install 37 | yarn bootstrap 38 | yarn test 39 | ``` 40 | 41 | This should install all dependencies, build and link the react-async and react-async-devtools packages to the examples, 42 | and finally run the unit tests. In the end it should succeed with a message (numbers may change): 43 | 44 | ``` 45 | Test Suites: 6 passed, 6 total 46 | Tests: 136 passed, 136 total 47 | ``` 48 | 49 | > Note that all work is done against the `next` branch, we only merge to `master` when doing a release. 50 | 51 | ## Editor setup 52 | 53 | We recommend using [Visual Studio Code](https://code.visualstudio.com/) with the following extensions: 54 | 55 | - [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 56 | - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 57 | - [DeepScan](https://marketplace.visualstudio.com/items?itemName=DeepScan.vscode-deepscan) 58 | - [Oceanic Plus](https://marketplace.visualstudio.com/items?itemName=marcoms.oceanic-plus) 59 | 60 | Make sure to enable `editor.formatOnSave`, so Prettier will automatically apply the right code style. For the full 61 | immersive experience you can also install and use the [Overpass Mono](https://overpassfont.org/) font. 62 | -------------------------------------------------------------------------------- /docs/contributing/testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | Use the following command to test all packages in watch mode. Refer to the [Jest CLI options][jest options] for details. 4 | 5 | [jest options]: https://jestjs.io/docs/en/cli#options 6 | 7 | ```sh 8 | yarn test:watch 9 | ``` 10 | 11 | In general, this is sufficient during development. CircleCI and Travis will eventually apply a more rigorous set of 12 | tests against your pull request, including the ones below. 13 | 14 | ## Testing the examples 15 | 16 | Because React Async is only a piece in a bigger puzzle, testing for integration with other libraries is very important. 17 | You can run the tests for all examples against your local changes with the following command: 18 | 19 | ```sh 20 | yarn test:examples 21 | ``` 22 | 23 | If you want to add integration tests for compatibility with another library, please add an example for it. 24 | 25 | ## Testing for compatibility 26 | 27 | ```sh 28 | yarn test:compat 29 | ``` 30 | 31 | This runs all tests using various versions of `react` and `react-dom`, to check for compatibility with older/newer 32 | versions of React. This is what CircleCI and Travis run. 33 | 34 | ## Linting 35 | 36 | Use `yarn lint` to verify your code style before committing. It's highly recommended to install the Prettier and ESLint plugins for your IDE. CircleCI and Travis will fail your build on lint errors. 37 | -------------------------------------------------------------------------------- /docs/getting-started/devtools.md: -------------------------------------------------------------------------------- 1 | # DevTools 2 | 3 | React Async comes with a separate DevTools package which helps you Debug and develop your asynchronous application 4 | states. You can install it from npm: 5 | 6 | ```text 7 | npm install --save react-async-devtools 8 | ``` 9 | 10 | Or if you're using Yarn: 11 | 12 | ```text 13 | yarn add react-async-devtools 14 | ``` 15 | 16 | Then simply import it and render the`` component at the root of your app: 17 | 18 | ```jsx 19 | import DevTools from "react-async-devtools" 20 | 21 | export const Root = () => ( 22 | <> 23 | 24 | 25 | 26 | ) 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | You can install `react-async` from npm: 4 | 5 | ```text 6 | npm install --save react-async 7 | ``` 8 | 9 | Or if you're using Yarn: 10 | 11 | ```text 12 | yarn add react-async 13 | ``` 14 | 15 | > This package requires `react` as a peer dependency. Please make sure to install that as well. If you want to use the 16 | > `useAsync` hook, you'll need `react@16.8.0` or later. 17 | 18 | ## Transpiling for legacy browsers 19 | 20 | This project targets the latest ECMAScript version. Our packages on npm do not contain ES5 code for legacy browsers. If you need to target a browser which does not support the latest version of ECMAScript, you'll have to handle transpilation yourself. Usually this will automatically be handled by the framework you use (CRA, Next.js, Gatsby), but sometimes you may need to tweak your Webpack settings to transpile `react-async` with Babel. 21 | 22 | To transpile `node_modules` with Babel you need to use a `babel.config.js`, for more information see [Babel's documentation](https://babeljs.io/docs/en/configuration#whats-your-use-case). 23 | 24 | In your `webpack.config.js` make sure that the rule for `babel-loader`: 25 | 26 | - doesn't exclude `node_modules` from matching via the `exclude` pattern; 27 | - excludes `core-js` as it shouldn't be transpiled; 28 | - is passed the `configFile` option pointing to the `babel.config.js` file. 29 | 30 | ``` 31 | { 32 | test: /\.(js|jsx)$/, 33 | exclude: /\/node_modules\/core-js\//, 34 | use: [{ 35 | loader: 'babel-loader', 36 | options: { 37 | configFile: './babel.config.js', 38 | // Caching is recommended when transpiling node_modules to speed up consecutive builds 39 | cacheDirectory: true, 40 | } 41 | }] 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/getting-started/upgrading.md: -------------------------------------------------------------------------------- 1 | # Upgrading 2 | 3 | ## Upgrade to v9 4 | 5 | The rejection value for failed requests with `useFetch` was changed. Previously it was the Response object. Now it's an 6 | Error object with `response` property. If you are using `useFetch` and are using the `error` value, expecting it to be 7 | of type Response, you must now use `error.response` instead. 8 | 9 | ## Upgrade to v8 10 | 11 | All standalone helper components were renamed to avoid import naming collision. 12 | 13 | - `` was renamed to ``. 14 | - `` was renamed to ``. 15 | - `` was renamed to ``. 16 | - `` was renamed to `` was renamed to ``. 18 | 19 | > A [codemod](https://github.com/async-library/react-async/tree/master/codemods) is available to automate the upgrade. 20 | 21 | The return type for `run` was changed from `Promise` to `undefined`. You should now use the `promise` prop instead. This 22 | is a manual upgrade. See [`promise`](state.md#promise) for details. 23 | 24 | ## Upgrade to v6 25 | 26 | - `` was renamed to ``. 27 | - Some of the other helpers were also renamed, but the old ones remain as alias. 28 | - Don't forget to deal with any custom instances of `` when upgrading. 29 | 30 | > A [codemod](https://github.com/async-library/react-async/tree/master/codemods) is available to automate the upgrade. 31 | 32 | ## Upgrade to v4 33 | 34 | - `deferFn` now receives an `args` array as the first argument, instead of arguments to `run` being spread at the front 35 | of the arguments list. This enables better interop with TypeScript. You can use destructuring to keep using your 36 | existing variables. 37 | 38 | - The shorthand version of `useAsync` now takes the `options` object as optional second argument. This used to be 39 | `initialValue`, but was undocumented and inflexible. 40 | -------------------------------------------------------------------------------- /docs/getting-started/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | React Async offers three primary APIs: the `useAsync` hook, the `` component and the `createInstance` factory function. Each has its unique benefits and downsides. 4 | 5 | ## As a hook 6 | 7 | The `useAsync` hook \(available [from React v16.8.0](https://reactjs.org/hooks)\) offers direct access to React Async's core functionality from within your own function components: 8 | 9 | ```jsx 10 | import { useAsync } from "react-async" 11 | 12 | // You can use async/await or any function that returns a Promise 13 | const loadPlayer = async ({ playerId }, { signal }) => { 14 | const res = await fetch(`/api/players/${playerId}`, { signal }) 15 | if (!res.ok) throw new Error(res.statusText) 16 | return res.json() 17 | } 18 | 19 | const MyComponent = () => { 20 | const { data, error, isPending } = useAsync({ promiseFn: loadPlayer, playerId: 1 }) 21 | if (isPending) return "Loading..." 22 | if (error) return `Something went wrong: ${error.message}` 23 | if (data) 24 | return ( 25 |
26 | Player data: 27 |
{JSON.stringify(data, null, 2)}
28 |
29 | ) 30 | return null 31 | } 32 | ``` 33 | 34 | > Using [helper components](usage.md#with-helper-components) can greatly improve readability of your render functions by not having to write all those conditional returns. 35 | 36 | Or using the shorthand version: 37 | 38 | ```jsx 39 | const MyComponent = () => { 40 | const { data, error, isPending } = useAsync(loadPlayer, options) 41 | // ... 42 | } 43 | ``` 44 | 45 | ### With `useFetch` 46 | 47 | Because fetch is so commonly used with `useAsync`, there's a dedicated `useFetch` hook for it: 48 | 49 | ```jsx 50 | import { useFetch } from "react-async" 51 | 52 | const MyComponent = () => { 53 | const headers = { Accept: "application/json" } 54 | const { data, error, isPending, run } = useFetch("/api/example", { headers }, options) 55 | // This will setup a promiseFn with a fetch request and JSON deserialization. 56 | 57 | // you can later call `run` with an optional callback argument to 58 | // last-minute modify the `init` parameter that is passed to `fetch` 59 | function clickHandler() { 60 | run(init => ({ 61 | ...init, 62 | headers: { 63 | ...init.headers, 64 | authentication: "...", 65 | }, 66 | })) 67 | } 68 | 69 | // alternatively, you can also just use an object that will be spread over `init`. 70 | // please note that this is not deep-merged, so you might override properties present in the 71 | // original `init` parameter 72 | function clickHandler2() { 73 | run({ body: JSON.stringify(formValues) }) 74 | } 75 | } 76 | ``` 77 | 78 | `useFetch` takes the same arguments as [fetch](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) itself, as well as `options` to the underlying `useAsync` hook. The `options` object takes two special boolean properties: `defer` and `json`. These can be used to switch between `deferFn` and `promiseFn`, and enable JSON parsing. By default `useFetch` automatically uses `promiseFn` or `deferFn` based on the request method \(`deferFn` for POST / PUT / PATCH / DELETE\) and handles JSON parsing if the `Accept` header is set to `"application/json"`. 79 | 80 | ## As a component 81 | 82 | The classic interface to React Async. Simply use `` directly in your JSX component tree, leveraging the render props pattern: 83 | 84 | ```jsx 85 | import Async from "react-async" 86 | 87 | // Your promiseFn receives all props from Async and an AbortController instance 88 | const loadPlayer = async ({ playerId }, { signal }) => { 89 | const res = await fetch(`/api/players/${playerId}`, { signal }) 90 | if (!res.ok) throw new Error(res.statusText) 91 | return res.json() 92 | } 93 | 94 | const MyComponent = () => ( 95 | 96 | {({ data, error, isPending }) => { 97 | if (isPending) return "Loading..." 98 | if (error) return `Something went wrong: ${error.message}` 99 | if (data) 100 | return ( 101 |
102 | Player data: 103 |
{JSON.stringify(data, null, 2)}
104 |
105 | ) 106 | return null 107 | }} 108 |
109 | ) 110 | ``` 111 | 112 | > Using [helper components](usage.md#with-helper-components) can greatly improve readability of your render functions by not having to write all those conditional returns. 113 | 114 | ## As a factory 115 | 116 | You can also create your own component instances, allowing you to preconfigure them with options such as default `onResolve` and `onReject` callbacks. 117 | 118 | ```jsx 119 | import { createInstance } from "react-async" 120 | 121 | const loadPlayer = async ({ playerId }, { signal }) => { 122 | const res = await fetch(`/api/players/${playerId}`, { signal }) 123 | if (!res.ok) throw new Error(res.statusText) 124 | return res.json() 125 | } 126 | 127 | // createInstance takes a defaultOptions object and a displayName (both optional) 128 | const AsyncPlayer = createInstance({ promiseFn: loadPlayer }, "AsyncPlayer") 129 | 130 | const MyComponent = () => ( 131 | 132 | {player => `Hello ${player.name}`} 133 | 134 | ) 135 | ``` 136 | 137 | ## With helper components 138 | 139 | Several [helper components](usage.md#helper-components) are available to improve legibility. They can be used with `useAsync` by passing in the state, or with `` by using Context. Each of these components simply enables or disables rendering of its children based on the current state. 140 | 141 | ```jsx 142 | import { useAsync, IfPending, IfFulfilled, IfRejected } from "react-async" 143 | 144 | const loadPlayer = async ({ playerId }, { signal }) => { 145 | // ... 146 | } 147 | 148 | const MyComponent = () => { 149 | const state = useAsync({ promiseFn: loadPlayer, playerId: 1 }) 150 | return ( 151 | <> 152 | Loading... 153 | {error => `Something went wrong: ${error.message}`} 154 | 155 | {data => ( 156 |
157 | Player data: 158 |
{JSON.stringify(data, null, 2)}
159 |
160 | )} 161 |
162 | 163 | ) 164 | } 165 | ``` 166 | 167 | ### As compounds to `` 168 | 169 | Each of the helper components are also available as static properties of ``. In this case you won't have to pass the state object, instead it will be automatically provided through Context. 170 | 171 | ```jsx 172 | import Async from "react-async" 173 | 174 | const loadPlayer = async ({ playerId }, { signal }) => { 175 | const res = await fetch(`/api/players/${playerId}`, { signal }) 176 | if (!res.ok) throw new Error(res.statusText) 177 | return res.json() 178 | } 179 | 180 | const MyComponent = () => ( 181 | 182 | Loading... 183 | 184 | {data => ( 185 |
186 | Player data: 187 |
{JSON.stringify(data, null, 2)}
188 |
189 | )} 190 |
191 | {error => `Something went wrong: ${error.message}`} 192 |
193 | ) 194 | ``` 195 | -------------------------------------------------------------------------------- /docs/guide/async-actions.md: -------------------------------------------------------------------------------- 1 | # Async actions 2 | 3 | Fetching data for display alone isn't sufficient for most applications. You'll often also want to submit data back to 4 | the server, or handle other types of asynchronous actions. To enable this, React Async has the concept of a 5 | [`deferFn`](../api/options.md#deferfn). 6 | 7 | Like `promiseFn`, a `deferFn` is a function that returns a Promise. The difference is that `deferFn` will not be 8 | automatically invoked by React Async when rendering the component. Instead it will have to be triggered by calling the 9 | [`run`](../api/state.md#run) function provided by React Async. 10 | 11 | ```jsx 12 | import React, { useState } from "react" 13 | import { useAsync } from "react-async" 14 | 15 | const subscribe = ([email], props, { signal }) => 16 | fetch("/newsletter", { method: "POST", body: JSON.stringify({ email }), signal }) 17 | 18 | const NewsletterForm = () => { 19 | const { isPending, error, run } = useAsync({ deferFn: subscribe }) 20 | const [email, setEmail] = useState("") 21 | 22 | const handleSubmit = event => { 23 | event.preventDefault() 24 | run(email) 25 | } 26 | 27 | return ( 28 |
29 | setEmail(event.target.value)} /> 30 | 33 | {error &&

{error.message}

} 34 |
35 | ) 36 | } 37 | ``` 38 | 39 | As you can see, the `deferFn` is invoked with 3 arguments: `args`, `props` and the AbortController. `args` is an array 40 | representing the arguments that were passed to `run`. In this case we passed the `email`, so we can extract that from 41 | the `args` array at the first index using [array destructuring] and pass it along to our `fetch` request. 42 | 43 | [array destructuring]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Array_destructuring 44 | 45 | ## Sending data with `useFetch` 46 | 47 | The above example can be simplified when we rely on [`useFetch`](../api/interfaces.md#usefetch-hook) instead of 48 | constructing the request manually. 49 | 50 | ```jsx 51 | import React, { useState } from "react" 52 | import { useFetch } from "react-async" 53 | 54 | const NewsletterForm = () => { 55 | const { isPending, error, run } = useFetch("/newsletter", { method: "POST" }) 56 | const [email, setEmail] = useState("") 57 | 58 | const handleSubmit = event => { 59 | event.preventDefault() 60 | run({ body: JSON.stringify({ email }) }) 61 | } 62 | 63 | return ( 64 |
65 | setEmail(event.target.value)} /> 66 | 69 | {error &&

{error.message}

} 70 |
71 | ) 72 | } 73 | ``` 74 | 75 | The [`run`](../api/state.md#run) function for `useFetch` is a little special because it allows you to override the 76 | request's resource and other params. This way you can pass in the body, add dynamic headers or override the URL. 77 | -------------------------------------------------------------------------------- /docs/guide/async-components.md: -------------------------------------------------------------------------------- 1 | # Async components 2 | 3 | The most common use case for React Async is data fetching. In single-page applications it's very common to dynamically 4 | load some data from a backend. React Async makes it incredibly easy to set this up, without having to worry about the 5 | details. 6 | 7 | The mental model of React Async is component-first. Rather than loading data high up in your application and passing it 8 | down to a component for display, you perform the data loading at the component level. Such a component is called an 9 | async component. An async component can render its state in a meaningful way like any other component, or be logic-only. 10 | In that case it doesn't render any UI but instead passes its state down to its children. Such separation of concerns is 11 | good practice. 12 | 13 | ## Creating an async component with `useFetch` 14 | 15 | The easiest way to create an async component for data fetching is through the 16 | [`useFetch` hook](../api/interfaces.md#usefetch-hook): 17 | 18 | ```jsx 19 | import React from "react" 20 | import { useFetch } from "react-async" 21 | 22 | const Person = ({ id }) => { 23 | const { data, error } = useFetch(`https://swapi.co/api/people/${id}/`, { 24 | headers: { accept: "application/json" }, 25 | }) 26 | if (error) return error.message 27 | if (data) return `Hi, my name is ${data.name}!` 28 | return null 29 | } 30 | 31 | const App = () => { 32 | return 33 | } 34 | ``` 35 | 36 | ## More flexibility with `useAsync` 37 | 38 | For most data fetching needs, `useFetch` is sufficient. However, sometimes you may want to take full control, for 39 | example if you want to combine multiple requests. In this case you can use the 40 | [`useAsync` hook](../api/interfaces.md#useasync-hook). 41 | 42 | The core concept of `useAsync` (and React Async in general), is the [`promiseFn`](../api/options.md#promisefn): a 43 | function that returns a `Promise`. It's the fundamental concept for modelling asynchronous operations. It enables React 44 | Async to take control over scheduling, the Promise lifecycle and things like (re)starting an operation on user action or 45 | other changes. We've deliberately chosen the `Promise` as our primitive, because it's natively supported and has various 46 | utility methods like `Promise.all`. That's also why you'll find our terminology closely follows the Promise [states and 47 | fates]. 48 | 49 | The above example, written with `useAsync`, would look like this: 50 | 51 | ```jsx 52 | import React from "react" 53 | import { useAsync } from "react-async" 54 | 55 | const fetchPerson = async ({ id }, { signal }) => { 56 | const response = await fetch(`https://swapi.co/api/people/${id}/`, { signal }) 57 | if (!response.ok) throw new Error(response.status) 58 | return response.json() 59 | } 60 | 61 | const Person = ({ id }) => { 62 | const { data, error } = useAsync({ promiseFn: fetchPerson, id }) 63 | if (error) return error.message 64 | if (data) return `Hi, my name is ${data.name}!` 65 | return null 66 | } 67 | 68 | const App = () => { 69 | return 70 | } 71 | ``` 72 | 73 | Notice the incoming parameters to `fetchPerson`. The `promiseFn` will be invoked with a `props` object and an 74 | `AbortController`. `props` are the options you passed to `useAsync`, which is why you can access the `id` property 75 | using [object destructuring]. The `AbortController` is created by React Async to enable [abortable fetch], so the 76 | underlying request will be aborted when the promise is cancelled (e.g. when a new one starts or we leave the page). We 77 | have to pass its `AbortSignal` down to `fetch` in order to wire this up. 78 | 79 | [states and fates]: https://github.com/domenic/promises-unwrapping/blob/master/docs/states-and-fates.md 80 | [object destructuring]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Object_destructuring 81 | [abortable fetch]: https://developers.google.com/web/updates/2017/09/abortable-fetch 82 | -------------------------------------------------------------------------------- /docs/guide/optimistic-updates.md: -------------------------------------------------------------------------------- 1 | # Optimistic updates 2 | 3 | A powerful pattern to improve your app's perceived performance is optimistic updates. When building an async action, you 4 | might be able to predict the outcome of the operation. If so, you can implement optimistic updates by proactively 5 | setting the `data` to the predicted value, when starting the async action. Once the action completes, it will update 6 | `data` to the actual value, probably the same value as predicted. 7 | 8 | The following example uses both `promiseFn` and `deferFn` along with [`setData`](../api/state.md#setdata) to implement 9 | optimistic updates. 10 | 11 | ```jsx 12 | import Async from "react-async" 13 | 14 | const getAttendance = () => fetch("/attendance").then(() => true, () => false) 15 | const updateAttendance = ([attend]) => 16 | fetch("/attendance", { method: attend ? "POST" : "DELETE" }).then(() => attend, () => !attend) 17 | 18 | const AttendanceToggle = () => ( 19 | 20 | {({ isPending, data: isAttending, run, setData }) => ( 21 | { 24 | setData(!isAttending) 25 | run(!isAttending) 26 | }} 27 | disabled={isPending} 28 | /> 29 | )} 30 | 31 | ) 32 | ``` 33 | 34 | Here we have a switch to toggle attentance for an event. Clicking the toggle will most likely succeed, so we can predict 35 | the value it will have after completion (because we're just flipping a boolean). 36 | 37 | Notice that React Async accepts both a `promiseFn` and a `deferFn` at the same time. This allows you to combine data 38 | fetching with performing actions. A typical example of where this is useful is with forms, where you first want to 39 | populate the fields with current values from the database, and send the new values back when submitting the form. Do 40 | note that `promiseFn` and `deferFn` operate on the same `data`, so they should both resolve to a similar kind of value. 41 | -------------------------------------------------------------------------------- /docs/guide/separating-view-logic.md: -------------------------------------------------------------------------------- 1 | # Separating view and logic 2 | 3 | It's generally good practice to separate view components from logic components. Async components should preferably be 4 | logic-only. That means they don't render anything by themselves. Instead you can use the [render props] pattern to pass 5 | down the async state: 6 | 7 | ```jsx 8 | import React from "react" 9 | import { useAsync } from "react-async" 10 | 11 | const fetchPerson = async ({ id }, { signal }) => { 12 | const response = await fetch(`https://swapi.co/api/people/${id}/`, { signal }) 13 | if (!response.ok) throw new Error(response.statusText) 14 | return response.json() 15 | } 16 | 17 | const Person = ({ id }) => { 18 | const state = useAsync({ promiseFn: fetchPerson, id }) 19 | return children(state) 20 | } 21 | 22 | const App = () => { 23 | return ( 24 | 25 | {({ isPending, data, error }) => { 26 | if (isPending) return "Loading..." 27 | if (error) return 28 | if (data) return 29 | return null 30 | }} 31 | 32 | ) 33 | } 34 | ``` 35 | 36 | > `ErrorMessage` and `Greeting` would be separate view components defined elsewhere. 37 | 38 | [render props]: https://reactjs.org/docs/render-props.html 39 | 40 | ## Cleaning up the JSX 41 | 42 | You'll notice the render props pattern is very powerful, but can also lead to code that's hard to read and understand. 43 | To make your JSX more declarative and less cluttered, you can use the [``](../api/interfaces.md#async-component) 44 | component and its [state helpers](../api/helpers.md). These take away the need for `if/else` statements and `return` 45 | keywords in your JSX. 46 | 47 | ```jsx 48 | import React from "react" 49 | import Async from "react-async" 50 | 51 | const fetchPerson = async ({ id }, { signal }) => { 52 | const response = await fetch(`https://swapi.co/api/people/${id}/`, { signal }) 53 | if (!response.ok) throw new Error(response.statusText) 54 | return response.json() 55 | } 56 | 57 | const App = () => { 58 | return ( 59 | 60 | Loading... 61 | {error => } 62 | {data => } 63 | 64 | ) 65 | } 66 | ``` 67 | 68 | You should know that these helper components do not have to be direct children of the `` component. Because they 69 | are automatically wired up using [Context], they can be placed anywhere down the component tree, so long as they are 70 | descendants. You can also use helpers of the same type, multiple times. 71 | 72 | Stand-alone versions of `` and the like are also available. However, these must be wired up manually by 73 | passing the `state` prop and are therefore only really useful when combined with one of the async hooks. 74 | 75 | [context]: https://reactjs.org/docs/context.html 76 | -------------------------------------------------------------------------------- /docs/guide/server-side-rendering.md: -------------------------------------------------------------------------------- 1 | # Server-side rendering 2 | 3 | There's a good chance you're using React with Server-side rendering (SSR), as many applications require this to be 4 | successful. If you happen to be using Next.js, it's really easy to integrate React Async. The crux is in setting a 5 | [`initialValue`](../api/options.md#initialvalue), which is fetched server-side for initial page loads and passed along 6 | through rehydration. 7 | 8 | ```jsx 9 | import fetch from "isomorphic-unfetch" 10 | 11 | const fetchPerson = async ({ id }) => { 12 | const response = await fetch(`https://swapi.co/api/people/${id}/`) 13 | if (!response.ok) throw new Error(response.status) 14 | return response.json() 15 | } 16 | 17 | const Person = ({ id, person }) => ( 18 | 19 | Loading... 20 | {error => } 21 | {data => } 22 | 23 | ) 24 | 25 | Person.getInitialProps = async ({ req }) => { 26 | const id = req.params.id 27 | const person = await fetchPerson({ id }) 28 | return { id, person } 29 | } 30 | ``` 31 | 32 | If React Async is provided an `initialValue`, it will not invoke the `promiseFn` on mount. Instead it will use the 33 | `initialValue` to immediately set `data` or `error`, and render accordingly. 34 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | React Async is a utility belt for declarative promise resolution and data fetching. It makes it easy to handle 4 | asynchronous UI states, without assumptions about the shape of your data or the type of request. React Async consists of 5 | a React component and several hooks. You can use it with `fetch`, Axios or other data fetching libraries, even GraphQL. 6 | 7 | ## Rationale 8 | 9 | React Async is different in that it tries to resolve data as close as possible to where it will be used, while using 10 | declarative syntax, using just JSX and native promises. This is in contrast to systems like Redux where you would 11 | configure any data fetching or updates on a higher (application global) level, using a special construct 12 | (actions/reducers). 13 | 14 | React Async works well even in larger applications with multiple or nested data dependencies. It encourages loading data 15 | on-demand and in parallel at component level instead of in bulk at the route/page level. It's entirely decoupled from 16 | your routes, so it works well in complex applications that have a dynamic routing model or don't use routes at all. 17 | 18 | React Async is promise-based, so you can resolve anything you want, not just `fetch` requests. 19 | 20 | ## Concurrent React and Suspense 21 | 22 | The React team is currently working on a large rewrite called [Concurrent React], previously known as "Async React". 23 | Part of this rewrite is Suspense, which is a generic way for components to suspend rendering while they load data from a 24 | cache. It can render a fallback UI while loading data, much like ``. 25 | 26 | React Async has no direct relation to Concurrent React. They are conceptually close, but not the same. React Async is 27 | meant to make dealing with asynchronous business logic easier. Concurrent React will make those features have less 28 | impact on performance and usability. When Suspense lands, React Async will make full use of Suspense features. In fact, 29 | you can already **start using React Async right now**, and in a later update, you'll **get Suspense features for free**. 30 | In fact, React Async already has experimental support for Suspense, by passing the `suspense` option. 31 | 32 | [concurrent react]: https://reactjs.org/docs/concurrent-mode-intro.html 33 | -------------------------------------------------------------------------------- /examples/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:prettier/recommended", 4 | "plugin:promise/recommended", 5 | "plugin:react/recommended" 6 | ], 7 | "parser": "babel-eslint", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | } 12 | }, 13 | "plugins": ["jest", "promise", "react", "react-hooks"], 14 | "rules": { 15 | "react/prop-types": "off", 16 | "react-hooks/rules-of-hooks": "error" 17 | }, 18 | "settings": { 19 | "react": { 20 | "version": "detect" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/basic-fetch/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/basic-fetch/README.md: -------------------------------------------------------------------------------- 1 | # Basic fetch with React Async 2 | 3 | This demonstrates a very simple HTTP GET using `fetch`, wrapped with React Async. 4 | 5 | 6 | live demo 7 | 8 | -------------------------------------------------------------------------------- /examples/basic-fetch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-fetch-example", 3 | "version": "10.0.0", 4 | "private": true, 5 | "homepage": "https://react-async.async-library.now.sh/examples/basic-fetch", 6 | "scripts": { 7 | "bootstrap": "yarn install", 8 | "postinstall": "relative-deps", 9 | "prestart": "relative-deps", 10 | "prebuild": "relative-deps", 11 | "pretest": "relative-deps", 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "now-build": "SKIP_PREFLIGHT_CHECK=true react-scripts build" 16 | }, 17 | "dependencies": { 18 | "react": "16.11.0", 19 | "react-async": "^10.0.0", 20 | "react-async-devtools": "^10.0.0", 21 | "react-dom": "16.11.0", 22 | "react-scripts": "3.4.1" 23 | }, 24 | "devDependencies": { 25 | "relative-deps": "0.2.0" 26 | }, 27 | "relativeDependencies": { 28 | "react-async": "../../packages/react-async/pkg", 29 | "react-async-devtools": "../../packages/react-async-devtools/pkg" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": [ 35 | ">0.2%", 36 | "not dead", 37 | "not ie <= 11", 38 | "not op_mini all" 39 | ], 40 | "engines": { 41 | "node": ">=8" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/basic-fetch/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/examples/basic-fetch/public/favicon.ico -------------------------------------------------------------------------------- /examples/basic-fetch/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React App 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/basic-fetch/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 20px; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", 5 | "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | .user { 11 | display: inline-block; 12 | margin: 20px; 13 | text-align: center; 14 | } 15 | 16 | .avatar { 17 | background: #eee; 18 | border-radius: 64px; 19 | width: 128px; 20 | height: 128px; 21 | } 22 | 23 | .name { 24 | margin-top: 10px; 25 | } 26 | 27 | .placeholder { 28 | opacity: 0.5; 29 | } 30 | -------------------------------------------------------------------------------- /examples/basic-fetch/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Async from "react-async" 3 | import DevTools from "react-async-devtools" 4 | import ReactDOM from "react-dom" 5 | import "./index.css" 6 | 7 | const loadUser = ({ userId }) => 8 | fetch(`https://reqres.in/api/users/${userId}`) 9 | .then(res => (res.ok ? res : Promise.reject(res))) 10 | .then(res => res.json()) 11 | 12 | const UserPlaceholder = () => ( 13 |
14 |
15 |
══════
16 |
17 | ) 18 | 19 | const UserDetails = ({ data }) => ( 20 |
21 | 22 |
23 | {data.data.first_name} {data.data.last_name} 24 |
25 |
26 | ) 27 | 28 | export const App = () => ( 29 | <> 30 | 31 | 32 | {({ data, error, isPending }) => { 33 | if (isPending) return 34 | if (error) return

{error.message}

35 | if (data) return 36 | return null 37 | }} 38 |
39 | 40 | 41 | 42 | 43 | 44 | {data => } 45 | {error =>

{error.message}

}
46 |
47 | 48 | ) 49 | 50 | if (process.env.NODE_ENV !== "test") ReactDOM.render(, document.getElementById("root")) 51 | -------------------------------------------------------------------------------- /examples/basic-fetch/src/index.test.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import { App } from "./" 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div") 7 | ReactDOM.render(, div) 8 | ReactDOM.unmountComponentAtNode(div) 9 | }) 10 | -------------------------------------------------------------------------------- /examples/basic-hook/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/basic-hook/README.md: -------------------------------------------------------------------------------- 1 | # Basic fetch with useAsync hook 2 | 3 | This demonstrates how to use the `useAsync` hook. 4 | 5 | 6 | live demo 7 | 8 | -------------------------------------------------------------------------------- /examples/basic-hook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-hook-example", 3 | "version": "10.0.0", 4 | "private": true, 5 | "homepage": "https://react-async.async-library.now.sh/examples/basic-hook", 6 | "scripts": { 7 | "bootstrap": "yarn install", 8 | "postinstall": "relative-deps", 9 | "prestart": "relative-deps", 10 | "prebuild": "relative-deps", 11 | "pretest": "relative-deps", 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "now-build": "SKIP_PREFLIGHT_CHECK=true react-scripts build" 16 | }, 17 | "dependencies": { 18 | "react": "16.11.0", 19 | "react-async": "^10.0.0", 20 | "react-async-devtools": "^10.0.0", 21 | "react-dom": "16.11.0", 22 | "react-scripts": "3.4.1" 23 | }, 24 | "devDependencies": { 25 | "relative-deps": "0.2.0" 26 | }, 27 | "relativeDependencies": { 28 | "react-async": "../../packages/react-async/pkg", 29 | "react-async-devtools": "../../packages/react-async-devtools/pkg" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": [ 35 | ">0.2%", 36 | "not dead", 37 | "not ie <= 11", 38 | "not op_mini all" 39 | ], 40 | "engines": { 41 | "node": ">=8" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/basic-hook/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/examples/basic-hook/public/favicon.ico -------------------------------------------------------------------------------- /examples/basic-hook/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React App 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/basic-hook/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 20px; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", 5 | "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | .user { 11 | display: inline-block; 12 | margin: 20px; 13 | text-align: center; 14 | } 15 | 16 | .avatar { 17 | background: #eee; 18 | border-radius: 64px; 19 | width: 128px; 20 | height: 128px; 21 | } 22 | 23 | .name { 24 | margin-top: 10px; 25 | } 26 | 27 | .placeholder { 28 | opacity: 0.5; 29 | } 30 | -------------------------------------------------------------------------------- /examples/basic-hook/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useAsync, IfPending, IfFulfilled, IfRejected } from "react-async" 3 | import ReactDOM from "react-dom" 4 | import DevTools from "react-async-devtools" 5 | import "./index.css" 6 | 7 | const loadUser = ({ userId }) => 8 | fetch(`https://reqres.in/api/users/${userId}`) 9 | .then(res => (res.ok ? res : Promise.reject(res))) 10 | .then(res => res.json()) 11 | .then(({ data }) => data) 12 | 13 | const UserPlaceholder = () => ( 14 |
15 |
16 |
══════
17 |
18 | ) 19 | 20 | const UserDetails = ({ data }) => ( 21 |
22 | 23 |
24 | {data.first_name} {data.last_name} 25 |
26 |
27 | ) 28 | 29 | const User = ({ userId }) => { 30 | const state = useAsync({ promiseFn: loadUser, debugLabel: `User ${userId}`, userId }) 31 | return ( 32 | <> 33 | 34 | 35 | 36 | {data => } 37 | {error =>

{error.message}

}
38 | 39 | ) 40 | } 41 | 42 | export const App = () => ( 43 | <> 44 | 45 | 46 | 47 | 48 | ) 49 | 50 | if (process.env.NODE_ENV !== "test") ReactDOM.render(, document.getElementById("root")) 51 | -------------------------------------------------------------------------------- /examples/basic-hook/src/index.test.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import { App } from "./" 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div") 7 | ReactDOM.render(, div) 8 | ReactDOM.unmountComponentAtNode(div) 9 | }) 10 | -------------------------------------------------------------------------------- /examples/custom-instance/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/custom-instance/README.md: -------------------------------------------------------------------------------- 1 | # Custom React Async instance 2 | 3 | Demonstrates how to use a preconfigured React Async instance. 4 | 5 | 6 | live demo 7 | 8 | -------------------------------------------------------------------------------- /examples/custom-instance/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-instance-example", 3 | "version": "10.0.0", 4 | "private": true, 5 | "homepage": "https://react-async.async-library.now.sh/examples/custom-instance", 6 | "scripts": { 7 | "bootstrap": "yarn install", 8 | "postinstall": "relative-deps", 9 | "prestart": "relative-deps", 10 | "prebuild": "relative-deps", 11 | "pretest": "relative-deps", 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "now-build": "SKIP_PREFLIGHT_CHECK=true react-scripts build" 16 | }, 17 | "dependencies": { 18 | "react": "16.11.0", 19 | "react-async": "^10.0.0", 20 | "react-async-devtools": "^10.0.0", 21 | "react-dom": "16.11.0", 22 | "react-scripts": "3.4.1" 23 | }, 24 | "devDependencies": { 25 | "relative-deps": "0.2.0" 26 | }, 27 | "relativeDependencies": { 28 | "react-async": "../../packages/react-async/pkg", 29 | "react-async-devtools": "../../packages/react-async-devtools/pkg" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": [ 35 | ">0.2%", 36 | "not dead", 37 | "not ie <= 11", 38 | "not op_mini all" 39 | ], 40 | "engines": { 41 | "node": ">=8" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/custom-instance/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/examples/custom-instance/public/favicon.ico -------------------------------------------------------------------------------- /examples/custom-instance/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React App 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/custom-instance/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 20px; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", 5 | "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | .user { 11 | display: inline-block; 12 | margin: 20px; 13 | text-align: center; 14 | } 15 | 16 | .avatar { 17 | background: #eee; 18 | border-radius: 64px; 19 | width: 128px; 20 | height: 128px; 21 | } 22 | 23 | .name { 24 | margin-top: 10px; 25 | } 26 | 27 | .placeholder { 28 | opacity: 0.5; 29 | } 30 | -------------------------------------------------------------------------------- /examples/custom-instance/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { createInstance } from "react-async" 3 | import DevTools from "react-async-devtools" 4 | import ReactDOM from "react-dom" 5 | import "./index.css" 6 | 7 | const loadUser = ({ userId }) => 8 | fetch(`https://reqres.in/api/users/${userId}`) 9 | .then(res => (res.ok ? res : Promise.reject(res))) 10 | .then(res => res.json()) 11 | 12 | const AsyncUser = createInstance({ promiseFn: loadUser }, "AsyncUser") 13 | 14 | const UserPlaceholder = () => ( 15 |
16 |
17 |
══════
18 |
19 | ) 20 | 21 | const UserDetails = ({ data }) => ( 22 |
23 | 24 |
25 | {data.data.first_name} {data.data.last_name} 26 |
27 |
28 | ) 29 | 30 | export const App = () => ( 31 | <> 32 | 33 | 34 | {({ data, error, isPending }) => { 35 | if (isPending) return 36 | if (error) return

{error.message}

37 | if (data) return 38 | return null 39 | }} 40 |
41 | 42 | 43 | 44 | 45 | {data => } 46 | {error =>

{error.message}

}
47 |
48 | 49 | ) 50 | 51 | if (process.env.NODE_ENV !== "test") ReactDOM.render(, document.getElementById("root")) 52 | -------------------------------------------------------------------------------- /examples/custom-instance/src/index.test.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import { App } from "./" 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div") 7 | ReactDOM.render(, div) 8 | ReactDOM.unmountComponentAtNode(div) 9 | }) 10 | -------------------------------------------------------------------------------- /examples/movie-app/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/movie-app/README.md: -------------------------------------------------------------------------------- 1 | # Movie app using React Async 2 | 3 | This is a rebuild of the [React Suspense IO demo](https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html) 4 | by Dan Abramov at JSConf Iceland 2018, using React Async instead of Suspense. 5 | 6 | 7 | live demo 8 | 9 | -------------------------------------------------------------------------------- /examples/movie-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movie-app-example", 3 | "version": "10.0.0", 4 | "private": true, 5 | "homepage": "https://react-async.async-library.now.sh/examples/movie-app", 6 | "scripts": { 7 | "bootstrap": "yarn install", 8 | "postinstall": "relative-deps", 9 | "prestart": "relative-deps", 10 | "prebuild": "relative-deps", 11 | "pretest": "relative-deps", 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "now-build": "SKIP_PREFLIGHT_CHECK=true react-scripts build" 16 | }, 17 | "dependencies": { 18 | "react": "16.11.0", 19 | "react-async": "^10.0.0", 20 | "react-async-devtools": "^10.0.0", 21 | "react-dom": "16.11.0", 22 | "react-scripts": "3.4.1" 23 | }, 24 | "devDependencies": { 25 | "relative-deps": "0.2.0" 26 | }, 27 | "relativeDependencies": { 28 | "react-async": "../../packages/react-async/pkg", 29 | "react-async-devtools": "../../packages/react-async-devtools/pkg" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": [ 35 | ">0.2%", 36 | "not dead", 37 | "not ie <= 11", 38 | "not op_mini all" 39 | ], 40 | "engines": { 41 | "node": ">=8" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/movie-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/examples/movie-app/public/favicon.ico -------------------------------------------------------------------------------- /examples/movie-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/movie-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/movie-app/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | max-width: 500px; 3 | margin: 0 auto; 4 | padding: 20px; 5 | } 6 | 7 | .Movie { 8 | position: relative; 9 | margin: 20px 0; 10 | background-color: #090e21; 11 | background-position: center; 12 | background-size: cover; 13 | border-radius: 5px; 14 | cursor: pointer; 15 | } 16 | .Movie .content { 17 | border-radius: 4px; 18 | background-image: linear-gradient(rgba(9, 14, 33, 0.6), rgb(9, 14, 33)); 19 | padding: 20px; 20 | display: flex; 21 | flex-direction: column; 22 | flex-grow: 1; 23 | } 24 | .Movie .content:hover { 25 | background-color: rgba(9, 14, 33, 0.6); 26 | } 27 | .Movie .title { 28 | font-size: 1.5em; 29 | font-weight: bold; 30 | margin-bottom: 8px; 31 | } 32 | .Movie .info { 33 | font-size: 0.9em; 34 | opacity: 0.5; 35 | } 36 | .Movie .desc { 37 | margin-top: 16px; 38 | font-size: 0.9em; 39 | line-height: 1.4em; 40 | opacity: 0.7; 41 | } 42 | 43 | .Details .main { 44 | display: flex; 45 | margin: 16px 0; 46 | align-items: flex-start; 47 | } 48 | .Details .info { 49 | display: flex; 50 | flex-direction: column; 51 | flex-grow: 1; 52 | line-height: 1.4em; 53 | } 54 | .Details h1 { 55 | margin: 10px 0 20px; 56 | line-height: 1em; 57 | } 58 | .Details img { 59 | margin-right: 20px; 60 | } 61 | .Details .ratings { 62 | display: flex; 63 | border-top: 2px solid #111629; 64 | padding: 15px 0; 65 | } 66 | .Details .rating { 67 | flex-grow: 1; 68 | } 69 | .Details .rating .title { 70 | margin-bottom: 10px; 71 | text-transform: uppercase; 72 | font-size: smaller; 73 | font-weight: bold; 74 | color: #949aa4; 75 | } 76 | .Details .rating .score { 77 | font-size: 2em; 78 | font-weight: bolder; 79 | } 80 | .Details .reviews { 81 | display: flex; 82 | flex-wrap: wrap; 83 | } 84 | .Details .review { 85 | margin-bottom: 16px; 86 | padding: 12px; 87 | background-color: #090e21; 88 | border: 1px solid #151828; 89 | border-radius: 3px; 90 | } 91 | .Details .review p { 92 | margin-top: 0; 93 | line-height: 1.4em; 94 | font-size: 0.9em; 95 | } 96 | .Details .review small { 97 | color: #949aa4; 98 | } 99 | -------------------------------------------------------------------------------- /examples/movie-app/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from "react" 2 | import Async from "react-async" 3 | import "./App.css" 4 | 5 | const apiRoot = "https://api.tmdb.org/3" 6 | const apiKey = "aac95dd4fa440999d92dcc8191cab0ee" 7 | 8 | const delay = ms => value => new Promise(resolve => setTimeout(resolve, ms, value)) 9 | 10 | const fetchMovies = () => 11 | fetch(`${apiRoot}/movie/popular?api_key=${apiKey}`) 12 | .then(res => (res.ok ? res : Promise.reject(res))) 13 | .then(res => res.json()) 14 | .then(data => data.results) 15 | .then(delay(500)) 16 | 17 | const fetchMovieDetails = ({ id }) => 18 | fetch(`${apiRoot}/movie/${id}?api_key=${apiKey}`) 19 | .then(res => (res.ok ? res : Promise.reject(res))) 20 | .then(res => res.json()) 21 | .then(delay(500)) 22 | 23 | const fetchMovieReviews = ({ id }) => 24 | fetch(`${apiRoot}/movie/${id}/reviews?api_key=${apiKey}`) 25 | .then(res => (res.ok ? res : Promise.reject(res))) 26 | .then(res => res.json()) 27 | .then(data => data.results) 28 | .then(delay(1500)) 29 | 30 | const Movie = ({ title, vote_average, release_date, onSelect, overview, backdrop_path }) => ( 31 |
36 |
37 | {title} 38 | 39 | {vote_average * 10}% · {release_date} 40 | 41 | {overview} 42 |
43 |
44 | ) 45 | 46 | const TopMovies = ({ handleSelect }) => ( 47 | 48 |

49 | Top Box Office{" "} 50 | 51 | 🍿 52 | 53 |

54 | 55 | 56 |

Loading...

57 |
58 | 59 | {movies => 60 | movies.map(movie => ) 61 | } 62 | 63 |
64 |
65 | ) 66 | 67 | const Review = ({ author, content }) => ( 68 |
69 |

{content}

70 | {author} 71 |
72 | ) 73 | 74 | const Details = ({ onBack, id }) => ( 75 |
76 | 81 | 87 | 88 |

Loading...

89 |
90 | 91 | {movie => ( 92 | 93 |
94 | 99 |
100 |

{movie.title}

101 |
102 |
103 |
Rating
104 |
{movie.vote_average * 10}%
105 |
106 |
107 |
108 |

{movie.overview}

109 |
110 |
111 |
112 |
113 | 119 | 120 |

Loading...

121 |
122 | {reviews => reviews.map(Review)} 123 |
124 |
125 |
126 | )} 127 |
128 |
129 |
130 | ) 131 | 132 | class App extends Component { 133 | state = { selectedMovie: undefined } 134 | select = movie => () => this.setState({ selectedMovie: movie.id }) 135 | render() { 136 | const { selectedMovie } = this.state 137 | return ( 138 |
139 | {selectedMovie ? ( 140 |
141 | ) : ( 142 | 143 | )} 144 |
145 | ) 146 | } 147 | } 148 | 149 | export default App 150 | -------------------------------------------------------------------------------- /examples/movie-app/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import App from "./App" 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div") 7 | ReactDOM.render(, div) 8 | ReactDOM.unmountComponentAtNode(div) 9 | }) 10 | -------------------------------------------------------------------------------- /examples/movie-app/src/api.js: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/BenoitZugmeyer/react-suspense-demo 2 | 3 | const CORS_PROXY_URL = "https://cors-anywhere.herokuapp.com" 4 | 5 | async function responseAsDOM(response) { 6 | const text = await response.text() 7 | const parser = new DOMParser() 8 | return parser.parseFromString(text, "text/html") 9 | } 10 | 11 | async function readBodyAndDecode(response) { 12 | const buffer = await response.arrayBuffer() 13 | 14 | const decoder = new TextDecoder("iso-8859-1") 15 | return decoder.decode(buffer) 16 | } 17 | 18 | async function scrapPage(path) { 19 | if (path.startsWith("/")) path = path.slice(1) 20 | const response = await fetch(`${CORS_PROXY_URL}/https://www.rottentomatoes.com/${path}`) 21 | return responseAsDOM(response) 22 | } 23 | 24 | export async function fetchMovies() { 25 | const document = await scrapPage("browse/in-theaters") 26 | for (const script of document.querySelectorAll("script")) { 27 | if (script.innerText.includes("loadPage")) { 28 | const movieListMatch = script.innerText.match(/^\s+(\[\{.*\}\]),$/m) 29 | if (movieListMatch) return JSON.parse(movieListMatch[1]) 30 | } 31 | } 32 | throw new Error("Failed to fetch movies") 33 | } 34 | 35 | export async function fetchMovieDetails({ id }) { 36 | const response = await fetch(`https://www.rottentomatoes.com/api/private/v1.0/movies/${id}.json`) 37 | const text = await readBodyAndDecode(response) 38 | 39 | return JSON.parse(text) 40 | } 41 | 42 | export async function fetchMovieReviews({ id }) { 43 | const details = await fetchMovieDetails({ id }) 44 | 45 | // Simulate long delay 46 | await new Promise(resolve => setTimeout(resolve, 2000)) 47 | 48 | return details.reviews.reviews 49 | } 50 | -------------------------------------------------------------------------------- /examples/movie-app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | background: #00010a; 5 | font-family: sans-serif; 6 | color: #fff; 7 | } 8 | 9 | h1 { 10 | margin: 0; 11 | } 12 | 13 | button { 14 | background: none; 15 | border: none; 16 | font-size: 1.5em; 17 | cursor: pointer; 18 | } 19 | -------------------------------------------------------------------------------- /examples/movie-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import DevTools from "react-async-devtools" 3 | import ReactDOM from "react-dom" 4 | import "./index.css" 5 | import App from "./App" 6 | // import * as serviceWorker from './serviceWorker'; 7 | 8 | ReactDOM.render( 9 | <> 10 | 11 | 12 | , 13 | document.getElementById("root") 14 | ) 15 | 16 | // If you want your app to work offline and load faster, you can change 17 | // unregister() to register() below. Note this comes with some pitfalls. 18 | // Learn more about service workers: http://bit.ly/CRA-PWA 19 | // serviceWorker.unregister(); 20 | -------------------------------------------------------------------------------- /examples/movie-app/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /examples/with-abortcontroller/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/with-abortcontroller/README.md: -------------------------------------------------------------------------------- 1 | # Abortable fetch with React Async 2 | 3 | This demonstrates a very simple HTTP GET using `fetch`, which gets the AbortSignal passed in to actually abort the HTTP request when the promise is canceled. 4 | 5 | 6 | live demo 7 | 8 | -------------------------------------------------------------------------------- /examples/with-abortcontroller/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-abortcontroller-example", 3 | "version": "10.0.0", 4 | "private": true, 5 | "homepage": "https://react-async.async-library.now.sh/examples/with-abortcontroller", 6 | "scripts": { 7 | "bootstrap": "yarn install", 8 | "postinstall": "relative-deps", 9 | "prestart": "relative-deps", 10 | "prebuild": "relative-deps", 11 | "pretest": "relative-deps", 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "now-build": "SKIP_PREFLIGHT_CHECK=true react-scripts build" 16 | }, 17 | "dependencies": { 18 | "react": "16.11.0", 19 | "react-async": "^10.0.0", 20 | "react-async-devtools": "^10.0.0", 21 | "react-dom": "16.11.0", 22 | "react-scripts": "3.4.1" 23 | }, 24 | "devDependencies": { 25 | "relative-deps": "0.2.0" 26 | }, 27 | "relativeDependencies": { 28 | "react-async": "../../packages/react-async/pkg", 29 | "react-async-devtools": "../../packages/react-async-devtools/pkg" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": [ 35 | ">0.2%", 36 | "not dead", 37 | "not ie <= 11", 38 | "not op_mini all" 39 | ], 40 | "engines": { 41 | "node": ">=8" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/with-abortcontroller/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/examples/with-abortcontroller/public/favicon.ico -------------------------------------------------------------------------------- /examples/with-abortcontroller/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React App 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/with-abortcontroller/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 20px; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", 5 | "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | button { 11 | background: none; 12 | color: palevioletred; 13 | border: 2px solid palevioletred; 14 | border-radius: 5px; 15 | padding: 10px 20px; 16 | font-size: 0.9em; 17 | font-weight: bold; 18 | text-transform: uppercase; 19 | } 20 | 21 | button:focus { 22 | outline: 0; 23 | } 24 | -------------------------------------------------------------------------------- /examples/with-abortcontroller/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useAsync } from "react-async" 3 | import DevTools from "react-async-devtools" 4 | import ReactDOM from "react-dom" 5 | import "./index.css" 6 | 7 | const download = (args, props, controller) => 8 | fetch(`https://reqres.in/api/users/1?delay=3`, { signal: controller.signal }) 9 | .then(res => (res.ok ? res : Promise.reject(res))) 10 | .then(res => res.json()) 11 | 12 | export const App = () => { 13 | const { run, cancel, isPending } = useAsync({ deferFn: download, debugLabel: "User 1" }) 14 | return ( 15 | <> 16 | {isPending ? : } 17 | {isPending ? ( 18 |

Loading...

19 | ) : ( 20 |

Inspect network traffic to see requests being canceled.

21 | )} 22 | 23 | ) 24 | } 25 | 26 | if (process.env.NODE_ENV !== "test") 27 | ReactDOM.render( 28 | <> 29 | 30 | 31 | , 32 | document.getElementById("root") 33 | ) 34 | -------------------------------------------------------------------------------- /examples/with-abortcontroller/src/index.test.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import { App } from "./" 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div") 7 | ReactDOM.render(, div) 8 | ReactDOM.unmountComponentAtNode(div) 9 | }) 10 | -------------------------------------------------------------------------------- /examples/with-graphql/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/with-graphql/README.md: -------------------------------------------------------------------------------- 1 | # GraphQL with useAsync 2 | 3 | This demonstrates how to use the `useAsync` hook with GraphQL. 4 | 5 | 6 | live demo 7 | 8 | -------------------------------------------------------------------------------- /examples/with-graphql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-graphql-example", 3 | "version": "10.0.0", 4 | "private": true, 5 | "homepage": "https://react-async.async-library.now.sh/examples/with-graphql", 6 | "scripts": { 7 | "bootstrap": "yarn install", 8 | "postinstall": "relative-deps", 9 | "prestart": "relative-deps", 10 | "prebuild": "relative-deps", 11 | "pretest": "relative-deps", 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "now-build": "SKIP_PREFLIGHT_CHECK=true react-scripts build" 16 | }, 17 | "dependencies": { 18 | "graphql-request": "1.8.2", 19 | "react": "16.11.0", 20 | "react-async": "^10.0.0", 21 | "react-async-devtools": "^10.0.0", 22 | "react-dom": "16.11.0", 23 | "react-scripts": "3.4.1" 24 | }, 25 | "devDependencies": { 26 | "relative-deps": "0.2.0" 27 | }, 28 | "relativeDependencies": { 29 | "react-async": "../../packages/react-async/pkg", 30 | "react-async-devtools": "../../packages/react-async-devtools/pkg" 31 | }, 32 | "eslintConfig": { 33 | "extends": "react-app" 34 | }, 35 | "browserslist": [ 36 | ">0.2%", 37 | "not dead", 38 | "not ie <= 11", 39 | "not op_mini all" 40 | ], 41 | "engines": { 42 | "node": ">=8" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/with-graphql/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/examples/with-graphql/public/favicon.ico -------------------------------------------------------------------------------- /examples/with-graphql/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React App 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/with-graphql/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 20px; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", 5 | "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | .movie { 11 | margin-bottom: 40px; 12 | line-height: 1.5em; 13 | } 14 | .movie dt { 15 | font-weight: bold; 16 | } 17 | .movie dd { 18 | margin-left: 10px; 19 | } 20 | -------------------------------------------------------------------------------- /examples/with-graphql/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useAsync, IfPending, IfFulfilled, IfRejected } from "react-async" 3 | import ReactDOM from "react-dom" 4 | import DevTools from "react-async-devtools" 5 | import { request } from "graphql-request" 6 | import "./index.css" 7 | 8 | const query = /* GraphQL */ ` 9 | query getMovie($slug: String!) { 10 | Movie(slug: $slug) { 11 | title 12 | releaseDate 13 | actors { 14 | id 15 | name 16 | } 17 | } 18 | } 19 | ` 20 | 21 | const loadMovie = async variables => { 22 | const { Movie } = await request("https://api.graph.cool/simple/v1/movies", query, variables) 23 | return Movie 24 | } 25 | 26 | const MovieDetails = ({ data }) => ( 27 |
28 |

{data.title}

29 |
30 |
Released
31 |
{data.releaseDate.substr(0, 10)}
32 |
Featuring
33 | {data.actors.map(actor => ( 34 |
{actor.name}
35 | ))} 36 |
37 |
38 | ) 39 | 40 | const Movie = ({ slug }) => { 41 | const state = useAsync({ promiseFn: loadMovie, debugLabel: slug, slug }) 42 | return ( 43 | <> 44 | 45 |

Loading...

46 |
47 | {data => } 48 | {error =>

{error.message}

}
49 | 50 | ) 51 | } 52 | 53 | export const App = () => ( 54 | <> 55 | 56 | 57 | 58 | 59 | 60 | ) 61 | 62 | if (process.env.NODE_ENV !== "test") ReactDOM.render(, document.getElementById("root")) 63 | -------------------------------------------------------------------------------- /examples/with-graphql/src/index.test.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import { App } from "./" 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div") 7 | ReactDOM.render(, div) 8 | ReactDOM.unmountComponentAtNode(div) 9 | }) 10 | -------------------------------------------------------------------------------- /examples/with-nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | .next -------------------------------------------------------------------------------- /examples/with-nextjs/README.md: -------------------------------------------------------------------------------- 1 | # React Async with Next.js 2 | 3 | This demonstrates how React Async integrates with Next.js. 4 | 5 | 6 | live demo 7 | 8 | -------------------------------------------------------------------------------- /examples/with-nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | target: "serverless", 3 | assetPrefix: "/examples/with-nextjs", 4 | } 5 | -------------------------------------------------------------------------------- /examples/with-nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-nextjs-example", 3 | "version": "10.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "scripts": { 7 | "bootstrap": "yarn install", 8 | "postinstall": "relative-deps", 9 | "predev": "relative-deps", 10 | "prebuild": "relative-deps", 11 | "prestart": "relative-deps", 12 | "pretest": "relative-deps", 13 | "dev": "next", 14 | "build": "next build", 15 | "start": "next start", 16 | "now-build": "next build" 17 | }, 18 | "dependencies": { 19 | "isomorphic-fetch": "2.2.1", 20 | "next": "9.1.3", 21 | "react": "16.11.0", 22 | "react-async": "^10.0.0", 23 | "react-async-devtools": "^10.0.0", 24 | "react-dom": "16.11.0" 25 | }, 26 | "devDependencies": { 27 | "relative-deps": "0.2.0" 28 | }, 29 | "relativeDependencies": { 30 | "react-async": "../../packages/react-async/pkg", 31 | "react-async-devtools": "../../packages/react-async-devtools/pkg" 32 | }, 33 | "engines": { 34 | "node": ">=8" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/with-nextjs/pages/examples/with-nextjs.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Async from "react-async" 3 | import DevTools from "react-async-devtools" 4 | import fetch from "isomorphic-fetch" 5 | import Link from "next/link" 6 | import { withRouter } from "next/router" 7 | 8 | const loadUser = ({ userId = 1 }) => 9 | fetch(`https://reqres.in/api/users/${userId}`) 10 | .then(res => (res.ok ? res : Promise.reject(res))) 11 | .then(res => res.json()) 12 | .then(data => data.data) 13 | 14 | class Hello extends React.Component { 15 | static async getInitialProps({ query }) { 16 | const data = await loadUser(query) 17 | return { data } 18 | } 19 | 20 | render() { 21 | const { data, router } = this.props 22 | const { pathname, query } = router 23 | const userId = parseInt(query.userId, 10) || 1 24 | return ( 25 | <> 26 | {process.browser && } 27 | 34 | 35 |

Loading...

36 |
37 | 38 | {data => ( 39 | <> 40 |

41 | [{userId}] Hello {data.first_name} 42 |

43 |

44 | {userId > 1 && ( 45 | 46 | Prev 47 | 48 | )}{" "} 49 | {userId < 12 && ( 50 | 51 | Next 52 | 53 | )} 54 |

55 | 56 | )} 57 |
58 | 59 | This data is initially loaded server-side, then client-side when navigating prev/next. 60 | 61 |
62 | 63 | ) 64 | } 65 | } 66 | 67 | export default withRouter(Hello) 68 | -------------------------------------------------------------------------------- /examples/with-react-native/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p12 6 | *.key 7 | *.mobileprovision 8 | *.orig.* 9 | web-build/ 10 | web-report/ 11 | -------------------------------------------------------------------------------- /examples/with-react-native/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /examples/with-react-native/App.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Async from "react-async" 3 | import { StyleSheet, Text, View, Image } from "react-native" 4 | 5 | const loadUser = ({ userId }) => 6 | fetch(`https://reqres.in/api/users/${userId}`) 7 | .then(res => (res.ok ? res : Promise.reject(res))) 8 | .then(res => res.json()) 9 | .then(json => json.data) 10 | 11 | export default function App() { 12 | return ( 13 | 14 | 15 | 16 | Loading... 17 | 18 | 19 | {user => ( 20 | <> 21 | 25 | 26 | {user.first_name} {user.last_name} 27 | 28 | 29 | )} 30 | 31 | {res => {res.status}} 32 | 33 | 34 | ) 35 | } 36 | 37 | const styles = StyleSheet.create({ 38 | container: { 39 | flex: 1, 40 | backgroundColor: "#fff", 41 | alignItems: "center", 42 | justifyContent: "center", 43 | }, 44 | }) 45 | -------------------------------------------------------------------------------- /examples/with-react-native/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "React Async example", 4 | "slug": "with-react-native", 5 | "privacy": "public", 6 | "sdkVersion": "33.0.0", 7 | "platforms": [ 8 | "ios", 9 | "android", 10 | "web" 11 | ], 12 | "version": "1.0.0", 13 | "orientation": "portrait", 14 | "icon": "./assets/icon.png", 15 | "splash": { 16 | "image": "./assets/splash.png", 17 | "resizeMode": "contain", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "updates": { 21 | "fallbackToCacheTimeout": 0 22 | }, 23 | "assetBundlePatterns": [ 24 | "**/*" 25 | ], 26 | "ios": { 27 | "supportsTablet": true 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /examples/with-react-native/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/examples/with-react-native/assets/icon.png -------------------------------------------------------------------------------- /examples/with-react-native/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/examples/with-react-native/assets/splash.png -------------------------------------------------------------------------------- /examples/with-react-native/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /examples/with-react-native/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-react-native-example", 3 | "version": "10.0.0", 4 | "private": true, 5 | "main": "node_modules/expo/AppEntry.js", 6 | "scripts": { 7 | "bootstrap": "yarn install", 8 | "postinstall": "relative-deps", 9 | "prestart": "relative-deps", 10 | "preandroid": "relative-deps", 11 | "preios": "relative-deps", 12 | "preweb": "relative-deps", 13 | "start": "expo start", 14 | "android": "expo start --android", 15 | "ios": "expo start --ios", 16 | "web": "expo start --web" 17 | }, 18 | "dependencies": { 19 | "expo": "35.0.1", 20 | "react": "16.11.0", 21 | "react-async": "^10.0.0", 22 | "react-dom": "16.11.0", 23 | "react-native": "https://github.com/expo/react-native/archive/sdk-33.0.0.tar.gz", 24 | "react-native-web": "0.11.7" 25 | }, 26 | "devDependencies": { 27 | "babel-preset-expo": "7.1.0", 28 | "relative-deps": "0.2.0" 29 | }, 30 | "relativeDependencies": { 31 | "react-async": "../../packages/react-async/pkg" 32 | }, 33 | "engines": { 34 | "node": ">=8" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/with-react-router/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react"] 3 | } -------------------------------------------------------------------------------- /examples/with-react-router/README.md: -------------------------------------------------------------------------------- 1 | ## An example of using `react-async` with `react-router` (built with parcel) 2 | 3 | The idea is to make fetching data for pages (components) configurable in a declarative way. 4 | 5 | ``` 6 | 7 | ... 8 | 9 | 10 | ``` 11 | 12 | ## Running the example 13 | 14 | ``` 15 | npm install 16 | npm start 17 | ``` 18 | 19 | Then visit [http://localhost:1234](http://localhost:1234). 20 | -------------------------------------------------------------------------------- /examples/with-react-router/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /examples/with-react-router/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import DevTools from "react-async-devtools" 3 | import ReactDOM from "react-dom" 4 | import { BrowserRouter as Router, NavLink } from "react-router-dom" 5 | import AsyncRoute from "./js/AsyncRoute" 6 | import Contributors from "./js/Contributors" 7 | import Repositories from "./js/Repositories" 8 | 9 | const App = () => ( 10 | 11 | <> 12 | 13 |
14 | Contributors To react-async 15 | Other github repositories 16 |
17 | 23 | 28 | 29 |
30 | ) 31 | 32 | document.addEventListener("DOMContentLoaded", () => { 33 | ReactDOM.render(, document.getElementById("app")) 34 | }) 35 | -------------------------------------------------------------------------------- /examples/with-react-router/js/AsyncRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Async from "react-async" 3 | import { Route } from "react-router-dom" 4 | 5 | const loader = fetchUrl => () => 6 | fetch(fetchUrl) 7 | .then(res => (res.ok ? res : Promise.reject(res))) 8 | .then(res => res.json()) 9 | 10 | const AsyncRoute = ({ component: Component, fetchUrl, ...props }) => ( 11 | ( 14 | 15 | {asyncState => } 16 | 17 | )} 18 | /> 19 | ) 20 | 21 | export default AsyncRoute 22 | -------------------------------------------------------------------------------- /examples/with-react-router/js/Contributors.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | const Contributors = ({ data, error, isPending }) => { 4 | if (isPending) return "Loading Contributers..." 5 | if (error) return "Error" 6 | return ( 7 |
    8 | {data.map(e => ( 9 |
  • {e.login}
  • 10 | ))} 11 |
12 | ) 13 | } 14 | 15 | export default Contributors 16 | -------------------------------------------------------------------------------- /examples/with-react-router/js/Repositories.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | const Repositories = ({ data, error, isPending }) => { 4 | if (isPending) return "Loading Repositories..." 5 | if (error) return "Error" 6 | return ( 7 |
    8 | {data.map(e => ( 9 |
  • {e.name}
  • 10 | ))} 11 |
12 | ) 13 | } 14 | 15 | export default Repositories 16 | -------------------------------------------------------------------------------- /examples/with-react-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-react-router-example", 3 | "version": "10.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "scripts": { 7 | "bootstrap": "yarn install", 8 | "postinstall": "relative-deps", 9 | "prestart": "relative-deps", 10 | "prebuild": "relative-deps", 11 | "start": "parcel index.html", 12 | "build": "parcel build index.html" 13 | }, 14 | "dependencies": { 15 | "react": "16.11.0", 16 | "react-async": "^10.0.0", 17 | "react-async-devtools": "^10.0.0", 18 | "react-dom": "16.11.0", 19 | "react-router-dom": "5.1.2" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "7.7.2", 23 | "@babel/preset-react": "7.7.0", 24 | "parcel-bundler": "1.12.4", 25 | "relative-deps": "0.2.0" 26 | }, 27 | "relativeDependencies": { 28 | "react-async": "../../packages/react-async/pkg", 29 | "react-async-devtools": "../../packages/react-async-devtools/pkg" 30 | }, 31 | "engines": { 32 | "node": ">=8" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/with-suspense/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/with-suspense/README.md: -------------------------------------------------------------------------------- 1 | # Basic fetch with Suspense 2 | 3 | This demonstrates how Suspense can be used to render a fallback UI while loading. 4 | 5 | 6 | live demo 7 | 8 | -------------------------------------------------------------------------------- /examples/with-suspense/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-suspense-example", 3 | "version": "10.0.0", 4 | "private": true, 5 | "homepage": "https://react-async.async-library.now.sh/examples/with-suspense", 6 | "scripts": { 7 | "bootstrap": "yarn install", 8 | "postinstall": "relative-deps", 9 | "prestart": "relative-deps", 10 | "prebuild": "relative-deps", 11 | "pretest": "relative-deps", 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "now-build": "SKIP_PREFLIGHT_CHECK=true react-scripts build" 16 | }, 17 | "dependencies": { 18 | "react": "16.11.0", 19 | "react-async": "^10.0.0", 20 | "react-async-devtools": "^10.0.0", 21 | "react-dom": "16.11.0", 22 | "react-scripts": "3.4.1" 23 | }, 24 | "devDependencies": { 25 | "relative-deps": "0.2.0" 26 | }, 27 | "relativeDependencies": { 28 | "react-async": "../../packages/react-async/pkg", 29 | "react-async-devtools": "../../packages/react-async-devtools/pkg" 30 | }, 31 | "eslintConfig": { 32 | "extends": "react-app" 33 | }, 34 | "browserslist": [ 35 | ">0.2%", 36 | "not dead", 37 | "not ie <= 11", 38 | "not op_mini all" 39 | ], 40 | "engines": { 41 | "node": ">=8" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/with-suspense/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/examples/with-suspense/public/favicon.ico -------------------------------------------------------------------------------- /examples/with-suspense/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React App 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/with-suspense/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 20px; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", 5 | "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | .user { 11 | display: inline-block; 12 | margin: 20px; 13 | text-align: center; 14 | } 15 | 16 | .avatar { 17 | background: #eee; 18 | border-radius: 64px; 19 | width: 128px; 20 | height: 128px; 21 | } 22 | 23 | .name { 24 | margin-top: 10px; 25 | } 26 | 27 | .placeholder { 28 | opacity: 0.5; 29 | } 30 | -------------------------------------------------------------------------------- /examples/with-suspense/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from "react" 2 | import { useAsync, IfFulfilled, IfRejected } from "react-async" 3 | import ReactDOM from "react-dom" 4 | import DevTools from "react-async-devtools" 5 | import "./index.css" 6 | 7 | const loadUser = ({ userId }) => 8 | fetch(`https://reqres.in/api/users/${userId}`) 9 | .then(res => (res.ok ? res : Promise.reject(res))) 10 | .then(res => res.json()) 11 | .then(({ data }) => data) 12 | 13 | const UserPlaceholder = () => ( 14 |
15 |
16 |
══════
17 |
18 | ) 19 | 20 | const UserDetails = ({ data }) => ( 21 |
22 | 23 |
24 | {data.first_name} {data.last_name} 25 |
26 |
27 | ) 28 | 29 | const User = ({ userId }) => { 30 | const state = useAsync({ 31 | suspense: true, 32 | promiseFn: loadUser, 33 | debugLabel: `User ${userId}`, 34 | userId, 35 | }) 36 | return ( 37 | <> 38 | {data => } 39 | {error =>

{error.message}

}
40 | 41 | ) 42 | } 43 | 44 | export const App = () => ( 45 | <> 46 | 47 | 50 | 51 | 52 | 53 | } 54 | > 55 | 56 | 57 | 58 | 59 | ) 60 | 61 | if (process.env.NODE_ENV !== "test") ReactDOM.render(, document.getElementById("root")) 62 | -------------------------------------------------------------------------------- /examples/with-suspense/src/index.test.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import { App } from "./" 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div") 7 | ReactDOM.render(, div) 8 | ReactDOM.unmountComponentAtNode(div) 9 | }) 10 | -------------------------------------------------------------------------------- /examples/with-typescript/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/with-typescript/README.md: -------------------------------------------------------------------------------- 1 | # Using React Async with TypeScript 2 | 3 | This demonstrates how React Async integrates with TypeScript. 4 | 5 | 6 | live demo 7 | 8 | -------------------------------------------------------------------------------- /examples/with-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-typescript-example", 3 | "version": "10.0.0", 4 | "private": true, 5 | "homepage": "https://react-async.async-library.now.sh/examples/with-typescript", 6 | "scripts": { 7 | "bootstrap": "yarn install", 8 | "postinstall": "relative-deps", 9 | "prestart": "relative-deps", 10 | "prebuild": "relative-deps", 11 | "pretest": "relative-deps", 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "now-build": "SKIP_PREFLIGHT_CHECK=true react-scripts build" 16 | }, 17 | "dependencies": { 18 | "@types/node": "12.12.7", 19 | "@types/react": "16.9.11", 20 | "@types/react-dom": "16.9.4", 21 | "react": "16.11.0", 22 | "react-async": "^10.0.0", 23 | "react-async-devtools": "^10.0.0", 24 | "react-dom": "16.11.0", 25 | "react-scripts": "3.4.1", 26 | "typescript": "3.7.2" 27 | }, 28 | "devDependencies": { 29 | "@types/jest": "24.0.22", 30 | "relative-deps": "0.2.0" 31 | }, 32 | "relativeDependencies": { 33 | "react-async": "../../packages/react-async/pkg", 34 | "react-async-devtools": "../../packages/react-async-devtools/pkg" 35 | }, 36 | "eslintConfig": { 37 | "extends": "react-app" 38 | }, 39 | "browserslist": [ 40 | ">0.2%", 41 | "not dead", 42 | "not ie <= 11", 43 | "not op_mini all" 44 | ], 45 | "engines": { 46 | "node": ">=8" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/with-typescript/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/async-library/react-async/b55964d266b66354be9d10ee9d64a41f8c10415e/examples/with-typescript/public/favicon.ico -------------------------------------------------------------------------------- /examples/with-typescript/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/with-typescript/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/with-typescript/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | } 9 | 10 | .App-header { 11 | background-color: #282c34; 12 | min-height: 100vh; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | font-size: calc(10px + 2vmin); 18 | color: white; 19 | } 20 | 21 | .App-link { 22 | color: #61dafb; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { 27 | transform: rotate(0deg); 28 | } 29 | to { 30 | transform: rotate(360deg); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/with-typescript/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import App from "./App" 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div") 7 | ReactDOM.render(, div) 8 | ReactDOM.unmountComponentAtNode(div) 9 | }) 10 | -------------------------------------------------------------------------------- /examples/with-typescript/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import Async, { 3 | createInstance, 4 | useAsync, 5 | IfPending, 6 | IfRejected, 7 | IfFulfilled, 8 | PromiseFn, 9 | } from "react-async" 10 | import DevTools from "react-async-devtools" 11 | import "./App.css" 12 | import { FetchHookExample } from "./FetchHookExample" 13 | 14 | const loadFirstName: PromiseFn = ({ userId }) => 15 | fetch(`https://reqres.in/api/users/${userId}`) 16 | .then(res => (res.ok ? Promise.resolve(res) : Promise.reject(res))) 17 | .then(res => res.json()) 18 | .then(({ data }) => data.first_name) 19 | 20 | const CustomAsync = createInstance({ promiseFn: loadFirstName }) 21 | 22 | const UseAsync = () => { 23 | const state = useAsync({ promiseFn: loadFirstName, userId: 1 }) 24 | return ( 25 | <> 26 | Loading... 27 | {error => `Something went wrong: ${error.message}`} 28 | 29 | {data => ( 30 |
31 | Loaded some data: 32 |
{JSON.stringify(data, null, 2)}
33 |
34 | )} 35 |
36 | 37 | ) 38 | } 39 | 40 | class App extends Component { 41 | render() { 42 | return ( 43 |
44 | 45 |
46 | Promise.resolve("foo")}>{({ data }) => <>{data}} 47 | Promise.resolve("bar")}> 48 | {data => <>{data}} 49 | 50 | 51 | {data => <>{data}} 52 | 53 | 54 | 55 |
56 |
57 | ) 58 | } 59 | } 60 | 61 | export default App 62 | -------------------------------------------------------------------------------- /examples/with-typescript/src/FetchHookExample.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { useFetch } from "react-async" 3 | 4 | export function FetchHookExample() { 5 | const result = useFetch<{ token: string }>("https://reqres.in/api/login", { 6 | method: "POST", 7 | headers: { 8 | "Content-Type": "application/json", 9 | Accept: "application/json", 10 | }, 11 | }) 12 | const { run } = result 13 | 14 | return ( 15 | <> 16 |

with fetch hook:

17 | 18 | 31 | 43 |
44 | Status: 45 |
46 | {result.isInitial && "initial"} 47 | {result.isLoading && "loading"} 48 | {result.isRejected && "rejected"} 49 | {result.isResolved && `token: ${result.data.token}`} 50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /examples/with-typescript/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", 5 | "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 12 | } 13 | -------------------------------------------------------------------------------- /examples/with-typescript/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import "./index.css" 4 | import App from "./App" 5 | 6 | ReactDOM.render(, document.getElementById("root")) 7 | -------------------------------------------------------------------------------- /examples/with-typescript/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/with-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2015", "dom"], 4 | "target": "es5", 5 | "allowJs": true, 6 | "skipLibCheck": false, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "preserve" 17 | }, 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | for (let pkg of [ 3 | "@testing-library/jest-dom", 4 | "@testing-library/react", 5 | "jest", 6 | "react", 7 | "react-dom", 8 | ]) { 9 | console.log( 10 | `Using \u001b[36;1m${pkg}@${require(`./node_modules/${pkg}/package.json`).version}\u001b[0m` 11 | ) 12 | } 13 | 14 | module.exports = { 15 | rootDir: "./", 16 | collectCoverage: true, 17 | coverageDirectory: "/coverage", 18 | verbose: true, 19 | bail: true, 20 | transform: { 21 | "^.+\\.[tj]sx?$": "babel-jest", 22 | }, 23 | projects: ["/packages/*"], 24 | setupFiles: ["/jest.setup.js"], 25 | testPathIgnorePatterns: ["/node_modules/", "/pkg/"], 26 | } 27 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | /** 4 | * This is just a little hack to silence a warning that we'll get until react fixes this 5 | * @see https://github.com/facebook/react/pull/14853 6 | */ 7 | 8 | const originalError = console.error 9 | 10 | beforeAll(() => { 11 | console.error = (...args) => { 12 | if (/Warning.*not wrapped in act/.test(args[0])) { 13 | return 14 | } 15 | originalError.call(console, ...args) 16 | } 17 | }) 18 | 19 | afterAll(() => { 20 | console.error = originalError 21 | }) 22 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "yarn", 3 | "packages": [ 4 | "examples/*", 5 | "packages/*" 6 | ], 7 | "publish": { 8 | "ignoreChanges": [ 9 | "examples/**" 10 | ] 11 | }, 12 | "useWorkspaces": true, 13 | "version": "10.0.0" 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-async/root", 3 | "private": true, 4 | "author": "Gert Hengeveld ", 5 | "license": "ISC", 6 | "homepage": "https://async-library.com", 7 | "browserslist": "last 2 Firefox versions", 8 | "repository": "git+https://github.com/async-library/react-async.git", 9 | "workspaces": [ 10 | "examples/*", 11 | "packages/*" 12 | ], 13 | "scripts": { 14 | "bootstrap": "yarn build:packages && yarn workspaces run bootstrap", 15 | "clean": "lerna clean", 16 | "start": "run-p start:*", 17 | "start:examples": "vercel dev", 18 | "start:storybook": "start-storybook -p 6006", 19 | "lint": "eslint packages/*/src/*.{js,ts,tsx}", 20 | "test": "jest packages/*/src/*.spec.js", 21 | "test:watch": "yarn test -- --watch", 22 | "test:devtools": "jest react-async-devtools/src", 23 | "test:components": "jest src/Async.spec.js --collectCoverageFrom=src/Async.js", 24 | "test:backwards": "yarn add -D -W react@16.3.1 react-dom@16.3.1 && yarn resolutions:fix-react && yarn test:components", 25 | "test:forwards": "yarn add -D -W react@next react-dom@next && yarn resolutions:fix-react && yarn test", 26 | "test:latest": "yarn add -D -W react@latest react-dom@latest && yarn resolutions:fix-react && yarn test", 27 | "test:compat": "yarn test:backwards && yarn test:forwards && yarn test:latest", 28 | "test:examples": "CI=1 lerna run --scope '*-example' test -- --passWithNoTests --watchAll=false", 29 | "test:chromatic": "chromatic -t iiua39bmt0j -b build:storybook --exit-zero-on-changes", 30 | "resolutions:fix-react": "jq '.resolutions.react = .devDependencies.react|.resolutions.\"react-dom\"=.devDependencies.react' package.json > package.json.new && mv package.json.new package.json && yarn install", 31 | "ci": "yarn lint && yarn test:compat && yarn test:examples", 32 | "build:packages": "lerna run --scope 'react-async*' build", 33 | "build:examples": "lerna run --scope '*-example' build", 34 | "build:storybook": "build-storybook -o storybook", 35 | "deploy:examples": "vercel --prod --scope async-library --token $NOW_API_TOKEN", 36 | "bump": "lerna version -m 'Bump' --no-git-tag-version --no-push", 37 | "postbump": "yarn build:packages" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "7.9.6", 41 | "@babel/plugin-proposal-class-properties": "7.8.3", 42 | "@babel/plugin-proposal-object-rest-spread": "7.9.6", 43 | "@babel/plugin-transform-runtime": "7.9.6", 44 | "@babel/preset-env": "7.9.6", 45 | "@babel/preset-react": "7.9.4", 46 | "@babel/preset-typescript": "7.9.0", 47 | "@pika/pack": "0.5.0", 48 | "@pika/plugin-build-node": "0.9.2", 49 | "@pika/plugin-build-types": "0.9.2", 50 | "@pika/plugin-build-umd": "0.9.2", 51 | "@pika/plugin-build-web": "0.9.2", 52 | "@pika/plugin-bundle-types": "0.9.2", 53 | "@pika/plugin-standard-pkg": "0.9.2", 54 | "@pika/plugin-ts-standard-pkg": "0.9.2", 55 | "@storybook/react": "5.3.18", 56 | "@testing-library/jest-dom": "4.2.4", 57 | "@testing-library/react": "9.5.0", 58 | "@typescript-eslint/eslint-plugin": "2.33.0", 59 | "@typescript-eslint/parser": "2.33.0", 60 | "babel-eslint": "10.1.0", 61 | "babel-jest": "24.9.0", 62 | "babel-loader": "8.1.0", 63 | "chromatic": "5.2.0", 64 | "copyfiles": "2.2.0", 65 | "eslint": "6.8.0", 66 | "eslint-config-prettier": "6.11.0", 67 | "eslint-plugin-jest": "23.11.0", 68 | "eslint-plugin-prettier": "3.1.3", 69 | "eslint-plugin-promise": "4.2.1", 70 | "eslint-plugin-react": "7.19.0", 71 | "eslint-plugin-react-hooks": "2.5.1", 72 | "jest": "24.9.0", 73 | "lerna": "3.20.2", 74 | "node-jq": "1.11.1", 75 | "vercel": "20.1.2", 76 | "npm-run-all": "4.1.5", 77 | "prettier": "1.19.1", 78 | "prop-types": "15.7.2", 79 | "react": "17.0.2", 80 | "react-async": "10.0.0-alpha.0", 81 | "react-dom": "17.0.2", 82 | "typescript": "3.8.3" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/react-async-devtools/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("../../babel.config.js") 2 | -------------------------------------------------------------------------------- /packages/react-async-devtools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-async-devtools", 3 | "version": "10.0.0", 4 | "description": "DevTools for React Async", 5 | "keywords": [ 6 | "react", 7 | "async", 8 | "devtools" 9 | ], 10 | "author": "Gert Hengeveld ", 11 | "license": "ISC", 12 | "homepage": "https://react-async.com/", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/async-library/react-async.git", 16 | "directory": "packages/react-async-devtools" 17 | }, 18 | "main": "src", 19 | "scripts": { 20 | "bootstrap": "yarn install", 21 | "build": "pika build", 22 | "postbuild": "copyfiles -f ../../LICENSE ../../README.md pkg", 23 | "publish": "npm publish pkg" 24 | }, 25 | "peerDependencies": { 26 | "react": ">=16.3.1", 27 | "react-async": ">=7.0.1" 28 | }, 29 | "optionalDependencies": { 30 | "prop-types": ">=15.5.7" 31 | }, 32 | "@pika/pack": { 33 | "pipeline": [ 34 | [ 35 | "@pika/plugin-standard-pkg", 36 | { 37 | "exclude": [ 38 | "specs.js", 39 | "*.spec.js" 40 | ] 41 | } 42 | ], 43 | [ 44 | "@pika/plugin-build-node" 45 | ], 46 | [ 47 | "@pika/plugin-build-web" 48 | ], 49 | [ 50 | "@pika/plugin-build-types" 51 | ] 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/react-async-devtools/src/components.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import React from "react" 3 | 4 | export const Root = props => ( 5 |
24 | ) 25 | 26 | export const Range = props => ( 27 | 38 | ) 39 | 40 | export const Checkbox = props => ( 41 | 51 | ) 52 | 53 | export const Label = props => ( 54 |