├── .babelrc ├── .editorconfig ├── .github └── workflows │ ├── auto-publish-beta-to-npm.yml │ ├── auto-publish-release-to-npm.yml │ ├── ci.yml │ ├── create-beta-version.yml │ └── create-release-version.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── .vscode └── launch.json ├── LICENSE ├── config ├── test │ ├── jest.js │ └── setup.js └── webpack │ ├── helpers.js │ ├── webpack.common.js │ ├── webpack.dev.js │ └── webpack.prod.js ├── examples ├── 00-example-basic │ ├── .babelrc │ ├── package.json │ ├── src │ │ ├── api │ │ │ ├── fetch.js │ │ │ ├── postAPI.js │ │ │ └── userAPI.js │ │ ├── app.css │ │ ├── app.js │ │ ├── common │ │ │ └── components │ │ │ │ ├── spinner │ │ │ │ ├── index.js │ │ │ │ ├── spinner.css │ │ │ │ └── spinner.js │ │ │ │ └── table │ │ │ │ ├── index.js │ │ │ │ ├── table.css │ │ │ │ └── table.js │ │ ├── components │ │ │ ├── index.js │ │ │ ├── loadButton │ │ │ │ ├── index.js │ │ │ │ ├── loadButton.css │ │ │ │ └── loadButton.js │ │ │ ├── postTable │ │ │ │ ├── header.js │ │ │ │ ├── index.js │ │ │ │ ├── row.js │ │ │ │ └── table.js │ │ │ └── userTable │ │ │ │ ├── header.js │ │ │ │ ├── index.js │ │ │ │ ├── row.js │ │ │ │ └── table.js │ │ ├── index.html │ │ └── index.jsx │ └── webpack.config.js ├── 01-example-areas │ ├── .babelrc │ ├── package.json │ ├── src │ │ ├── api │ │ │ ├── fetch.js │ │ │ ├── postAPI.js │ │ │ └── userAPI.js │ │ ├── app.css │ │ ├── app.js │ │ ├── common │ │ │ ├── components │ │ │ │ ├── spinner │ │ │ │ │ ├── index.js │ │ │ │ │ ├── spinner.css │ │ │ │ │ └── spinner.js │ │ │ │ └── table │ │ │ │ │ ├── index.js │ │ │ │ │ ├── table.css │ │ │ │ │ └── table.js │ │ │ └── constants │ │ │ │ └── areas.js │ │ ├── components │ │ │ ├── index.js │ │ │ ├── loadButton │ │ │ │ ├── index.js │ │ │ │ ├── loadButton.css │ │ │ │ └── loadButton.js │ │ │ ├── postTable │ │ │ │ ├── header.js │ │ │ │ ├── index.js │ │ │ │ ├── row.js │ │ │ │ └── table.js │ │ │ └── userTable │ │ │ │ ├── header.js │ │ │ │ ├── index.js │ │ │ │ ├── row.js │ │ │ │ └── table.js │ │ ├── index.html │ │ └── index.jsx │ └── webpack.config.js ├── 02-example-delay │ ├── .babelrc │ ├── package.json │ ├── src │ │ ├── api │ │ │ ├── fetch.js │ │ │ ├── postAPI.js │ │ │ └── userAPI.js │ │ ├── app.css │ │ ├── app.js │ │ ├── common │ │ │ └── components │ │ │ │ ├── spinner │ │ │ │ ├── index.js │ │ │ │ ├── spinner.css │ │ │ │ └── spinner.js │ │ │ │ └── table │ │ │ │ ├── index.js │ │ │ │ ├── table.css │ │ │ │ └── table.js │ │ ├── components │ │ │ ├── index.js │ │ │ ├── loadButton │ │ │ │ ├── index.js │ │ │ │ ├── loadButton.css │ │ │ │ └── loadButton.js │ │ │ ├── postTable │ │ │ │ ├── header.js │ │ │ │ ├── index.js │ │ │ │ ├── row.js │ │ │ │ └── table.js │ │ │ └── userTable │ │ │ │ ├── header.js │ │ │ │ ├── index.js │ │ │ │ ├── row.js │ │ │ │ └── table.js │ │ ├── index.html │ │ └── index.jsx │ └── webpack.config.js ├── 03-example-hoc │ ├── .babelrc │ ├── package.json │ ├── src │ │ ├── api │ │ │ ├── fetch.js │ │ │ ├── postAPI.js │ │ │ └── userAPI.js │ │ ├── app.css │ │ ├── app.js │ │ ├── common │ │ │ └── components │ │ │ │ ├── spinner │ │ │ │ ├── index.js │ │ │ │ ├── spinner.css │ │ │ │ └── spinner.js │ │ │ │ └── table │ │ │ │ ├── index.js │ │ │ │ ├── table.css │ │ │ │ └── table.js │ │ ├── components │ │ │ ├── index.js │ │ │ ├── loadButton │ │ │ │ ├── index.js │ │ │ │ ├── loadButton.css │ │ │ │ └── loadButton.js │ │ │ ├── postTable │ │ │ │ ├── header.js │ │ │ │ ├── index.js │ │ │ │ ├── row.js │ │ │ │ └── table.js │ │ │ └── userTable │ │ │ │ ├── header.js │ │ │ │ ├── index.js │ │ │ │ ├── row.js │ │ │ │ └── table.js │ │ ├── index.html │ │ └── index.jsx │ └── webpack.config.js ├── 04-initial-load │ ├── .babelrc │ ├── package.json │ ├── src │ │ ├── api │ │ │ ├── fetch.js │ │ │ ├── postAPI.js │ │ │ └── userAPI.js │ │ ├── app.css │ │ ├── app.js │ │ ├── common │ │ │ └── components │ │ │ │ ├── spinner │ │ │ │ ├── index.js │ │ │ │ ├── spinner.css │ │ │ │ └── spinner.js │ │ │ │ └── table │ │ │ │ ├── index.js │ │ │ │ ├── table.css │ │ │ │ └── table.js │ │ ├── components │ │ │ ├── index.js │ │ │ ├── loadButton │ │ │ │ ├── index.js │ │ │ │ ├── loadButton.css │ │ │ │ └── loadButton.js │ │ │ ├── postTable │ │ │ │ ├── header.js │ │ │ │ ├── index.js │ │ │ │ ├── row.js │ │ │ │ └── table.js │ │ │ └── userTable │ │ │ │ ├── header.js │ │ │ │ ├── index.js │ │ │ │ ├── row.js │ │ │ │ └── table.js │ │ ├── index.html │ │ └── index.jsx │ └── webpack.config.js ├── 05-typescript │ ├── .babelrc │ ├── Readme.md │ ├── package.json │ ├── src │ │ ├── api │ │ │ ├── fetch.ts │ │ │ ├── postAPI.ts │ │ │ └── userAPI.ts │ │ ├── app.css │ │ ├── app.tsx │ │ ├── common │ │ │ ├── spinner │ │ │ │ ├── index.ts │ │ │ │ ├── spinner.css │ │ │ │ └── spinner.tsx │ │ │ └── table │ │ │ │ ├── index.ts │ │ │ │ ├── table.css │ │ │ │ └── table.tsx │ │ ├── components │ │ │ ├── index.ts │ │ │ ├── loadButton │ │ │ │ ├── index.ts │ │ │ │ ├── loadButton.css │ │ │ │ └── loadButton.tsx │ │ │ ├── postTable │ │ │ │ ├── header.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── row.tsx │ │ │ │ └── table.tsx │ │ │ └── userTable │ │ │ │ ├── header.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── row.tsx │ │ │ │ └── table.tsx │ │ ├── hello.tsx │ │ ├── index.html │ │ └── index.tsx │ ├── tsconfig.json │ └── webpack.config.js ├── 06-suspense-like │ ├── .babelrc │ ├── package.json │ ├── src │ │ ├── api.ts │ │ ├── app.tsx │ │ ├── index.html │ │ ├── index.tsx │ │ └── styles.ts │ ├── tsconfig.json │ ├── tslint.json │ └── webpack.config.js └── 07-suspense-custom │ ├── .babelrc │ ├── package.json │ ├── src │ ├── api.ts │ ├── app.tsx │ ├── index.html │ ├── index.tsx │ └── styles.ts │ ├── tsconfig.json │ ├── tslint.json │ └── webpack.config.js ├── package-lock.json ├── package.json ├── readme.md ├── readme_es.md ├── resources └── 00-shopping-cart-sample.png └── src ├── __snapshots__ └── trackerHoc.test.js.snap ├── constants.js ├── index.d.ts ├── index.js ├── setupConfig.js ├── setupConfig.test.js ├── tinyEmmiter.js ├── trackPromise.js ├── trackPromise.test.js ├── trackerHoc.js ├── trackerHoc.test.js ├── trackerHook.js └── trackerHook.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | // Compatibility Profile. 4 | // ES5 output and CommonJS module format. 5 | "es5_cjs": { 6 | "presets": ["@babel/preset-env", "@babel/preset-react"] 7 | }, 8 | // Future Profile. 9 | // ES6 output with no module transformation (ES Modules syntax). 10 | "es": { 11 | "presets": [ 12 | [ 13 | "@babel/preset-env", 14 | { 15 | "modules": false, 16 | "targets": { 17 | "node": "6.5" 18 | } 19 | } 20 | ], 21 | "@babel/preset-react" 22 | ] 23 | }, 24 | // Bundled Profile. 25 | // ES5 output and UMD module format. 26 | "umd": { 27 | "presets": [ 28 | [ 29 | "@babel/preset-env", 30 | { 31 | "modules": false 32 | } 33 | ], 34 | "@babel/preset-react" 35 | ] 36 | }, 37 | // Jest Profile. 38 | // To be used by jest tests. 39 | "test": { 40 | "presets": ["@babel/preset-env", "@babel/preset-react"] 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # All files 4 | [*] 5 | end_of_line = crlf 6 | insert_final_newline = true 7 | indent_size = 2 8 | indent_style = space 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.github/workflows/auto-publish-beta-to-npm.yml: -------------------------------------------------------------------------------- 1 | name: Auto publish Beta Version to NPM 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*-beta*" 7 | jobs: 8 | publish-package: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout Repository 12 | uses: actions/checkout@v3 13 | - name: Add NPM registry 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: "16.x" 17 | registry-url: "https://registry.npmjs.org" 18 | - name: Install 19 | run: npm ci 20 | - name: Build 21 | run: npm run build 22 | - name: Publish Beta 23 | env: 24 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTOMATION_TOKEN }} 25 | run: npm publish ./build --tag beta --access public 26 | -------------------------------------------------------------------------------- /.github/workflows/auto-publish-release-to-npm.yml: -------------------------------------------------------------------------------- 1 | name: Auto publish Release Version to NPM 2 | 3 | on: 4 | push: 5 | tags-ignore: 6 | - "v*-beta*" 7 | jobs: 8 | publish-package: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout Repository 12 | uses: actions/checkout@v3 13 | - name: Add NPM registry 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: "16.x" 17 | registry-url: "https://registry.npmjs.org" 18 | - name: Install 19 | run: npm ci 20 | - name: Build 21 | run: npm run build 22 | - name: Publish Release 23 | env: 24 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTOMATION_TOKEN }} 25 | run: npm publish ./build --access public 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuos Integration 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout Repository 10 | uses: actions/checkout@v3 11 | - name: Install 12 | run: npm ci 13 | - name: Tests 14 | run: npm test 15 | -------------------------------------------------------------------------------- /.github/workflows/create-beta-version.yml: -------------------------------------------------------------------------------- 1 | name: Create Beta Version 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | type: choice 8 | options: 9 | - premajor 10 | - preminor 11 | - prepatch 12 | - prerelease 13 | required: true 14 | default: prerelease 15 | description: "The beta version to publish" 16 | 17 | jobs: 18 | create-pre-release: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout Repository 23 | uses: actions/checkout@v3 24 | with: 25 | token: ${{secrets.PAT}} 26 | - name: Config git 27 | run: | 28 | mkdir -p ~/.ssh/ 29 | echo "${{secrets.SSH_PRIVATE_KEY}}" > ~/.ssh/id_rsa 30 | sudo chmod 600 ~/.ssh/id_rsa 31 | git config --global user.email "cd-user@lemoncode.net" 32 | git config --global user.name "cd-user" 33 | - name: Create beta version tag 34 | run: | 35 | npm version ${{github.event.inputs.version}} --preid=beta 36 | git push 37 | git push --tags 38 | echo "TAG_NAME=$(git describe --tags)" >> $GITHUB_ENV 39 | - name: Create beta pre-release 40 | env: 41 | GH_TOKEN: ${{ secrets.PAT }} 42 | run: gh release create ${{env.TAG_NAME}} --generate-notes --prerelease 43 | -------------------------------------------------------------------------------- /.github/workflows/create-release-version.yml: -------------------------------------------------------------------------------- 1 | name: Create Release Version 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | type: choice 8 | options: 9 | - major 10 | - minor 11 | - patch 12 | required: true 13 | default: patch 14 | description: "The package version to publish" 15 | 16 | jobs: 17 | create-release: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout Repository 21 | uses: actions/checkout@v3 22 | with: 23 | token: ${{secrets.PAT}} 24 | - name: Config git 25 | run: | 26 | mkdir -p ~/.ssh/ 27 | echo "${{secrets.SSH_PRIVATE_KEY}}" > ~/.ssh/id_rsa 28 | sudo chmod 600 ~/.ssh/id_rsa 29 | git config --global user.email "cd-user@lemoncode.net" 30 | git config --global user.name "cd-user" 31 | - name: Create version tag 32 | run: | 33 | npm version ${{github.event.inputs.version}} 34 | git push 35 | git push --tags 36 | echo "TAG_NAME=$(git describe --tags)" >> $GITHUB_ENV 37 | - name: Create Release 38 | env: 39 | GH_TOKEN: ${{ secrets.PAT }} 40 | run: gh release create ${{env.TAG_NAME}} --generate-notes 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Common aux folders 2 | .awcache/ 3 | .vscode/ 4 | .idea/ 5 | .cache/ 6 | 7 | # Dependencies & Build 8 | node_modules/ 9 | build/ 10 | lib/ 11 | dist/ 12 | es/ 13 | 14 | # Aux files 15 | *.cer 16 | *.log 17 | */src/**/*.orig 18 | */src/**/*.js.map 19 | 20 | # Win 21 | desktop.ini 22 | 23 | # MacOs 24 | .DS_Store 25 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "endOfLine": "lf" 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "type": "node", 7 | "request": "launch", 8 | "name": "Jest single run", 9 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 10 | "args": [ 11 | "--verbose", 12 | "-i", 13 | "--no-cache" 14 | ], 15 | "console": "integratedTerminal", 16 | "internalConsoleOptions": "neverOpen" 17 | }, 18 | { 19 | "type": "node", 20 | "request": "launch", 21 | "name": "Jest watch run", 22 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 23 | "args": [ 24 | "--verbose", 25 | "-i", 26 | "--no-cache", 27 | "--watchAll" 28 | ], 29 | "console": "integratedTerminal", 30 | "internalConsoleOptions": "neverOpen" 31 | }, 32 | { 33 | "type": "node", 34 | "request": "launch", 35 | "name": "Jest current file", 36 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 37 | "args": [ 38 | "${fileBasename}", 39 | "--verbose", 40 | "-i", 41 | "--no-cache", 42 | "--watchAll" 43 | ], 44 | "console": "integratedTerminal", 45 | "internalConsoleOptions": "neverOpen" 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Braulio Diez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/test/jest.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: '../../', 3 | verbose: true, 4 | restoreMocks: true, 5 | testEnvironment: 'jsdom', 6 | moduleDirectories: ['/src', 'node_modules'], 7 | setupFilesAfterEnv: ['/config/test/setup.js'], 8 | }; 9 | -------------------------------------------------------------------------------- /config/test/setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import '@testing-library/jest-dom/extend-expect'; 3 | -------------------------------------------------------------------------------- /config/webpack/helpers.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // String helpers 4 | const capitalizeString = s => s.charAt(0).toUpperCase() + s.slice(1); 5 | const camelCaseString = dashedName => dashedName.split("-").map( 6 | (s, i) => i > 0 ? capitalizeString(s) : s 7 | ).join(""); 8 | 9 | // Name helpers 10 | const packageName = process.env.npm_package_name; 11 | const packageNameCamelCase = camelCaseString(packageName); 12 | const version = JSON.stringify(process.env.npm_package_version).replace(/"/g, ''); 13 | const getBundleFileName = min => `${packageName}-${version}${min ? ".min" : ''}.js`; 14 | 15 | // Path helpers 16 | const rootPath = path.resolve(__dirname, "../.."); 17 | const resolveFromRootPath = (...args) => path.join(rootPath, ...args); 18 | 19 | // Export constants 20 | exports.srcPath = resolveFromRootPath("src"); 21 | exports.buildPath = resolveFromRootPath("build",); 22 | exports.distPath = resolveFromRootPath("build", "dist"); 23 | exports.version = version; 24 | exports.packageNameCamelCase = packageNameCamelCase; 25 | exports.getBundleFileName = getBundleFileName; 26 | -------------------------------------------------------------------------------- /config/webpack/webpack.common.js: -------------------------------------------------------------------------------- 1 | const helpers = require('./helpers'); 2 | 3 | module.exports = (env, argv) => { 4 | const minimizeBundle = Boolean(argv['optimize-minimize']); 5 | 6 | return { 7 | entry: ['./src/index.js'], 8 | output: { 9 | path: helpers.distPath, 10 | filename: helpers.getBundleFileName(minimizeBundle), 11 | library: helpers.packageNameCamelCase, 12 | libraryTarget: 'umd', 13 | }, 14 | externals: { 15 | react: 'react', 16 | }, 17 | optimization: { 18 | minimize: minimizeBundle, 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.js$/, 24 | exclude: /node_modules/, 25 | loader: 'babel-loader', 26 | }, 27 | ], 28 | }, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /config/webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const {merge} = require('webpack-merge'); 2 | const commonConfig = require('./webpack.common.js'); 3 | 4 | module.exports = (env, argv) => merge(commonConfig(env, argv), { 5 | mode: 'development', 6 | devtool: 'eval-source-map', 7 | }); 8 | -------------------------------------------------------------------------------- /config/webpack/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const {merge} = require('webpack-merge'); 2 | const commonConfig = require('./webpack.common.js'); 3 | const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; 4 | const CompressionPlugin = require('compression-webpack-plugin'); 5 | 6 | module.exports = (env, argv) => merge(commonConfig(env, argv), { 7 | mode: 'production', 8 | plugins: [ 9 | new BundleAnalyzerPlugin({ 10 | analyzerMode: "static", 11 | openAnalyzer: false, 12 | reportFilename: "report/report.html", 13 | }), 14 | new CompressionPlugin(), 15 | ], 16 | }); 17 | -------------------------------------------------------------------------------- /examples/00-example-basic/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "useBuiltIns": "entry" 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /examples/00-example-basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example0", 3 | "version": "1.0.0", 4 | "description": "In this sample we are going to setup a web project that can be easily managed\r by webpack.", 5 | "main": "src/index.jsx", 6 | "scripts": { 7 | "start": "webpack-dev-server --mode development --open", 8 | "build": "rimraf dist && webpack --mode development", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babel/cli": "^7.2.3", 15 | "@babel/core": "^7.4.0", 16 | "@babel/preset-env": "^7.4.0", 17 | "@babel/preset-react": "^7.0.0", 18 | "babel-loader": "^8.0.5", 19 | "css-loader": "^2.1.1", 20 | "html-webpack-plugin": "^3.2.0", 21 | "mini-css-extract-plugin": "^0.5.0", 22 | "rimraf": "^2.6.3", 23 | "style-loader": "^0.23.1", 24 | "webpack": "^4.29.6", 25 | "webpack-cli": "^3.3.0", 26 | "webpack-dev-server": "^3.2.1" 27 | }, 28 | "dependencies": { 29 | "react": "^16.8.4", 30 | "react-dom": "^16.8.4", 31 | "react-loader-spinner": "^2.3.0", 32 | "react-promise-tracker": "^2.1.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/00-example-basic/src/api/fetch.js: -------------------------------------------------------------------------------- 1 | export const fetchWithDelay = (url) => { 2 | const promise = new Promise((resolve, reject) => { 3 | setTimeout(() => { 4 | resolve(fetch(url, { 5 | method: 'GET', 6 | }) 7 | .then((response) => response.json())); 8 | }, 3000) 9 | }); 10 | 11 | return promise; 12 | } -------------------------------------------------------------------------------- /examples/00-example-basic/src/api/postAPI.js: -------------------------------------------------------------------------------- 1 | import { fetchWithDelay } from './fetch'; 2 | const url = 'https://jsonplaceholder.typicode.com/posts'; 3 | 4 | const fetchPosts = () => fetchWithDelay(url) 5 | .then((posts) => posts.slice(0, 10)); 6 | 7 | export const postAPI = { 8 | fetchPosts, 9 | }; -------------------------------------------------------------------------------- /examples/00-example-basic/src/api/userAPI.js: -------------------------------------------------------------------------------- 1 | import { fetchWithDelay } from './fetch'; 2 | const url = 'https://jsonplaceholder.typicode.com/users'; 3 | 4 | const fetchUsers = () => fetchWithDelay(url); 5 | 6 | export const userAPI = { 7 | fetchUsers, 8 | }; -------------------------------------------------------------------------------- /examples/00-example-basic/src/app.css: -------------------------------------------------------------------------------- 1 | .tables { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: nowrap; 5 | } 6 | 7 | .tables > div { 8 | flex-basis: 50%; 9 | margin-left: 1rem; 10 | margin-right: 1rem; 11 | } -------------------------------------------------------------------------------- /examples/00-example-basic/src/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { trackPromise } from 'react-promise-tracker'; 3 | import { userAPI } from './api/userAPI'; 4 | import { postAPI } from './api/postAPI'; 5 | import { UserTable, PostTable, LoadButton } from './components'; 6 | import './app.css'; 7 | 8 | export class App extends Component { 9 | constructor() { 10 | super(); 11 | 12 | this.state = { 13 | users: [], 14 | posts: [], 15 | }; 16 | 17 | this.onLoadTables = this.onLoadTables.bind(this); 18 | } 19 | 20 | onLoadTables() { 21 | this.setState({ 22 | users: [], 23 | posts: [], 24 | }); 25 | 26 | trackPromise( 27 | userAPI.fetchUsers() 28 | .then((users) => { 29 | this.setState({ 30 | users, 31 | }) 32 | }) 33 | ); 34 | 35 | trackPromise( 36 | postAPI.fetchPosts() 37 | .then((posts) => { 38 | this.setState({ 39 | posts, 40 | }) 41 | }) 42 | ); 43 | } 44 | 45 | render() { 46 | return ( 47 |
48 | 52 |
53 | 54 | 55 |
56 |
57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/00-example-basic/src/common/components/spinner/index.js: -------------------------------------------------------------------------------- 1 | export * from './spinner'; -------------------------------------------------------------------------------- /examples/00-example-basic/src/common/components/spinner/spinner.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .spinner > div { 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | } -------------------------------------------------------------------------------- /examples/00-example-basic/src/common/components/spinner/spinner.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { usePromiseTracker } from "react-promise-tracker"; 3 | import Loader from "react-loader-spinner"; 4 | import "./spinner.css"; 5 | 6 | export const Spinner = (props) => { 7 | const { promiseInProgress } = usePromiseTracker(); 8 | 9 | return ( 10 | promiseInProgress && ( 11 |
12 | 13 |
14 | ) 15 | ); 16 | }; 17 | 18 | -------------------------------------------------------------------------------- /examples/00-example-basic/src/common/components/table/index.js: -------------------------------------------------------------------------------- 1 | export * from './table' -------------------------------------------------------------------------------- /examples/00-example-basic/src/common/components/table/table.css: -------------------------------------------------------------------------------- 1 | .title { 2 | color: #000; 3 | } 4 | 5 | .table { 6 | width: 100%; 7 | max-width: 100%; 8 | margin-bottom: 1rem; 9 | background-color: transparent; 10 | border-collapse: collapse; 11 | } 12 | 13 | .table thead th { 14 | vertical-align: bottom; 15 | border-bottom: 2px solid #248f4f; 16 | } 17 | 18 | .table td, .table th { 19 | text-align: inherit; 20 | padding: .75rem; 21 | vertical-align: top; 22 | border-top: 1px solid #248f4f; 23 | } 24 | 25 | .table tbody tr:nth-of-type(odd) { 26 | background-color: rgba(43, 173, 96, .05); 27 | } -------------------------------------------------------------------------------- /examples/00-example-basic/src/common/components/table/table.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './table.css'; 3 | 4 | export const Table = (props) => ( 5 |
6 |

{props.title}

7 | 8 | 9 | {props.headerRender()} 10 | 11 | 12 | {props.items.map(props.rowRender)} 13 | 14 |
15 |
16 | ); 17 | -------------------------------------------------------------------------------- /examples/00-example-basic/src/components/index.js: -------------------------------------------------------------------------------- 1 | export * from './userTable'; 2 | export * from './postTable'; 3 | export * from './loadButton'; -------------------------------------------------------------------------------- /examples/00-example-basic/src/components/loadButton/index.js: -------------------------------------------------------------------------------- 1 | export * from './loadButton'; -------------------------------------------------------------------------------- /examples/00-example-basic/src/components/loadButton/loadButton.css: -------------------------------------------------------------------------------- 1 | .load-button { 2 | cursor: pointer; 3 | margin: 1rem; 4 | color: #fff; 5 | text-shadow: 1px 1px 0 #888; 6 | background-color: #2BAD60; 7 | border-color: #14522d; 8 | display: inline-block; 9 | font-weight: 400; 10 | text-align: center; 11 | white-space: nowrap; 12 | vertical-align: middle; 13 | border: 1px solid transparent; 14 | padding: .375rem .75rem; 15 | font-size: 1rem; 16 | line-height: 1.5; 17 | border-radius: .25rem; 18 | outline: none; 19 | } 20 | 21 | 22 | 23 | .load-button:hover { 24 | background-color: #248f4f; 25 | border-color: #1e7b43; 26 | } -------------------------------------------------------------------------------- /examples/00-example-basic/src/components/loadButton/loadButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './loadButton.css'; 3 | 4 | export const LoadButton = (props) => ( 5 | 11 | ); -------------------------------------------------------------------------------- /examples/00-example-basic/src/components/postTable/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Header = (props) => ( 4 | 5 | Id 6 | Title 7 | Body 8 | 9 | ); -------------------------------------------------------------------------------- /examples/00-example-basic/src/components/postTable/index.js: -------------------------------------------------------------------------------- 1 | export * from './table'; -------------------------------------------------------------------------------- /examples/00-example-basic/src/components/postTable/row.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Row = (post) => ( 4 | 5 | 6 | {post.id} 7 | 8 | 9 | {post.title} 10 | 11 | 12 | {post.body} 13 | 14 | 15 | ); -------------------------------------------------------------------------------- /examples/00-example-basic/src/components/postTable/table.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Table } from '../../common/components/table'; 3 | import { Header } from './header'; 4 | import { Row } from './row'; 5 | 6 | export const PostTable = (props) => ( 7 | 13 | ); 14 | -------------------------------------------------------------------------------- /examples/00-example-basic/src/components/userTable/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Header = (props) => ( 4 | 5 | 6 | 7 | 8 | 9 | ); -------------------------------------------------------------------------------- /examples/00-example-basic/src/components/userTable/index.js: -------------------------------------------------------------------------------- 1 | export * from './table'; -------------------------------------------------------------------------------- /examples/00-example-basic/src/components/userTable/row.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Row = (user) => ( 4 | 5 | 8 | 11 | 14 | 15 | ); -------------------------------------------------------------------------------- /examples/00-example-basic/src/components/userTable/table.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Table } from '../../common/components/table'; 3 | import { Header } from './header'; 4 | import { Row } from './row'; 5 | 6 | export const UserTable = (props) => ( 7 |
IdNameEmail
6 | {user.id} 7 | 9 | {user.name} 10 | 12 | {user.email} 13 |
13 | ); -------------------------------------------------------------------------------- /examples/00-example-basic/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Promise tracker sample app 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/00-example-basic/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { App } from './app'; 4 | import { Spinner } from './common/components/spinner'; 5 | 6 | render( 7 |
8 | 9 | 10 |
, 11 | document.getElementById('root')); 12 | -------------------------------------------------------------------------------- /examples/00-example-basic/webpack.config.js: -------------------------------------------------------------------------------- 1 | var HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | var webpack = require("webpack"); 3 | var MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | 5 | var path = require("path"); 6 | var basePath = __dirname; 7 | 8 | module.exports = { 9 | context: path.join(basePath, "src"), 10 | resolve: { 11 | extensions: [".js", ".jsx"], 12 | alias: { 13 | react: path.resolve('./node_modules/react'), 14 | 'react-dom': path.resolve('./node_modules/react-dom') 15 | }, 16 | }, 17 | entry: { 18 | app: "./index.jsx", 19 | vendor: ["react", "react-dom"], 20 | }, 21 | output: { 22 | filename: "[name].[chunkhash].js" 23 | }, 24 | optimization: { 25 | splitChunks: { 26 | cacheGroups: { 27 | vendor: { 28 | chunks: "initial", 29 | name: "vendor", 30 | test: "vendor", 31 | enforce: true 32 | } 33 | } 34 | } 35 | }, 36 | devtool: 'inline-source-map', 37 | module: { 38 | rules: [ 39 | { 40 | test: /\.jsx?$/, 41 | exclude: /node_modules/, 42 | loader: "babel-loader" 43 | }, 44 | { 45 | test: /\.css$/, 46 | use: [MiniCssExtractPlugin.loader, "css-loader"] 47 | } 48 | ] 49 | }, 50 | plugins: [ 51 | //Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin 52 | new HtmlWebpackPlugin({ 53 | filename: "index.html", //Name of file in ./dist/ 54 | template: "index.html", //Name of template in ./src 55 | hash: true 56 | }), 57 | new MiniCssExtractPlugin({ 58 | filename: "[name].css", 59 | chunkFilename: "[id].css" 60 | }) 61 | ] 62 | }; 63 | -------------------------------------------------------------------------------- /examples/01-example-areas/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "useBuiltIns": "entry" 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /examples/01-example-areas/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example1", 3 | "version": "1.0.0", 4 | "description": "In this sample we are going to setup a web project that can be easily managed\r by webpack.", 5 | "main": "src/index.jsx", 6 | "scripts": { 7 | "start": "webpack-dev-server --mode development --open", 8 | "build": "rimraf dist && webpack --mode development", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babel/cli": "^7.2.3", 15 | "@babel/core": "^7.4.0", 16 | "@babel/preset-env": "^7.4.0", 17 | "@babel/preset-react": "^7.0.0", 18 | "babel-loader": "^8.0.5", 19 | "css-loader": "^2.1.1", 20 | "html-webpack-plugin": "^3.2.0", 21 | "mini-css-extract-plugin": "^0.5.0", 22 | "rimraf": "^2.6.3", 23 | "style-loader": "^0.23.1", 24 | "webpack": "^4.29.6", 25 | "webpack-cli": "^3.3.0", 26 | "webpack-dev-server": "^3.2.1" 27 | }, 28 | "dependencies": { 29 | "react": "^16.8.4", 30 | "react-dom": "^16.8.4", 31 | "react-loader-spinner": "^2.3.0", 32 | "react-promise-tracker": "^2.1.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/01-example-areas/src/api/fetch.js: -------------------------------------------------------------------------------- 1 | export const fetchWithDelay = (url) => { 2 | const promise = new Promise((resolve, reject) => { 3 | setTimeout(() => { 4 | resolve(fetch(url, { 5 | method: 'GET', 6 | }) 7 | .then((response) => response.json())); 8 | }, 3000) 9 | }); 10 | 11 | return promise; 12 | } -------------------------------------------------------------------------------- /examples/01-example-areas/src/api/postAPI.js: -------------------------------------------------------------------------------- 1 | import { fetchWithDelay } from './fetch'; 2 | const url = 'https://jsonplaceholder.typicode.com/posts'; 3 | 4 | const fetchPosts = () => fetchWithDelay(url) 5 | .then((posts) => posts.slice(0, 10)); 6 | 7 | export const postAPI = { 8 | fetchPosts, 9 | }; -------------------------------------------------------------------------------- /examples/01-example-areas/src/api/userAPI.js: -------------------------------------------------------------------------------- 1 | import { fetchWithDelay } from './fetch'; 2 | const url = 'https://jsonplaceholder.typicode.com/users'; 3 | 4 | const fetchUsers = () => fetchWithDelay(url); 5 | 6 | export const userAPI = { 7 | fetchUsers, 8 | }; -------------------------------------------------------------------------------- /examples/01-example-areas/src/app.css: -------------------------------------------------------------------------------- 1 | .tables, .load-buttons { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: nowrap; 5 | } 6 | 7 | .tables > div, .load-buttons > button { 8 | flex-basis: 50%; 9 | margin-left: 1rem; 10 | margin-right: 1rem; 11 | } 12 | -------------------------------------------------------------------------------- /examples/01-example-areas/src/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { trackPromise } from 'react-promise-tracker'; 3 | import { userAPI } from './api/userAPI'; 4 | import { postAPI } from './api/postAPI'; 5 | import { UserTable, PostTable, LoadButton } from './components'; 6 | import { areas } from './common/constants/areas'; 7 | import './app.css'; 8 | 9 | export class App extends Component { 10 | constructor() { 11 | super(); 12 | 13 | this.state = { 14 | users: [], 15 | posts: [], 16 | }; 17 | 18 | this.onLoadUsers = this.onLoadUsers.bind(this); 19 | this.onLoadPosts = this.onLoadPosts.bind(this); 20 | } 21 | 22 | onLoadUsers() { 23 | this.setState({ 24 | users: [], 25 | }); 26 | 27 | trackPromise( 28 | userAPI.fetchUsers() 29 | .then((users) => { 30 | this.setState({ 31 | users, 32 | }) 33 | }), 34 | areas.user, 35 | ); 36 | } 37 | 38 | onLoadPosts() { 39 | this.setState({ 40 | posts: [], 41 | }); 42 | 43 | trackPromise( 44 | postAPI.fetchPosts() 45 | .then((posts) => { 46 | this.setState({ 47 | posts, 48 | }) 49 | }), 50 | areas.post, 51 | ); 52 | } 53 | 54 | render() { 55 | return ( 56 |
57 |
58 | 62 | 66 |
67 |
68 | 69 | 70 |
71 |
72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /examples/01-example-areas/src/common/components/spinner/index.js: -------------------------------------------------------------------------------- 1 | export * from './spinner'; -------------------------------------------------------------------------------- /examples/01-example-areas/src/common/components/spinner/spinner.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .spinner > div { 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | -------------------------------------------------------------------------------- /examples/01-example-areas/src/common/components/spinner/spinner.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { usePromiseTracker } from "react-promise-tracker"; 3 | import Loader from "react-loader-spinner"; 4 | import "./spinner.css"; 5 | 6 | export const Spinner = (props) => { 7 | const { promiseInProgress } = usePromiseTracker({area: props.area, delay: 0}); 8 | 9 | return ( 10 | promiseInProgress && ( 11 |
12 | 13 |
14 | ) 15 | ); 16 | }; 17 | 18 | -------------------------------------------------------------------------------- /examples/01-example-areas/src/common/components/table/index.js: -------------------------------------------------------------------------------- 1 | export * from './table' -------------------------------------------------------------------------------- /examples/01-example-areas/src/common/components/table/table.css: -------------------------------------------------------------------------------- 1 | .title { 2 | color: #000; 3 | } 4 | 5 | .table { 6 | width: 100%; 7 | max-width: 100%; 8 | margin-bottom: 1rem; 9 | background-color: transparent; 10 | border-collapse: collapse; 11 | } 12 | 13 | .table thead th { 14 | vertical-align: bottom; 15 | border-bottom: 2px solid #248f4f; 16 | } 17 | 18 | .table td, .table th { 19 | text-align: inherit; 20 | padding: .75rem; 21 | vertical-align: top; 22 | border-top: 1px solid #248f4f; 23 | } 24 | 25 | .table tbody tr:nth-of-type(odd) { 26 | background-color: rgba(43, 173, 96, .05); 27 | } 28 | -------------------------------------------------------------------------------- /examples/01-example-areas/src/common/components/table/table.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './table.css'; 3 | 4 | export const Table = (props) => ( 5 |
6 |

{props.title}

7 |
8 | 9 | {props.headerRender()} 10 | 11 | 12 | {props.items.map(props.rowRender)} 13 | 14 |
15 | 16 | ); 17 | -------------------------------------------------------------------------------- /examples/01-example-areas/src/common/constants/areas.js: -------------------------------------------------------------------------------- 1 | export const areas = { 2 | user: 'user-area', 3 | post: 'post-area', 4 | }; 5 | -------------------------------------------------------------------------------- /examples/01-example-areas/src/components/index.js: -------------------------------------------------------------------------------- 1 | export * from './userTable'; 2 | export * from './postTable'; 3 | export * from './loadButton'; -------------------------------------------------------------------------------- /examples/01-example-areas/src/components/loadButton/index.js: -------------------------------------------------------------------------------- 1 | export * from './loadButton'; -------------------------------------------------------------------------------- /examples/01-example-areas/src/components/loadButton/loadButton.css: -------------------------------------------------------------------------------- 1 | .load-button { 2 | cursor: pointer; 3 | margin: 1rem; 4 | color: #fff; 5 | text-shadow: 1px 1px 0 #888; 6 | background-color: #2BAD60; 7 | border-color: #14522d; 8 | display: inline-block; 9 | font-weight: 400; 10 | text-align: center; 11 | white-space: nowrap; 12 | vertical-align: middle; 13 | border: 1px solid transparent; 14 | padding: .375rem .75rem; 15 | font-size: 1rem; 16 | line-height: 1.5; 17 | border-radius: .25rem; 18 | outline: none; 19 | } 20 | 21 | 22 | 23 | .load-button:hover { 24 | background-color: #248f4f; 25 | border-color: #1e7b43; 26 | } -------------------------------------------------------------------------------- /examples/01-example-areas/src/components/loadButton/loadButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './loadButton.css'; 3 | 4 | export const LoadButton = (props) => ( 5 | 11 | ); -------------------------------------------------------------------------------- /examples/01-example-areas/src/components/postTable/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Header = (props) => ( 4 | 5 | Id 6 | Title 7 | Body 8 | 9 | ); -------------------------------------------------------------------------------- /examples/01-example-areas/src/components/postTable/index.js: -------------------------------------------------------------------------------- 1 | export * from './table'; -------------------------------------------------------------------------------- /examples/01-example-areas/src/components/postTable/row.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Row = (post) => ( 4 | 5 | 6 | {post.id} 7 | 8 | 9 | {post.title} 10 | 11 | 12 | {post.body} 13 | 14 | 15 | ); -------------------------------------------------------------------------------- /examples/01-example-areas/src/components/postTable/table.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Spinner } from '../../common/components/spinner'; 3 | import { areas } from '../../common/constants/areas'; 4 | import { Table } from '../../common/components/table'; 5 | import { Header } from './header'; 6 | import { Row } from './row'; 7 | 8 | export const PostTable = (props) => ( 9 |
10 | 16 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /examples/01-example-areas/src/components/userTable/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Header = (props) => ( 4 | 5 | 6 | 7 | 8 | 9 | ); -------------------------------------------------------------------------------- /examples/01-example-areas/src/components/userTable/index.js: -------------------------------------------------------------------------------- 1 | export * from './table'; -------------------------------------------------------------------------------- /examples/01-example-areas/src/components/userTable/row.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Row = (user) => ( 4 | 5 | 8 | 11 | 14 | 15 | ); -------------------------------------------------------------------------------- /examples/01-example-areas/src/components/userTable/table.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Spinner } from '../../common/components/spinner'; 3 | import { areas } from '../../common/constants/areas'; 4 | import { Table } from '../../common/components/table'; 5 | import { Header } from './header'; 6 | import { Row } from './row'; 7 | 8 | export const UserTable = (props) => ( 9 |
10 |
IdNameEmail
6 | {user.id} 7 | 9 | {user.name} 10 | 12 | {user.email} 13 |
16 | 19 | 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /examples/01-example-areas/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Promise tracker sample app 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/01-example-areas/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { App } from './app'; 4 | import { Spinner } from './common/components/spinner'; 5 | 6 | render( 7 |
8 | 9 | 10 |
, 11 | document.getElementById('root')); 12 | -------------------------------------------------------------------------------- /examples/01-example-areas/webpack.config.js: -------------------------------------------------------------------------------- 1 | var HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | var webpack = require("webpack"); 3 | var MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | 5 | var path = require("path"); 6 | var basePath = __dirname; 7 | 8 | module.exports = { 9 | context: path.join(basePath, "src"), 10 | resolve: { 11 | extensions: [".js", ".jsx"], 12 | alias: { 13 | react: path.resolve('./node_modules/react'), 14 | 'react-dom': path.resolve('./node_modules/react-dom') 15 | }, 16 | }, 17 | entry: { 18 | app: "./index.jsx", 19 | vendor: ["react", "react-dom"], 20 | }, 21 | output: { 22 | filename: "[name].[chunkhash].js" 23 | }, 24 | optimization: { 25 | splitChunks: { 26 | cacheGroups: { 27 | vendor: { 28 | chunks: "initial", 29 | name: "vendor", 30 | test: "vendor", 31 | enforce: true 32 | } 33 | } 34 | } 35 | }, 36 | devtool: 'inline-source-map', 37 | module: { 38 | rules: [ 39 | { 40 | test: /\.jsx?$/, 41 | exclude: /node_modules/, 42 | loader: "babel-loader" 43 | }, 44 | { 45 | test: /\.css$/, 46 | use: [MiniCssExtractPlugin.loader, "css-loader"] 47 | } 48 | ] 49 | }, 50 | plugins: [ 51 | //Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin 52 | new HtmlWebpackPlugin({ 53 | filename: "index.html", //Name of file in ./dist/ 54 | template: "index.html", //Name of template in ./src 55 | hash: true 56 | }), 57 | new MiniCssExtractPlugin({ 58 | filename: "[name].css", 59 | chunkFilename: "[id].css" 60 | }) 61 | ] 62 | }; 63 | -------------------------------------------------------------------------------- /examples/02-example-delay/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "useBuiltIns": "entry" 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /examples/02-example-delay/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example2", 3 | "version": "1.0.0", 4 | "description": "In this sample we are going to setup a web project that can be easily managed\r by webpack.", 5 | "main": "src/index.jsx", 6 | "scripts": { 7 | "start": "webpack-dev-server --mode development --open", 8 | "build": "rimraf dist && webpack --mode development", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babel/cli": "^7.2.3", 15 | "@babel/core": "^7.4.0", 16 | "@babel/preset-env": "^7.4.0", 17 | "@babel/preset-react": "^7.0.0", 18 | "babel-loader": "^8.0.5", 19 | "css-loader": "^2.1.1", 20 | "html-webpack-plugin": "^3.2.0", 21 | "mini-css-extract-plugin": "^0.5.0", 22 | "rimraf": "^2.6.3", 23 | "style-loader": "^0.23.1", 24 | "webpack": "^4.29.6", 25 | "webpack-cli": "^3.3.0", 26 | "webpack-dev-server": "^3.2.1" 27 | }, 28 | "dependencies": { 29 | "react": "^16.8.4", 30 | "react-dom": "^16.8.4", 31 | "react-loader-spinner": "^2.3.0", 32 | "react-promise-tracker": "^2.1.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/02-example-delay/src/api/fetch.js: -------------------------------------------------------------------------------- 1 | export const fetchWithDelay = (url) => { 2 | const promise = new Promise((resolve, reject) => { 3 | setTimeout(() => { 4 | resolve(fetch(url, { 5 | method: 'GET', 6 | }) 7 | .then((response) => response.json())); 8 | }, 3000) 9 | }); 10 | 11 | return promise; 12 | } -------------------------------------------------------------------------------- /examples/02-example-delay/src/api/postAPI.js: -------------------------------------------------------------------------------- 1 | import { fetchWithDelay } from './fetch'; 2 | const url = 'https://jsonplaceholder.typicode.com/posts'; 3 | 4 | const fetchPosts = () => fetchWithDelay(url) 5 | .then((posts) => posts.slice(0, 10)); 6 | 7 | export const postAPI = { 8 | fetchPosts, 9 | }; -------------------------------------------------------------------------------- /examples/02-example-delay/src/api/userAPI.js: -------------------------------------------------------------------------------- 1 | import { fetchWithDelay } from './fetch'; 2 | const url = 'https://jsonplaceholder.typicode.com/users'; 3 | 4 | const fetchUsers = () => fetchWithDelay(url); 5 | 6 | export const userAPI = { 7 | fetchUsers, 8 | }; -------------------------------------------------------------------------------- /examples/02-example-delay/src/app.css: -------------------------------------------------------------------------------- 1 | .tables { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: nowrap; 5 | } 6 | 7 | .tables > div { 8 | flex-basis: 50%; 9 | margin-left: 1rem; 10 | margin-right: 1rem; 11 | } -------------------------------------------------------------------------------- /examples/02-example-delay/src/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { trackPromise } from 'react-promise-tracker'; 3 | import { userAPI } from './api/userAPI'; 4 | import { postAPI } from './api/postAPI'; 5 | import { UserTable, PostTable, LoadButton } from './components'; 6 | import './app.css'; 7 | 8 | export class App extends Component { 9 | constructor() { 10 | super(); 11 | 12 | this.state = { 13 | users: [], 14 | posts: [], 15 | }; 16 | 17 | this.onLoadTables = this.onLoadTables.bind(this); 18 | } 19 | 20 | onLoadTables() { 21 | this.setState({ 22 | users: [], 23 | posts: [], 24 | }); 25 | 26 | trackPromise( 27 | userAPI.fetchUsers() 28 | .then((users) => { 29 | this.setState({ 30 | users, 31 | }) 32 | }) 33 | ); 34 | 35 | trackPromise( 36 | postAPI.fetchPosts() 37 | .then((posts) => { 38 | this.setState({ 39 | posts, 40 | }) 41 | }) 42 | ); 43 | } 44 | 45 | render() { 46 | return ( 47 |
48 | 52 |
53 | 54 | 55 |
56 |
57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/02-example-delay/src/common/components/spinner/index.js: -------------------------------------------------------------------------------- 1 | export * from './spinner'; -------------------------------------------------------------------------------- /examples/02-example-delay/src/common/components/spinner/spinner.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .spinner > div { 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | } -------------------------------------------------------------------------------- /examples/02-example-delay/src/common/components/spinner/spinner.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { usePromiseTracker } from "react-promise-tracker"; 3 | import Loader from "react-loader-spinner"; 4 | import "./spinner.css"; 5 | 6 | export const Spinner = (props) => { 7 | 8 | const { promiseInProgress } = usePromiseTracker({delay: 200}); 9 | 10 | return ( 11 | promiseInProgress && ( 12 |
13 | 14 |
15 | ) 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /examples/02-example-delay/src/common/components/table/index.js: -------------------------------------------------------------------------------- 1 | export * from './table' -------------------------------------------------------------------------------- /examples/02-example-delay/src/common/components/table/table.css: -------------------------------------------------------------------------------- 1 | .title { 2 | color: #000; 3 | } 4 | 5 | .table { 6 | width: 100%; 7 | max-width: 100%; 8 | margin-bottom: 1rem; 9 | background-color: transparent; 10 | border-collapse: collapse; 11 | } 12 | 13 | .table thead th { 14 | vertical-align: bottom; 15 | border-bottom: 2px solid #248f4f; 16 | } 17 | 18 | .table td, .table th { 19 | text-align: inherit; 20 | padding: .75rem; 21 | vertical-align: top; 22 | border-top: 1px solid #248f4f; 23 | } 24 | 25 | .table tbody tr:nth-of-type(odd) { 26 | background-color: rgba(43, 173, 96, .05); 27 | } -------------------------------------------------------------------------------- /examples/02-example-delay/src/common/components/table/table.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './table.css'; 3 | 4 | export const Table = (props) => ( 5 |
6 |

{props.title}

7 |
8 | 9 | {props.headerRender()} 10 | 11 | 12 | {props.items.map(props.rowRender)} 13 | 14 |
15 |
16 | ); 17 | -------------------------------------------------------------------------------- /examples/02-example-delay/src/components/index.js: -------------------------------------------------------------------------------- 1 | export * from './userTable'; 2 | export * from './postTable'; 3 | export * from './loadButton'; -------------------------------------------------------------------------------- /examples/02-example-delay/src/components/loadButton/index.js: -------------------------------------------------------------------------------- 1 | export * from './loadButton'; -------------------------------------------------------------------------------- /examples/02-example-delay/src/components/loadButton/loadButton.css: -------------------------------------------------------------------------------- 1 | .load-button { 2 | cursor: pointer; 3 | margin: 1rem; 4 | color: #fff; 5 | text-shadow: 1px 1px 0 #888; 6 | background-color: #2BAD60; 7 | border-color: #14522d; 8 | display: inline-block; 9 | font-weight: 400; 10 | text-align: center; 11 | white-space: nowrap; 12 | vertical-align: middle; 13 | border: 1px solid transparent; 14 | padding: .375rem .75rem; 15 | font-size: 1rem; 16 | line-height: 1.5; 17 | border-radius: .25rem; 18 | outline: none; 19 | } 20 | 21 | 22 | 23 | .load-button:hover { 24 | background-color: #248f4f; 25 | border-color: #1e7b43; 26 | } -------------------------------------------------------------------------------- /examples/02-example-delay/src/components/loadButton/loadButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './loadButton.css'; 3 | 4 | export const LoadButton = (props) => ( 5 | 11 | ); -------------------------------------------------------------------------------- /examples/02-example-delay/src/components/postTable/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Header = (props) => ( 4 | 5 | Id 6 | Title 7 | Body 8 | 9 | ); -------------------------------------------------------------------------------- /examples/02-example-delay/src/components/postTable/index.js: -------------------------------------------------------------------------------- 1 | export * from './table'; -------------------------------------------------------------------------------- /examples/02-example-delay/src/components/postTable/row.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Row = (post) => ( 4 | 5 | 6 | {post.id} 7 | 8 | 9 | {post.title} 10 | 11 | 12 | {post.body} 13 | 14 | 15 | ); -------------------------------------------------------------------------------- /examples/02-example-delay/src/components/postTable/table.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Table } from '../../common/components/table'; 3 | import { Header } from './header'; 4 | import { Row } from './row'; 5 | 6 | export const PostTable = (props) => ( 7 | 13 | ); 14 | -------------------------------------------------------------------------------- /examples/02-example-delay/src/components/userTable/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Header = (props) => ( 4 | 5 | 6 | 7 | 8 | 9 | ); -------------------------------------------------------------------------------- /examples/02-example-delay/src/components/userTable/index.js: -------------------------------------------------------------------------------- 1 | export * from './table'; -------------------------------------------------------------------------------- /examples/02-example-delay/src/components/userTable/row.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Row = (user) => ( 4 | 5 | 8 | 11 | 14 | 15 | ); -------------------------------------------------------------------------------- /examples/02-example-delay/src/components/userTable/table.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Table } from '../../common/components/table'; 3 | import { Header } from './header'; 4 | import { Row } from './row'; 5 | 6 | export const UserTable = (props) => ( 7 |
IdNameEmail
6 | {user.id} 7 | 9 | {user.name} 10 | 12 | {user.email} 13 |
13 | ); -------------------------------------------------------------------------------- /examples/02-example-delay/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Promise tracker sample app 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/02-example-delay/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { App } from './app'; 4 | import { Spinner } from './common/components/spinner'; 5 | 6 | render( 7 |
8 | 9 | 10 |
, 11 | document.getElementById('root')); 12 | -------------------------------------------------------------------------------- /examples/02-example-delay/webpack.config.js: -------------------------------------------------------------------------------- 1 | var HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | var webpack = require("webpack"); 3 | var MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | 5 | var path = require("path"); 6 | var basePath = __dirname; 7 | 8 | module.exports = { 9 | context: path.join(basePath, "src"), 10 | resolve: { 11 | extensions: [".js", ".jsx"], 12 | alias: { 13 | react: path.resolve('./node_modules/react'), 14 | 'react-dom': path.resolve('./node_modules/react-dom') 15 | }, 16 | }, 17 | entry: { 18 | app: "./index.jsx", 19 | vendor: ["react", "react-dom"], 20 | }, 21 | output: { 22 | filename: "[name].[chunkhash].js" 23 | }, 24 | optimization: { 25 | splitChunks: { 26 | cacheGroups: { 27 | vendor: { 28 | chunks: "initial", 29 | name: "vendor", 30 | test: "vendor", 31 | enforce: true 32 | } 33 | } 34 | } 35 | }, 36 | devtool: 'inline-source-map', 37 | module: { 38 | rules: [ 39 | { 40 | test: /\.jsx?$/, 41 | exclude: /node_modules/, 42 | loader: "babel-loader" 43 | }, 44 | { 45 | test: /\.css$/, 46 | use: [MiniCssExtractPlugin.loader, "css-loader"] 47 | } 48 | ] 49 | }, 50 | plugins: [ 51 | //Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin 52 | new HtmlWebpackPlugin({ 53 | filename: "index.html", //Name of file in ./dist/ 54 | template: "index.html", //Name of template in ./src 55 | hash: true 56 | }), 57 | new MiniCssExtractPlugin({ 58 | filename: "[name].css", 59 | chunkFilename: "[id].css" 60 | }) 61 | ] 62 | }; 63 | -------------------------------------------------------------------------------- /examples/03-example-hoc/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "useBuiltIns": "entry" 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /examples/03-example-hoc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example3", 3 | "version": "1.0.0", 4 | "description": "In this sample we are going to setup a web project that can be easily managed\r by webpack.", 5 | "main": "src/index.jsx", 6 | "scripts": { 7 | "start": "webpack-dev-server --mode development --open", 8 | "build": "rimraf dist && webpack --mode development", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babel/cli": "^7.2.3", 15 | "@babel/core": "^7.4.0", 16 | "@babel/preset-env": "^7.4.0", 17 | "@babel/preset-react": "^7.0.0", 18 | "babel-loader": "^8.0.5", 19 | "css-loader": "^2.1.1", 20 | "html-webpack-plugin": "^3.2.0", 21 | "mini-css-extract-plugin": "^0.5.0", 22 | "rimraf": "^2.6.3", 23 | "style-loader": "^0.23.1", 24 | "webpack": "^4.29.6", 25 | "webpack-cli": "^3.3.0", 26 | "webpack-dev-server": "^3.2.1" 27 | }, 28 | "dependencies": { 29 | "react": "^16.8.4", 30 | "react-dom": "^16.8.4", 31 | "react-loader-spinner": "^2.3.0", 32 | "react-promise-tracker": "^2.1.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/03-example-hoc/src/api/fetch.js: -------------------------------------------------------------------------------- 1 | export const fetchWithDelay = async (url) => { 2 | 3 | try { 4 | const response = await new Promise((resolve, reject) => { 5 | setTimeout( 6 | () => 7 | resolve( 8 | fetch(url).then((data) => { 9 | return data; 10 | }) 11 | ), 12 | 3000 13 | ); 14 | }); 15 | const responseJSON = await response.json(); 16 | return responseJSON; 17 | } catch (err) { 18 | throw new Error(err); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/03-example-hoc/src/api/postAPI.js: -------------------------------------------------------------------------------- 1 | import { fetchWithDelay } from './fetch'; 2 | const url = 'https://jsonplaceholder.typicode.com/posts'; 3 | 4 | const fetchPosts = () => fetchWithDelay(url) 5 | .then((posts) => posts.slice(0, 10)); 6 | 7 | export const postAPI = { 8 | fetchPosts, 9 | }; -------------------------------------------------------------------------------- /examples/03-example-hoc/src/api/userAPI.js: -------------------------------------------------------------------------------- 1 | import { fetchWithDelay } from './fetch'; 2 | const url = 'https://jsonplaceholder.typicode.com/users'; 3 | 4 | const fetchUsers = () => fetchWithDelay(url); 5 | 6 | export const userAPI = { 7 | fetchUsers, 8 | }; -------------------------------------------------------------------------------- /examples/03-example-hoc/src/app.css: -------------------------------------------------------------------------------- 1 | .tables { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: nowrap; 5 | } 6 | 7 | .tables > div { 8 | flex-basis: 50%; 9 | margin-left: 1rem; 10 | margin-right: 1rem; 11 | } -------------------------------------------------------------------------------- /examples/03-example-hoc/src/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { trackPromise } from 'react-promise-tracker'; 3 | import { userAPI } from './api/userAPI'; 4 | import { postAPI } from './api/postAPI'; 5 | import { UserTable, PostTable, LoadButton } from './components'; 6 | import './app.css'; 7 | 8 | export class App extends Component { 9 | constructor() { 10 | super(); 11 | 12 | this.state = { 13 | users: [], 14 | posts: [], 15 | }; 16 | 17 | this.onLoadTables = this.onLoadTables.bind(this); 18 | } 19 | 20 | onLoadTables() { 21 | this.setState({ 22 | users: [], 23 | posts: [], 24 | }); 25 | 26 | trackPromise( 27 | userAPI.fetchUsers() 28 | .then((users) => { 29 | this.setState({ 30 | users, 31 | }) 32 | }) 33 | ); 34 | 35 | trackPromise( 36 | postAPI.fetchPosts() 37 | .then((posts) => { 38 | this.setState({ 39 | posts, 40 | }) 41 | }) 42 | ); 43 | } 44 | 45 | render() { 46 | return ( 47 |
48 | 52 |
53 | 54 | 55 |
56 |
57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/03-example-hoc/src/common/components/spinner/index.js: -------------------------------------------------------------------------------- 1 | export * from './spinner'; -------------------------------------------------------------------------------- /examples/03-example-hoc/src/common/components/spinner/spinner.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .spinner > div { 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | } -------------------------------------------------------------------------------- /examples/03-example-hoc/src/common/components/spinner/spinner.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { promiseTrackerHoc } from "react-promise-tracker"; 3 | import Loader from "react-loader-spinner"; 4 | import "./spinner.css"; 5 | 6 | const SpinnerInner = (props) => 7 | props.promiseInProgress && ( 8 |
9 | 10 |
11 | ) 12 | 13 | export const Spinner = promiseTrackerHoc(SpinnerInner); 14 | -------------------------------------------------------------------------------- /examples/03-example-hoc/src/common/components/table/index.js: -------------------------------------------------------------------------------- 1 | export * from './table' -------------------------------------------------------------------------------- /examples/03-example-hoc/src/common/components/table/table.css: -------------------------------------------------------------------------------- 1 | .title { 2 | color: #000; 3 | } 4 | 5 | .table { 6 | width: 100%; 7 | max-width: 100%; 8 | margin-bottom: 1rem; 9 | background-color: transparent; 10 | border-collapse: collapse; 11 | } 12 | 13 | .table thead th { 14 | vertical-align: bottom; 15 | border-bottom: 2px solid #248f4f; 16 | } 17 | 18 | .table td, .table th { 19 | text-align: inherit; 20 | padding: .75rem; 21 | vertical-align: top; 22 | border-top: 1px solid #248f4f; 23 | } 24 | 25 | .table tbody tr:nth-of-type(odd) { 26 | background-color: rgba(43, 173, 96, .05); 27 | } -------------------------------------------------------------------------------- /examples/03-example-hoc/src/common/components/table/table.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './table.css'; 3 | 4 | export const Table = (props) => ( 5 |
6 |

{props.title}

7 |
8 | 9 | {props.headerRender()} 10 | 11 | 12 | {props.items.map(props.rowRender)} 13 | 14 |
15 | 16 | ); 17 | -------------------------------------------------------------------------------- /examples/03-example-hoc/src/components/index.js: -------------------------------------------------------------------------------- 1 | export * from './userTable'; 2 | export * from './postTable'; 3 | export * from './loadButton'; -------------------------------------------------------------------------------- /examples/03-example-hoc/src/components/loadButton/index.js: -------------------------------------------------------------------------------- 1 | export * from './loadButton'; -------------------------------------------------------------------------------- /examples/03-example-hoc/src/components/loadButton/loadButton.css: -------------------------------------------------------------------------------- 1 | .load-button { 2 | cursor: pointer; 3 | margin: 1rem; 4 | color: #fff; 5 | text-shadow: 1px 1px 0 #888; 6 | background-color: #2BAD60; 7 | border-color: #14522d; 8 | display: inline-block; 9 | font-weight: 400; 10 | text-align: center; 11 | white-space: nowrap; 12 | vertical-align: middle; 13 | border: 1px solid transparent; 14 | padding: .375rem .75rem; 15 | font-size: 1rem; 16 | line-height: 1.5; 17 | border-radius: .25rem; 18 | outline: none; 19 | } 20 | 21 | 22 | 23 | .load-button:hover { 24 | background-color: #248f4f; 25 | border-color: #1e7b43; 26 | } -------------------------------------------------------------------------------- /examples/03-example-hoc/src/components/loadButton/loadButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './loadButton.css'; 3 | 4 | export const LoadButton = (props) => ( 5 | 11 | ); -------------------------------------------------------------------------------- /examples/03-example-hoc/src/components/postTable/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Header = (props) => ( 4 | 5 | Id 6 | Title 7 | Body 8 | 9 | ); -------------------------------------------------------------------------------- /examples/03-example-hoc/src/components/postTable/index.js: -------------------------------------------------------------------------------- 1 | export * from './table'; -------------------------------------------------------------------------------- /examples/03-example-hoc/src/components/postTable/row.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Row = (post) => ( 4 | 5 | 6 | {post.id} 7 | 8 | 9 | {post.title} 10 | 11 | 12 | {post.body} 13 | 14 | 15 | ); -------------------------------------------------------------------------------- /examples/03-example-hoc/src/components/postTable/table.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Table } from '../../common/components/table'; 3 | import { Header } from './header'; 4 | import { Row } from './row'; 5 | 6 | export const PostTable = (props) => ( 7 | 13 | ); 14 | -------------------------------------------------------------------------------- /examples/03-example-hoc/src/components/userTable/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Header = (props) => ( 4 | 5 | 6 | 7 | 8 | 9 | ); -------------------------------------------------------------------------------- /examples/03-example-hoc/src/components/userTable/index.js: -------------------------------------------------------------------------------- 1 | export * from './table'; -------------------------------------------------------------------------------- /examples/03-example-hoc/src/components/userTable/row.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Row = (user) => ( 4 | 5 | 8 | 11 | 14 | 15 | ); -------------------------------------------------------------------------------- /examples/03-example-hoc/src/components/userTable/table.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Table } from '../../common/components/table'; 3 | import { Header } from './header'; 4 | import { Row } from './row'; 5 | 6 | export const UserTable = (props) => ( 7 |
IdNameEmail
6 | {user.id} 7 | 9 | {user.name} 10 | 12 | {user.email} 13 |
13 | ); -------------------------------------------------------------------------------- /examples/03-example-hoc/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Promise tracker sample app 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/03-example-hoc/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { App } from './app'; 4 | import { Spinner } from './common/components/spinner'; 5 | 6 | render( 7 |
8 | 9 | 10 |
, 11 | document.getElementById('root')); 12 | -------------------------------------------------------------------------------- /examples/03-example-hoc/webpack.config.js: -------------------------------------------------------------------------------- 1 | var HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | var webpack = require("webpack"); 3 | var MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | 5 | var path = require("path"); 6 | var basePath = __dirname; 7 | 8 | module.exports = { 9 | context: path.join(basePath, "src"), 10 | resolve: { 11 | extensions: [".js", ".jsx"], 12 | alias: { 13 | react: path.resolve('./node_modules/react'), 14 | 'react-dom': path.resolve('./node_modules/react-dom') 15 | }, 16 | }, 17 | entry: { 18 | app: "./index.jsx", 19 | vendor: ["react", "react-dom"], 20 | }, 21 | output: { 22 | filename: "[name].[chunkhash].js" 23 | }, 24 | optimization: { 25 | splitChunks: { 26 | cacheGroups: { 27 | vendor: { 28 | chunks: "initial", 29 | name: "vendor", 30 | test: "vendor", 31 | enforce: true 32 | } 33 | } 34 | } 35 | }, 36 | devtool: 'inline-source-map', 37 | module: { 38 | rules: [ 39 | { 40 | test: /\.jsx?$/, 41 | exclude: /node_modules/, 42 | loader: "babel-loader" 43 | }, 44 | { 45 | test: /\.css$/, 46 | use: [MiniCssExtractPlugin.loader, "css-loader"] 47 | } 48 | ] 49 | }, 50 | plugins: [ 51 | //Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin 52 | new HtmlWebpackPlugin({ 53 | filename: "index.html", //Name of file in ./dist/ 54 | template: "index.html", //Name of template in ./src 55 | hash: true 56 | }), 57 | new MiniCssExtractPlugin({ 58 | filename: "[name].css", 59 | chunkFilename: "[id].css" 60 | }) 61 | ] 62 | }; 63 | -------------------------------------------------------------------------------- /examples/04-initial-load/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "useBuiltIns": "entry" 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /examples/04-initial-load/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example4", 3 | "version": "1.0.0", 4 | "description": "In this sample we are going to setup a web project that can be easily managed\r by webpack.", 5 | "main": "src/index.jsx", 6 | "scripts": { 7 | "start": "webpack-dev-server --mode development --open", 8 | "build": "rimraf dist && webpack --mode development", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babel/cli": "^7.2.3", 15 | "@babel/core": "^7.4.0", 16 | "@babel/preset-env": "^7.4.0", 17 | "@babel/preset-react": "^7.0.0", 18 | "babel-loader": "^8.0.5", 19 | "css-loader": "^2.1.1", 20 | "html-webpack-plugin": "^3.2.0", 21 | "mini-css-extract-plugin": "^0.5.0", 22 | "rimraf": "^2.6.3", 23 | "style-loader": "^0.23.1", 24 | "webpack": "^4.29.6", 25 | "webpack-cli": "^3.3.0", 26 | "webpack-dev-server": "^3.2.1" 27 | }, 28 | "dependencies": { 29 | "react": "^16.8.4", 30 | "react-dom": "^16.8.4", 31 | "react-loader-spinner": "^2.3.0", 32 | "react-promise-tracker": "^2.1.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/04-initial-load/src/api/fetch.js: -------------------------------------------------------------------------------- 1 | export const fetchWithDelay = (url) => { 2 | const promise = new Promise((resolve, reject) => { 3 | setTimeout(() => { 4 | resolve(fetch(url, { 5 | method: 'GET', 6 | }) 7 | .then((response) => response.json())); 8 | }, 3000) 9 | }); 10 | 11 | return promise; 12 | } -------------------------------------------------------------------------------- /examples/04-initial-load/src/api/postAPI.js: -------------------------------------------------------------------------------- 1 | import { fetchWithDelay } from './fetch'; 2 | const url = 'https://jsonplaceholder.typicode.com/posts'; 3 | 4 | const fetchPosts = () => fetchWithDelay(url) 5 | .then((posts) => posts.slice(0, 10)); 6 | 7 | export const postAPI = { 8 | fetchPosts, 9 | }; -------------------------------------------------------------------------------- /examples/04-initial-load/src/api/userAPI.js: -------------------------------------------------------------------------------- 1 | import { fetchWithDelay } from './fetch'; 2 | const url = 'https://jsonplaceholder.typicode.com/users'; 3 | 4 | const fetchUsers = () => fetchWithDelay(url); 5 | 6 | export const userAPI = { 7 | fetchUsers, 8 | }; -------------------------------------------------------------------------------- /examples/04-initial-load/src/app.css: -------------------------------------------------------------------------------- 1 | .tables { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: nowrap; 5 | } 6 | 7 | .tables > div { 8 | flex-basis: 50%; 9 | margin-left: 1rem; 10 | margin-right: 1rem; 11 | } -------------------------------------------------------------------------------- /examples/04-initial-load/src/app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { trackPromise } from 'react-promise-tracker'; 3 | import { userAPI } from './api/userAPI'; 4 | import { postAPI } from './api/postAPI'; 5 | import { UserTable, PostTable, LoadButton } from './components'; 6 | import './app.css'; 7 | 8 | export class App extends Component { 9 | constructor() { 10 | super(); 11 | 12 | this.state = { 13 | users: [], 14 | posts: [], 15 | }; 16 | 17 | } 18 | 19 | componentDidMount() { 20 | this.setState({ 21 | users: [], 22 | posts: [], 23 | }); 24 | 25 | trackPromise( 26 | userAPI.fetchUsers() 27 | .then((users) => { 28 | this.setState({ 29 | users, 30 | }) 31 | }) 32 | ); 33 | 34 | trackPromise( 35 | postAPI.fetchPosts() 36 | .then((posts) => { 37 | this.setState({ 38 | posts, 39 | }) 40 | }) 41 | ); 42 | } 43 | 44 | render() { 45 | return ( 46 |
47 |
48 | 49 | 50 |
51 |
52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/04-initial-load/src/common/components/spinner/index.js: -------------------------------------------------------------------------------- 1 | export * from './spinner'; -------------------------------------------------------------------------------- /examples/04-initial-load/src/common/components/spinner/spinner.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .spinner > div { 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | } -------------------------------------------------------------------------------- /examples/04-initial-load/src/common/components/spinner/spinner.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { usePromiseTracker } from "react-promise-tracker"; 3 | import Loader from "react-loader-spinner"; 4 | import "./spinner.css"; 5 | 6 | export const Spinner = (props) => { 7 | const { promiseInProgress } = usePromiseTracker(); 8 | 9 | return ( 10 | promiseInProgress && ( 11 |
12 | 13 |
14 | ) 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /examples/04-initial-load/src/common/components/table/index.js: -------------------------------------------------------------------------------- 1 | export * from './table' -------------------------------------------------------------------------------- /examples/04-initial-load/src/common/components/table/table.css: -------------------------------------------------------------------------------- 1 | .title { 2 | color: #000; 3 | } 4 | 5 | .table { 6 | width: 100%; 7 | max-width: 100%; 8 | margin-bottom: 1rem; 9 | background-color: transparent; 10 | border-collapse: collapse; 11 | } 12 | 13 | .table thead th { 14 | vertical-align: bottom; 15 | border-bottom: 2px solid #248f4f; 16 | } 17 | 18 | .table td, .table th { 19 | text-align: inherit; 20 | padding: .75rem; 21 | vertical-align: top; 22 | border-top: 1px solid #248f4f; 23 | } 24 | 25 | .table tbody tr:nth-of-type(odd) { 26 | background-color: rgba(43, 173, 96, .05); 27 | } -------------------------------------------------------------------------------- /examples/04-initial-load/src/common/components/table/table.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './table.css'; 3 | 4 | export const Table = (props) => ( 5 |
6 |

{props.title}

7 |
8 | 9 | {props.headerRender()} 10 | 11 | 12 | {props.items.map(props.rowRender)} 13 | 14 |
15 | 16 | ); 17 | -------------------------------------------------------------------------------- /examples/04-initial-load/src/components/index.js: -------------------------------------------------------------------------------- 1 | export * from './userTable'; 2 | export * from './postTable'; 3 | export * from './loadButton'; -------------------------------------------------------------------------------- /examples/04-initial-load/src/components/loadButton/index.js: -------------------------------------------------------------------------------- 1 | export * from './loadButton'; -------------------------------------------------------------------------------- /examples/04-initial-load/src/components/loadButton/loadButton.css: -------------------------------------------------------------------------------- 1 | .load-button { 2 | cursor: pointer; 3 | margin: 1rem; 4 | color: #fff; 5 | text-shadow: 1px 1px 0 #888; 6 | background-color: #2BAD60; 7 | border-color: #14522d; 8 | display: inline-block; 9 | font-weight: 400; 10 | text-align: center; 11 | white-space: nowrap; 12 | vertical-align: middle; 13 | border: 1px solid transparent; 14 | padding: .375rem .75rem; 15 | font-size: 1rem; 16 | line-height: 1.5; 17 | border-radius: .25rem; 18 | outline: none; 19 | } 20 | 21 | 22 | 23 | .load-button:hover { 24 | background-color: #248f4f; 25 | border-color: #1e7b43; 26 | } -------------------------------------------------------------------------------- /examples/04-initial-load/src/components/loadButton/loadButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './loadButton.css'; 3 | 4 | export const LoadButton = (props) => ( 5 | 11 | ); -------------------------------------------------------------------------------- /examples/04-initial-load/src/components/postTable/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Header = (props) => ( 4 | 5 | Id 6 | Title 7 | Body 8 | 9 | ); -------------------------------------------------------------------------------- /examples/04-initial-load/src/components/postTable/index.js: -------------------------------------------------------------------------------- 1 | export * from './table'; -------------------------------------------------------------------------------- /examples/04-initial-load/src/components/postTable/row.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Row = (post) => ( 4 | 5 | 6 | {post.id} 7 | 8 | 9 | {post.title} 10 | 11 | 12 | {post.body} 13 | 14 | 15 | ); -------------------------------------------------------------------------------- /examples/04-initial-load/src/components/postTable/table.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Table } from '../../common/components/table'; 3 | import { Header } from './header'; 4 | import { Row } from './row'; 5 | 6 | export const PostTable = (props) => ( 7 | 13 | ); 14 | -------------------------------------------------------------------------------- /examples/04-initial-load/src/components/userTable/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Header = (props) => ( 4 | 5 | 6 | 7 | 8 | 9 | ); -------------------------------------------------------------------------------- /examples/04-initial-load/src/components/userTable/index.js: -------------------------------------------------------------------------------- 1 | export * from './table'; -------------------------------------------------------------------------------- /examples/04-initial-load/src/components/userTable/row.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Row = (user) => ( 4 | 5 | 8 | 11 | 14 | 15 | ); -------------------------------------------------------------------------------- /examples/04-initial-load/src/components/userTable/table.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Table } from '../../common/components/table'; 3 | import { Header } from './header'; 4 | import { Row } from './row'; 5 | 6 | export const UserTable = (props) => ( 7 |
IdNameEmail
6 | {user.id} 7 | 9 | {user.name} 10 | 12 | {user.email} 13 |
13 | ); -------------------------------------------------------------------------------- /examples/04-initial-load/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Promise tracker sample app 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/04-initial-load/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { App } from './app'; 4 | import { Spinner } from './common/components/spinner'; 5 | 6 | render( 7 |
8 | 9 | 10 |
, 11 | document.getElementById('root')); 12 | -------------------------------------------------------------------------------- /examples/04-initial-load/webpack.config.js: -------------------------------------------------------------------------------- 1 | var HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | var webpack = require("webpack"); 3 | var MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | 5 | var path = require("path"); 6 | var basePath = __dirname; 7 | 8 | module.exports = { 9 | context: path.join(basePath, "src"), 10 | resolve: { 11 | extensions: [".js", ".jsx"], 12 | alias: { 13 | react: path.resolve('./node_modules/react'), 14 | 'react-dom': path.resolve('./node_modules/react-dom') 15 | }, 16 | }, 17 | entry: { 18 | app: "./index.jsx", 19 | vendor: ["react", "react-dom"], 20 | }, 21 | output: { 22 | filename: "[name].[chunkhash].js" 23 | }, 24 | optimization: { 25 | splitChunks: { 26 | cacheGroups: { 27 | vendor: { 28 | chunks: "initial", 29 | name: "vendor", 30 | test: "vendor", 31 | enforce: true 32 | } 33 | } 34 | } 35 | }, 36 | devtool: 'inline-source-map', 37 | module: { 38 | rules: [ 39 | { 40 | test: /\.jsx?$/, 41 | exclude: /node_modules/, 42 | loader: "babel-loader" 43 | }, 44 | { 45 | test: /\.css$/, 46 | use: [MiniCssExtractPlugin.loader, "css-loader"] 47 | } 48 | ] 49 | }, 50 | plugins: [ 51 | //Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin 52 | new HtmlWebpackPlugin({ 53 | filename: "index.html", //Name of file in ./dist/ 54 | template: "index.html", //Name of template in ./src 55 | hash: true 56 | }), 57 | new MiniCssExtractPlugin({ 58 | filename: "[name].css", 59 | chunkFilename: "[id].css" 60 | }) 61 | ] 62 | }; 63 | -------------------------------------------------------------------------------- /examples/05-typescript/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "useBuiltIns": "entry" 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /examples/05-typescript/Readme.md: -------------------------------------------------------------------------------- 1 | # 01 Hello React 2 | 3 | In this sample we will create our first react component and connect it with the DOM via react-dom. 4 | 5 | We will take a startup point sample _00 Boilerplate_. 6 | 7 | Summary steps: 8 | 9 | - Install react and react-dom libraries. 10 | - Install react and react-dom typescript definitions. 11 | - Update the index.html to create a placeholder for the react components. 12 | - Create a simple react component. 13 | - Wire up this component by using react-dom. 14 | 15 | ## Prerequisites 16 | 17 | Install [Node.js and npm](https://nodejs.org/en/) (v8.9.4 or higher) if they are not already installed on your computer. 18 | 19 | > Verify that you are running at least node v8.x.x and npm 5.x.x by running `node -v` and `npm -v` 20 | > in a terminal/console window. Older versions may produce errors. 21 | 22 | ## Steps to build it 23 | 24 | - Copy the content of the `00 Boilerplate` folder to an empty folder for the sample. 25 | 26 | - Install the npm packages described in the [./package.json](./package.json) and verify that it works: 27 | 28 | ```bash 29 | npm install 30 | ``` 31 | 32 | - Install `react` and `react-dom` libraries as project dependencies. 33 | 34 | ```bash 35 | npm install react react-dom --save 36 | ``` 37 | 38 | - Install also the typescript definitions for `react` and `react-dom` 39 | but as dev dependencies. 40 | 41 | ```bash 42 | npm install @types/react @types/react-dom --save-dev 43 | ``` 44 | 45 | - Update the [./src/index.html](./src/index.html) to create a placeholder for the react components. 46 | 47 | _[./src/index.html](./src/index.html)_ 48 | 49 | ```diff 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 |

Sample app

59 | +
60 |
61 | 62 | 63 | 64 | ``` 65 | 66 | - Create a simple react component (let's create it within a new file called `hello.tsx` in `src`folder). 67 | 68 | _[./src/hello.tsx](./src/hello.tsx)_ 69 | 70 | ```javascript 71 | import * as React from "react"; 72 | 73 | export const HelloComponent = () => { 74 | return

Hello component !

; 75 | }; 76 | ``` 77 | 78 | - Wire up this component by using `react-dom` under [./src/index.tsx](./src/index.tsx) (we have to rename 79 | this file extension from `ts` to `tsx` and replace the content). 80 | 81 | _[./src/index.tsx](./src/index.tsx)_ 82 | 83 | ```diff 84 | - document.write('Hello from index.ts!'); 85 | 86 | + import * as React from 'react'; 87 | + import * as ReactDOM from 'react-dom'; 88 | 89 | + import { HelloComponent } from './hello'; 90 | 91 | + ReactDOM.render( 92 | + , 93 | + document.getElementById('root') 94 | + ); 95 | ``` 96 | 97 | - Delete the file _main.ts_ we are not going to need it anymore. 98 | 99 | - Modify the [./webpack.config.js](./webpack.config.js) file and change the entry point from `./index.ts` 100 | to `./index.tsx`. 101 | 102 | _[./webpack.config.js](./webpack.config.js)_ 103 | 104 | ```diff 105 | ... 106 | 107 | module.exports = { 108 | context: path.join(basePath, 'src'), 109 | resolve: { 110 | extensions: ['.js', '.ts', '.tsx'] 111 | }, 112 | entry: { 113 | - app: './index.ts', 114 | + app: './index.tsx', 115 | vendorStyles: [ 116 | '../node_modules/bootstrap/dist/css/bootstrap.css', 117 | ], 118 | }, 119 | ``` 120 | 121 | - Execute the example: 122 | 123 | ```bash 124 | npm start 125 | ``` 126 | 127 | # About Basefactor + Lemoncode 128 | 129 | We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. 130 | 131 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 132 | 133 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 134 | 135 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 136 | -------------------------------------------------------------------------------- /examples/05-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example5", 3 | "version": "1.0.0", 4 | "description": "React Typescript examples", 5 | "main": "src/index.tsx", 6 | "scripts": { 7 | "start": "webpack-dev-server --mode development --inline --hot --open", 8 | "build": "webpack --mode development" 9 | }, 10 | "keywords": [ 11 | "react", 12 | "typescript", 13 | "hooks" 14 | ], 15 | "author": "Braulio Diez Botella", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@babel/cli": "^7.2.3", 19 | "@babel/core": "^7.2.2", 20 | "@babel/polyfill": "^7.2.5", 21 | "@babel/preset-env": "^7.3.1", 22 | "@babel/preset-react": "^7.0.0", 23 | "@types/react": "^16.8.3", 24 | "@types/react-dom": "^16.8.1", 25 | "awesome-typescript-loader": "^5.2.1", 26 | "babel-loader": "^8.0.5", 27 | "css-loader": "^2.1.0", 28 | "file-loader": "^3.0.1", 29 | "html-webpack-plugin": "^3.2.0", 30 | "mini-css-extract-plugin": "^0.5.0", 31 | "style-loader": "^0.23.1", 32 | "typescript": "^3.3.3", 33 | "url-loader": "^1.1.2", 34 | "webpack": "^4.29.3", 35 | "webpack-cli": "^3.2.3", 36 | "webpack-dev-server": "^3.1.14" 37 | }, 38 | "dependencies": { 39 | "react": "^16.8.5", 40 | "react-dom": "^16.8.5", 41 | "react-loader-spinner": "^2.3.0", 42 | "react-promise-tracker": "^2.1.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/05-typescript/src/api/fetch.ts: -------------------------------------------------------------------------------- 1 | export const fetchWithDelay = (url) => { 2 | const promise = new Promise((resolve, reject) => { 3 | setTimeout(() => { 4 | resolve(fetch(url, { 5 | method: 'GET', 6 | }) 7 | .then((response) => response.json())); 8 | }, 3000) 9 | }); 10 | 11 | return promise; 12 | } 13 | -------------------------------------------------------------------------------- /examples/05-typescript/src/api/postAPI.ts: -------------------------------------------------------------------------------- 1 | import { fetchWithDelay } from './fetch'; 2 | const url = 'https://jsonplaceholder.typicode.com/posts'; 3 | 4 | const fetchPosts = () => fetchWithDelay(url) 5 | .then((posts : any[]) => posts.slice(0, 10)); 6 | 7 | export const postAPI = { 8 | fetchPosts, 9 | }; 10 | -------------------------------------------------------------------------------- /examples/05-typescript/src/api/userAPI.ts: -------------------------------------------------------------------------------- 1 | import { fetchWithDelay } from './fetch'; 2 | const url = 'https://jsonplaceholder.typicode.com/users'; 3 | 4 | const fetchUsers = () => fetchWithDelay(url); 5 | 6 | export const userAPI = { 7 | fetchUsers, 8 | }; 9 | -------------------------------------------------------------------------------- /examples/05-typescript/src/app.css: -------------------------------------------------------------------------------- 1 | .tables { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: nowrap; 5 | } 6 | 7 | .tables > div { 8 | flex-basis: 50%; 9 | margin-left: 1rem; 10 | margin-right: 1rem; 11 | } 12 | -------------------------------------------------------------------------------- /examples/05-typescript/src/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { trackPromise } from 'react-promise-tracker'; 3 | import { userAPI } from './api/userAPI'; 4 | import { postAPI } from './api/postAPI'; 5 | import { UserTable, PostTable, LoadButton } from './components'; 6 | import './app.css'; 7 | 8 | interface State { 9 | users: any[], 10 | posts: any[], 11 | } 12 | 13 | export class App extends React.Component<{}, State> { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.state = { 18 | users: [], 19 | posts: [], 20 | }; 21 | 22 | this.onLoadTables = this.onLoadTables.bind(this); 23 | } 24 | 25 | onLoadTables() { 26 | this.setState({ 27 | users: [], 28 | posts: [], 29 | }); 30 | 31 | trackPromise( 32 | userAPI.fetchUsers() 33 | .then((users : any[]) => { 34 | this.setState({ 35 | users, 36 | }) 37 | }) 38 | ); 39 | 40 | trackPromise( 41 | postAPI.fetchPosts() 42 | .then((posts) => { 43 | this.setState({ 44 | posts, 45 | }) 46 | }) 47 | ); 48 | } 49 | 50 | render() { 51 | return ( 52 |
53 | 57 |
58 | 59 | 60 |
61 |
62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/05-typescript/src/common/spinner/index.ts: -------------------------------------------------------------------------------- 1 | export * from './spinner'; 2 | -------------------------------------------------------------------------------- /examples/05-typescript/src/common/spinner/spinner.css: -------------------------------------------------------------------------------- 1 | .spinner { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .spinner > div { 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | -------------------------------------------------------------------------------- /examples/05-typescript/src/common/spinner/spinner.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { usePromiseTracker } from 'react-promise-tracker'; 3 | import Loader from 'react-loader-spinner'; 4 | import './spinner.css'; 5 | 6 | export const Spinner: React.FunctionComponent = () => { 7 | const { promiseInProgress } = usePromiseTracker(null); 8 | 9 | return ( 10 | promiseInProgress && ( 11 |
12 | 13 |
14 | ) 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /examples/05-typescript/src/common/table/index.ts: -------------------------------------------------------------------------------- 1 | export * from './table' 2 | -------------------------------------------------------------------------------- /examples/05-typescript/src/common/table/table.css: -------------------------------------------------------------------------------- 1 | .title { 2 | color: #000; 3 | } 4 | 5 | .table { 6 | width: 100%; 7 | max-width: 100%; 8 | margin-bottom: 1rem; 9 | background-color: transparent; 10 | border-collapse: collapse; 11 | } 12 | 13 | .table thead th { 14 | vertical-align: bottom; 15 | border-bottom: 2px solid #248f4f; 16 | } 17 | 18 | .table td, .table th { 19 | text-align: inherit; 20 | padding: .75rem; 21 | vertical-align: top; 22 | border-top: 1px solid #248f4f; 23 | } 24 | 25 | .table tbody tr:nth-of-type(odd) { 26 | background-color: rgba(43, 173, 96, .05); 27 | } 28 | -------------------------------------------------------------------------------- /examples/05-typescript/src/common/table/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './table.css'; 3 | 4 | export const Table = (props) => ( 5 |
6 |

{props.title}

7 |
8 | 9 | {props.headerRender()} 10 | 11 | 12 | {props.items.map(props.rowRender)} 13 | 14 |
15 | 16 | ); 17 | -------------------------------------------------------------------------------- /examples/05-typescript/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './userTable'; 2 | export * from './postTable'; 3 | export * from './loadButton'; 4 | -------------------------------------------------------------------------------- /examples/05-typescript/src/components/loadButton/index.ts: -------------------------------------------------------------------------------- 1 | export * from './loadButton'; 2 | -------------------------------------------------------------------------------- /examples/05-typescript/src/components/loadButton/loadButton.css: -------------------------------------------------------------------------------- 1 | .load-button { 2 | cursor: pointer; 3 | margin: 1rem; 4 | color: #fff; 5 | text-shadow: 1px 1px 0 #888; 6 | background-color: #2BAD60; 7 | border-color: #14522d; 8 | display: inline-block; 9 | font-weight: 400; 10 | text-align: center; 11 | white-space: nowrap; 12 | vertical-align: middle; 13 | border: 1px solid transparent; 14 | padding: .375rem .75rem; 15 | font-size: 1rem; 16 | line-height: 1.5; 17 | border-radius: .25rem; 18 | outline: none; 19 | } 20 | 21 | 22 | 23 | .load-button:hover { 24 | background-color: #248f4f; 25 | border-color: #1e7b43; 26 | } 27 | -------------------------------------------------------------------------------- /examples/05-typescript/src/components/loadButton/loadButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './loadButton.css'; 3 | 4 | export const LoadButton = (props) => ( 5 | 11 | ); 12 | -------------------------------------------------------------------------------- /examples/05-typescript/src/components/postTable/header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const Header = (props) => ( 4 | 5 | Id 6 | Title 7 | Body 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /examples/05-typescript/src/components/postTable/index.ts: -------------------------------------------------------------------------------- 1 | export * from './table'; 2 | -------------------------------------------------------------------------------- /examples/05-typescript/src/components/postTable/row.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const Row = (post) => ( 4 | 5 | 6 | {post.id} 7 | 8 | 9 | {post.title} 10 | 11 | 12 | {post.body} 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /examples/05-typescript/src/components/postTable/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Table } from '../../common/table'; 3 | import { Header } from './header'; 4 | import { Row } from './row'; 5 | 6 | export const PostTable = (props) => ( 7 | 13 | ); 14 | -------------------------------------------------------------------------------- /examples/05-typescript/src/components/userTable/header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const Header = (props) => ( 4 | 5 | 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /examples/05-typescript/src/components/userTable/index.ts: -------------------------------------------------------------------------------- 1 | export * from './table'; 2 | -------------------------------------------------------------------------------- /examples/05-typescript/src/components/userTable/row.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const Row = (user) => ( 4 | 5 | 8 | 11 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /examples/05-typescript/src/components/userTable/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Table } from '../../common/table'; 3 | import { Header } from './header'; 4 | import { Row } from './row'; 5 | 6 | export const UserTable = (props) => ( 7 |
IdNameEmail
6 | {user.id} 7 | 9 | {user.name} 10 | 12 | {user.email} 13 |
13 | ); 14 | -------------------------------------------------------------------------------- /examples/05-typescript/src/hello.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export const HelloComponent = () => { 4 | return

Hello component !

; 5 | }; 6 | -------------------------------------------------------------------------------- /examples/05-typescript/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |

Sample app

10 |
11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/05-typescript/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | 4 | import { App } from './app'; 5 | import { Spinner } from './common/spinner'; 6 | 7 | ReactDOM.render( 8 | <> 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | -------------------------------------------------------------------------------- /examples/05-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "es6", 5 | "moduleResolution": "node", 6 | "declaration": false, 7 | "noImplicitAny": false, 8 | "jsx": "react", 9 | "sourceMap": true, 10 | "noLib": false, 11 | "suppressImplicitAnyIndexErrors": true 12 | }, 13 | "compileOnSave": false, 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /examples/05-typescript/webpack.config.js: -------------------------------------------------------------------------------- 1 | var HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | var MiniCssExtractPlugin = require("mini-css-extract-plugin"); 3 | var webpack = require("webpack"); 4 | var path = require("path"); 5 | 6 | var basePath = __dirname; 7 | 8 | module.exports = { 9 | context: path.join(basePath, "src"), 10 | resolve: { 11 | extensions: [".js", ".ts", ".tsx"], 12 | alias: { 13 | react: path.resolve('./node_modules/react'), 14 | 'react-dom': path.resolve('./node_modules/react-dom') 15 | } 16 | }, 17 | entry: ["@babel/polyfill", "./index.tsx"], 18 | output: { 19 | path: path.join(basePath, "dist"), 20 | filename: "bundle.js" 21 | }, 22 | devtool: "source-map", 23 | devServer: { 24 | contentBase: "./dist", // Content base 25 | inline: true, // Enable watch and live reload 26 | host: "localhost", 27 | port: 8080, 28 | stats: "errors-only" 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.(ts|tsx)$/, 34 | exclude: /node_modules/, 35 | loader: "awesome-typescript-loader", 36 | options: { 37 | useBabel: true, 38 | babelCore: "@babel/core" // needed for Babel v7 39 | } 40 | }, 41 | { 42 | test: /\.css$/, 43 | use: [MiniCssExtractPlugin.loader, "css-loader"] 44 | }, 45 | { 46 | test: /\.(png|jpg|gif|svg)$/, 47 | loader: "file-loader", 48 | options: { 49 | name: "assets/img/[name].[ext]?[hash]" 50 | } 51 | } 52 | ] 53 | }, 54 | plugins: [ 55 | //Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin 56 | new HtmlWebpackPlugin({ 57 | filename: "index.html", //Name of file in ./dist/ 58 | template: "index.html", //Name of template in ./src 59 | hash: true 60 | }), 61 | new MiniCssExtractPlugin({ 62 | filename: "[name].css", 63 | chunkFilename: "[id].css" 64 | }) 65 | ] 66 | }; 67 | -------------------------------------------------------------------------------- /examples/06-suspense-like/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "useBuiltIns": "usage" 7 | } 8 | ], 9 | "@babel/preset-typescript", 10 | "@babel/preset-react" 11 | ], 12 | "plugins": [ 13 | "@babel/proposal-class-properties", 14 | "@babel/proposal-object-rest-spread" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /examples/06-suspense-like/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "06-suspense-like", 3 | "version": "1.0.0", 4 | "description": "React Promise Tracker sample with suspense-like component", 5 | "keywords": [ 6 | "react", 7 | "promise", 8 | "tracker", 9 | "hook", 10 | "hooks", 11 | "typescript" 12 | ], 13 | "author": "Javier Calzado (javi.calzado@lemoncode.net)", 14 | "license": "MIT", 15 | "main": "src/index.tsx", 16 | "scripts": { 17 | "start": "webpack-dev-server --mode development --inline --hot --open", 18 | "typecheck": "tsc --pretty --noEmit", 19 | "build": "npm run typecheck && webpack --mode development" 20 | }, 21 | "dependencies": { 22 | "@babel/polyfill": "^7.2.5", 23 | "@material-ui/core": "3.9.3", 24 | "react": "16.8.4", 25 | "react-dom": "16.8.4", 26 | "react-promise-tracker": "^2.1.1" 27 | }, 28 | "devDependencies": { 29 | "@babel/cli": "^7.2.3", 30 | "@babel/core": "^7.2.2", 31 | "@babel/plugin-proposal-class-properties": "^7.1.0", 32 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 33 | "@babel/preset-env": "^7.3.1", 34 | "@babel/preset-react": "^7.0.0", 35 | "@babel/preset-typescript": "^7.3.3", 36 | "@types/node": "11.13.4", 37 | "@types/react": "16.8.8", 38 | "@types/react-dom": "16.8.2", 39 | "babel-loader": "^8.0.6", 40 | "core-js": "^2.6.5", 41 | "html-webpack-plugin": "^3.2.0", 42 | "tslint": "^5.16.0", 43 | "tslint-react": "^4.0.0", 44 | "typescript": "^3.4.5", 45 | "webpack": "^4.29.3", 46 | "webpack-cli": "^3.2.3", 47 | "webpack-dev-server": "^3.1.14" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/06-suspense-like/src/api.ts: -------------------------------------------------------------------------------- 1 | const fetchWithDelay = (url: string, delay: number): Promise => 2 | new Promise(resolve => setTimeout(() => resolve(fetch(url, { method: "GET" })), delay)); 3 | 4 | export interface Quote { 5 | body: string; 6 | author: string; 7 | } 8 | 9 | export const getQuote = () => 10 | fetchWithDelay("https://favqs.com/api/qotd", Math.random() * 3000) 11 | .then(result => result.json()) 12 | .then<{ quote: Quote }>(result => result.quote); 13 | 14 | const arrayBufferToBase64 = buffer => { 15 | let binary = ""; 16 | const bytes = [].slice.call(new Uint8Array(buffer)); 17 | bytes.forEach(b => (binary += String.fromCharCode(b))); 18 | return window.btoa(binary); 19 | }; 20 | 21 | export const getPicture = (width: number, height: number) => 22 | fetchWithDelay(`https://picsum.photos/${width}/${height}`, Math.random() * 3000).then(res => 23 | res.arrayBuffer().then(buffer => "data:image/jpeg;base64," + arrayBufferToBase64(buffer)) 24 | ); 25 | -------------------------------------------------------------------------------- /examples/06-suspense-like/src/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import CircularProgress from "@material-ui/core/CircularProgress"; 3 | import Typography from "@material-ui/core/Typography"; 4 | import Button from "@material-ui/core/Button"; 5 | import { WithStyles, withStyles } from "@material-ui/core/styles"; 6 | import { trackPromise, usePromiseTracker } from "react-promise-tracker"; 7 | import { Quote, getQuote, getPicture } from "./api"; 8 | import { styles } from "./styles"; 9 | 10 | 11 | const Suspense: React.FC<{ tag: string, className?: string }> = ({ tag, className, children }) => { 12 | const { promiseInProgress } = usePromiseTracker({ area: tag }); 13 | return promiseInProgress ? : <>{children}; 14 | }; 15 | 16 | const AppInner: React.FC> = ({classes}) => { 17 | const [quote, setQuote] = React.useState({ body: "", author: "" }); 18 | const [picture, setPicture] = React.useState(); 19 | const loadData = React.useCallback(() => { 20 | trackPromise(getQuote(), "quote").then(setQuote); 21 | trackPromise(getPicture(500, 200), "picture").then(setPicture); 22 | }, []); 23 | 24 | React.useEffect(() => loadData(), []); 25 | 26 | return ( 27 |
28 | 29 | 30 | 31 | 32 | 33 | {quote.body} 34 | {quote.author} 35 | 36 |
37 | ); 38 | }; 39 | 40 | export const App = withStyles(styles)(AppInner); 41 | -------------------------------------------------------------------------------- /examples/06-suspense-like/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Promise Tracker - Suspense 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/06-suspense-like/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import CssBaseline from "@material-ui/core/CssBaseline"; 4 | import { App } from "./app"; 5 | 6 | ReactDOM.render( 7 | <> 8 | 9 | 10 | , 11 | document.getElementById("root") 12 | ); 13 | -------------------------------------------------------------------------------- /examples/06-suspense-like/src/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from "@material-ui/core/styles"; 2 | 3 | export const styles = () => createStyles({ 4 | progress: { 5 | margin: "1rem", 6 | }, 7 | container: { 8 | display: "flex", 9 | flexDirection: "column", 10 | alignItems: "center", 11 | padding: "2rem", 12 | }, 13 | button: { 14 | marginBottom: "2rem", 15 | }, 16 | pic: { 17 | marginBottom: "1.25rem", 18 | borderRadius: "8px", 19 | }, 20 | text: { 21 | marginBottom: "0.75rem", 22 | } 23 | }); 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/06-suspense-like/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "es6", 5 | "moduleResolution": "node", 6 | "declaration": false, 7 | "noImplicitAny": false, 8 | "jsx": "react", 9 | "sourceMap": true, 10 | "noLib": false, 11 | "suppressImplicitAnyIndexErrors": true, 12 | "allowSyntheticDefaultImports": true 13 | }, 14 | "compileOnSave": false, 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /examples/06-suspense-like/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], 3 | "defaultSeverity": "warning", 4 | "rules": { 5 | "align": [true, "parameters", "statements"], 6 | "array-type": false, 7 | "arrow-parens": false, 8 | "class-name": true, 9 | "comment-format": [true, "check-space"], 10 | "curly": false, 11 | "eofline": true, 12 | "forin": false, 13 | "import-spacing": true, 14 | "indent": [true, "spaces"], 15 | "interface-name": [true, "never-prefix"], 16 | "jsdoc-format": true, 17 | "jsx-no-lambda": false, 18 | "jsx-no-multiline-js": false, 19 | "label-position": true, 20 | "max-line-length": [true, 120], 21 | "member-ordering": false, 22 | "member-access": false, 23 | "no-any": false, 24 | "no-arg": true, 25 | "no-bitwise": true, 26 | "no-console": false, 27 | "no-consecutive-blank-lines": [true, 2], 28 | "no-construct": true, 29 | "no-debugger": true, 30 | "no-default-export": false, 31 | "no-duplicate-variable": true, 32 | "no-empty": true, 33 | "no-empty-interface": false, 34 | "no-eval": true, 35 | "no-implicit-dependencies": false, 36 | "no-internal-module": true, 37 | "no-object-literal-type-assertion": false, 38 | "no-shadowed-variable": true, 39 | "no-string-literal": false, 40 | "no-submodule-imports": false, 41 | "no-trailing-whitespace": true, 42 | "no-unsafe-finally": true, 43 | "no-unused-expression": true, 44 | "no-var-keyword": true, 45 | "no-var-requires": false, 46 | "object-literal-key-quotes": [true, "as-needed"], 47 | "object-literal-sort-keys": false, 48 | "one-line": [true, "check-catch", "check-else", "check-open-brace", "check-whitespace"], 49 | "only-arrow-functions": false, 50 | "ordered-imports": false, 51 | "quotemark": [true, "double", "jsx-double"], 52 | "radix": false, 53 | "semicolon": [true, "always", "strict-bound-class-methods"], 54 | "trailing-comma": [ 55 | true, 56 | { 57 | "multiline": { 58 | "objects": "always", 59 | "arrays": "always", 60 | "imports": "always", 61 | "exports": "always", 62 | "typeLiterals": "always" 63 | }, 64 | "singleline": "never", 65 | "esSpecCompliant": true 66 | } 67 | ], 68 | "triple-equals": [true, "allow-null-check"], 69 | "typedef": [true, "parameter", "property-declaration"], 70 | "typedef-whitespace": [ 71 | true, 72 | { 73 | "call-signature": "nospace", 74 | "index-signature": "nospace", 75 | "parameter": "nospace", 76 | "property-declaration": "nospace", 77 | "variable-declaration": "nospace" 78 | } 79 | ], 80 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"], 81 | "whitespace": [ 82 | true, 83 | "check-branch", 84 | "check-decl", 85 | "check-module", 86 | "check-operator", 87 | "check-separator", 88 | "check-type", 89 | "check-typecast" 90 | ] 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /examples/06-suspense-like/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | const path = require("path"); 3 | 4 | const basePath = __dirname; 5 | 6 | module.exports = { 7 | context: path.join(basePath, "src"), 8 | resolve: { 9 | extensions: [".js", ".ts", ".tsx"], 10 | }, 11 | entry: ["./index.tsx"], 12 | output: { 13 | path: path.join(basePath, "dist"), 14 | filename: "bundle.js" 15 | }, 16 | devtool: "source-map", 17 | devServer: { 18 | contentBase: "./dist", // Content base 19 | inline: true, // Enable watch and live reload 20 | host: "localhost", 21 | port: 8080, 22 | stats: "errors-only" 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.(tsx?)|(js)$/, 28 | exclude: /node_modules/, 29 | loader: "babel-loader", 30 | }, 31 | ] 32 | }, 33 | plugins: [ 34 | //Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin 35 | new HtmlWebpackPlugin({ 36 | filename: "index.html", //Name of file in ./dist/ 37 | template: "index.html", //Name of template in ./src 38 | hash: true 39 | }) 40 | ] 41 | }; 42 | -------------------------------------------------------------------------------- /examples/07-suspense-custom/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "useBuiltIns": "usage" 7 | } 8 | ], 9 | "@babel/preset-typescript", 10 | "@babel/preset-react" 11 | ], 12 | "plugins": [ 13 | "@babel/proposal-class-properties", 14 | "@babel/proposal-object-rest-spread" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /examples/07-suspense-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "07-suspense-custom", 3 | "version": "1.0.0", 4 | "description": "React Promise Tracker sample with suspense-like customizable component", 5 | "keywords": [ 6 | "react", 7 | "promise", 8 | "tracker", 9 | "hook", 10 | "hooks", 11 | "typescript" 12 | ], 13 | "author": "Javier Calzado (javi.calzado@lemoncode.net)", 14 | "license": "MIT", 15 | "main": "src/index.tsx", 16 | "scripts": { 17 | "start": "webpack-dev-server --mode development --inline --hot --open", 18 | "typecheck": "tsc --pretty --noEmit", 19 | "build": "npm run typecheck && webpack --mode development" 20 | }, 21 | "dependencies": { 22 | "@babel/polyfill": "^7.2.5", 23 | "@material-ui/core": "3.9.3", 24 | "react": "16.8.4", 25 | "react-dom": "16.8.4", 26 | "react-promise-tracker": "^2.1.1" 27 | }, 28 | "devDependencies": { 29 | "@babel/cli": "^7.2.3", 30 | "@babel/core": "^7.2.2", 31 | "@babel/plugin-proposal-class-properties": "^7.1.0", 32 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 33 | "@babel/preset-env": "^7.3.1", 34 | "@babel/preset-react": "^7.0.0", 35 | "@babel/preset-typescript": "^7.3.3", 36 | "@types/node": "11.13.4", 37 | "@types/react": "16.8.8", 38 | "@types/react-dom": "16.8.2", 39 | "babel-loader": "^8.0.6", 40 | "core-js": "^2.6.5", 41 | "html-webpack-plugin": "^3.2.0", 42 | "tslint": "^5.16.0", 43 | "tslint-react": "^4.0.0", 44 | "typescript": "^3.4.5", 45 | "webpack": "^4.29.3", 46 | "webpack-cli": "^3.2.3", 47 | "webpack-dev-server": "^3.1.14" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/07-suspense-custom/src/api.ts: -------------------------------------------------------------------------------- 1 | const fetchWithDelay = (url: string, delay: number): Promise => 2 | new Promise(resolve => setTimeout(() => resolve(fetch(url, { method: "GET" })), delay)); 3 | 4 | const arrayBufferToBase64 = buffer => { 5 | let binary = ""; 6 | const bytes = [].slice.call(new Uint8Array(buffer)); 7 | bytes.forEach(b => (binary += String.fromCharCode(b))); 8 | return window.btoa(binary); 9 | }; 10 | 11 | export const getPicture = (width: number, height: number) => 12 | fetchWithDelay(`https://picsum.photos/${width}/${height}`, Math.random() * 3000).then(res => 13 | res.arrayBuffer().then(buffer => "data:image/jpeg;base64," + arrayBufferToBase64(buffer)) 14 | ); 15 | -------------------------------------------------------------------------------- /examples/07-suspense-custom/src/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import CircularProgress from "@material-ui/core/CircularProgress"; 3 | import Button from "@material-ui/core/Button"; 4 | import { WithStyles, withStyles } from "@material-ui/core/styles"; 5 | import { trackPromise, usePromiseTracker } from "react-promise-tracker"; 6 | import { getPicture } from "./api"; 7 | import { styles } from "./styles"; 8 | import { randomBytes } from "crypto"; 9 | 10 | const Suspense: React.FC<{tag: string, Progress?: React.ReactNode}> = ({ tag, Progress, children }) => { 11 | const { promiseInProgress } = usePromiseTracker({ area: tag }); 12 | return <>{promiseInProgress ? Progress : children}; 13 | }; 14 | 15 | const randomWidth = () => Math.round(Math.random() * 250 + 80); 16 | const height = 100; 17 | 18 | const RandomPicInner: React.FC<{id: string} & WithStyles> = ({id, classes}) => { 19 | const [picture, setPicture] = React.useState(null); 20 | const [width] = React.useState(randomWidth()); 21 | 22 | React.useEffect(() => { 23 | trackPromise(getPicture(width, height), id).then(setPicture); 24 | }, [width, id]); 25 | return ( 26 |
27 | }> 28 | 29 | 30 |
31 | 32 | ); 33 | }; 34 | const RandomPic = withStyles(styles)(RandomPicInner); 35 | 36 | const AppInner: React.FC> = ({classes}) => { 37 | const [picIds, setPicIds] = React.useState([]); 38 | const randomize = React.useCallback(() => 39 | setPicIds(Array(16).fill(0).map(id => randomBytes(20).toString("hex"))) 40 | , []); 41 | React.useEffect(() => randomize(), [randomize]); 42 | 43 | return ( 44 |
45 | 46 |
47 | {picIds.map(id => )} 48 |
49 |
50 | ); 51 | }; 52 | export const App = withStyles(styles)(AppInner); 53 | -------------------------------------------------------------------------------- /examples/07-suspense-custom/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Promise Tracker - Suspense 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/07-suspense-custom/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import CssBaseline from "@material-ui/core/CssBaseline"; 4 | import { App } from "./app"; 5 | 6 | ReactDOM.render( 7 | <> 8 | 9 | 10 | , 11 | document.getElementById("root") 12 | ); 13 | -------------------------------------------------------------------------------- /examples/07-suspense-custom/src/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from "@material-ui/core/styles"; 2 | 3 | export const styles = () => createStyles({ 4 | picContainer: { 5 | display: "flex", 6 | justifyContent: "center", 7 | alignItems: "center", 8 | margin: "0.25rem", 9 | }, 10 | pic: { 11 | borderRadius: "4px", 12 | }, 13 | progress: { 14 | margin: "1rem", 15 | }, 16 | container: { 17 | display: "flex", 18 | flexDirection: "column", 19 | alignItems: "center", 20 | padding: "2rem", 21 | }, 22 | gallery: { 23 | display: "flex", 24 | flexDirection: "row", 25 | flexWrap: "wrap", 26 | justifyContent: "center", 27 | }, 28 | button: { 29 | marginBottom: "2rem", 30 | }, 31 | 32 | }); 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/07-suspense-custom/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "es6", 5 | "moduleResolution": "node", 6 | "declaration": false, 7 | "noImplicitAny": false, 8 | "jsx": "react", 9 | "sourceMap": true, 10 | "noLib": false, 11 | "suppressImplicitAnyIndexErrors": true, 12 | "allowSyntheticDefaultImports": true 13 | }, 14 | "compileOnSave": false, 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /examples/07-suspense-custom/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], 3 | "defaultSeverity": "warning", 4 | "rules": { 5 | "align": [true, "parameters", "statements"], 6 | "array-type": false, 7 | "arrow-parens": false, 8 | "class-name": true, 9 | "comment-format": [true, "check-space"], 10 | "curly": false, 11 | "eofline": true, 12 | "forin": false, 13 | "import-spacing": true, 14 | "indent": [true, "spaces"], 15 | "interface-name": [true, "never-prefix"], 16 | "jsdoc-format": true, 17 | "jsx-no-lambda": false, 18 | "jsx-no-multiline-js": false, 19 | "label-position": true, 20 | "max-line-length": [true, 120], 21 | "member-ordering": false, 22 | "member-access": false, 23 | "no-any": false, 24 | "no-arg": true, 25 | "no-bitwise": true, 26 | "no-console": false, 27 | "no-consecutive-blank-lines": [true, 2], 28 | "no-construct": true, 29 | "no-debugger": true, 30 | "no-default-export": false, 31 | "no-duplicate-variable": true, 32 | "no-empty": true, 33 | "no-empty-interface": false, 34 | "no-eval": true, 35 | "no-implicit-dependencies": false, 36 | "no-internal-module": true, 37 | "no-object-literal-type-assertion": false, 38 | "no-shadowed-variable": true, 39 | "no-string-literal": false, 40 | "no-submodule-imports": false, 41 | "no-trailing-whitespace": true, 42 | "no-unsafe-finally": true, 43 | "no-unused-expression": true, 44 | "no-var-keyword": true, 45 | "no-var-requires": false, 46 | "object-literal-key-quotes": [true, "as-needed"], 47 | "object-literal-sort-keys": false, 48 | "one-line": [true, "check-catch", "check-else", "check-open-brace", "check-whitespace"], 49 | "only-arrow-functions": false, 50 | "ordered-imports": false, 51 | "quotemark": [true, "double", "jsx-double"], 52 | "radix": false, 53 | "semicolon": [true, "always", "strict-bound-class-methods"], 54 | "trailing-comma": [ 55 | true, 56 | { 57 | "multiline": { 58 | "objects": "always", 59 | "arrays": "always", 60 | "imports": "always", 61 | "exports": "always", 62 | "typeLiterals": "always" 63 | }, 64 | "singleline": "never", 65 | "esSpecCompliant": true 66 | } 67 | ], 68 | "triple-equals": [true, "allow-null-check"], 69 | "typedef": [true, "parameter", "property-declaration"], 70 | "typedef-whitespace": [ 71 | true, 72 | { 73 | "call-signature": "nospace", 74 | "index-signature": "nospace", 75 | "parameter": "nospace", 76 | "property-declaration": "nospace", 77 | "variable-declaration": "nospace" 78 | } 79 | ], 80 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"], 81 | "whitespace": [ 82 | true, 83 | "check-branch", 84 | "check-decl", 85 | "check-module", 86 | "check-operator", 87 | "check-separator", 88 | "check-type", 89 | "check-typecast" 90 | ] 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /examples/07-suspense-custom/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | const path = require("path"); 3 | 4 | const basePath = __dirname; 5 | 6 | module.exports = { 7 | context: path.join(basePath, "src"), 8 | resolve: { 9 | extensions: [".js", ".ts", ".tsx"], 10 | }, 11 | entry: ["./index.tsx"], 12 | output: { 13 | path: path.join(basePath, "dist"), 14 | filename: "bundle.js" 15 | }, 16 | devtool: "source-map", 17 | devServer: { 18 | contentBase: "./dist", // Content base 19 | inline: true, // Enable watch and live reload 20 | host: "localhost", 21 | port: 8080, 22 | stats: "errors-only" 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.(tsx?)|(js)$/, 28 | exclude: /node_modules/, 29 | loader: "babel-loader", 30 | }, 31 | ] 32 | }, 33 | plugins: [ 34 | //Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin 35 | new HtmlWebpackPlugin({ 36 | filename: "index.html", //Name of file in ./dist/ 37 | template: "index.html", //Name of template in ./src 38 | hash: true 39 | }) 40 | ] 41 | }; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-promise-tracker", 3 | "version": "2.1.1", 4 | "description": "Simple React Promise tracker Hook/HOC helper to add loading spinner indicators", 5 | "keywords": [ 6 | "react", 7 | "promise", 8 | "tracker", 9 | "track", 10 | "hook", 11 | "hoc", 12 | "higher order component", 13 | "spinner", 14 | "component" 15 | ], 16 | "homepage": "https://github.com/Lemoncode/react-promise-tracker#readme", 17 | "bugs": { 18 | "url": "https://github.com/Lemoncode/react-promise-tracker/issues" 19 | }, 20 | "license": "MIT", 21 | "author": "Lemoncode", 22 | "contributors": [ 23 | "Braulio Diez (https://github.com/brauliodiez)", 24 | "Javier Calzado (https://github.com/fjcalzado)", 25 | "Daniel Sanchez (https://github.com/nasdan)", 26 | "Alejandro Rosa <> (https://github.com/arp82)" 27 | ], 28 | "files": [ 29 | "dist", 30 | "es", 31 | "lib", 32 | "index.d.ts", 33 | "LICENSE.txt", 34 | "package.json", 35 | "readme.md" 36 | ], 37 | "browser": "lib/index.js", 38 | "main": "lib/index.js", 39 | "types": "index.d.ts", 40 | "module": "es/index.js", 41 | "jsnext:main": "es/index.js", 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/Lemoncode/react-promise-tracker.git" 45 | }, 46 | "scripts": { 47 | "clean": "rimraf build/*", 48 | "build": "npm run clean && npm run build:lib && npm run build:es && npm run build:dist:prod && npm run build:copy", 49 | "build:lib": "cross-env BABEL_ENV=es5_cjs babel src --out-dir build/lib --ignore 'src/**/*.spec.js,src/**/*.test.js'", 50 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir build/es --ignore 'src/**/*.spec.js,src/**/*.test.js'", 51 | "build:dist:prod": "cross-env BABEL_ENV=umd webpack --config ./config/webpack/webpack.prod.js", 52 | "build:dist:dev": "cross-env BABEL_ENV=umd webpack --config ./config/webpack/webpack.dev.js", 53 | "build:copy": "copyfiles package.json readme.md LICENSE.txt build && copyfiles \"./src/**/*.d.ts\" -u 1 build", 54 | "test": "cross-env NODE_ENV=test jest -c ./config/test/jest.js", 55 | "test:watch": "npm run test -- --watchAll -i", 56 | "prepare": "husky install" 57 | }, 58 | "peerDependencies": { 59 | "react": ">=16.8.0" 60 | }, 61 | "devDependencies": { 62 | "@babel/cli": "^7.19.3", 63 | "@babel/core": "^7.19.6", 64 | "@babel/preset-env": "^7.19.4", 65 | "@babel/preset-react": "^7.18.6", 66 | "@babel/register": "^7.18.9", 67 | "@testing-library/jest-dom": "^5.16.5", 68 | "@testing-library/react": "^13.4.0", 69 | "@testing-library/user-event": "^14.4.3", 70 | "babel-loader": "^9.0.1", 71 | "compression-webpack-plugin": "^10.0.0", 72 | "copyfiles": "^2.4.1", 73 | "cross-env": "^7.0.3", 74 | "jest": "^29.2.2", 75 | "jest-environment-jsdom": "^29.2.2", 76 | "prettier": "^2.7.1", 77 | "pretty-quick": "^3.1.3", 78 | "react": "^18.2.0", 79 | "react-dom": "^18.2.0", 80 | "regenerator-runtime": "^0.13.10", 81 | "rimraf": "^3.0.2", 82 | "webpack": "^5.74.0", 83 | "webpack-bundle-analyzer": "^4.7.0", 84 | "webpack-cli": "^4.10.0", 85 | "webpack-merge": "^5.8.0", 86 | "husky": "^7.0.0" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/Lemoncode/react-promise-tracker/actions/workflows/ci.yml/badge.svg)](https://github.com/Lemoncode/react-promise-tracker/actions/workflows/ci.yml) 2 | 3 | # react-promise-tracker 4 | 5 | Simple promise tracker React Hoc. You can see it in action in this [Live Demo](https://stackblitz.com/github/lemoncode/react-promise-tracker/tree/master/examples/00-example-basic), and find the basic info to get started in this [post](https://www.basefactor.com/react-how-to-display-a-loading-indicator-on-fetch-calls). 6 | 7 | For detailed information check out the [documentation](https://lemoncode.github.io/react-promise-tracker/) 8 | 9 | # Why do I need this? 10 | 11 | Sometimes we need to track blocking promises (e.g. fetch or axios http calls), and control whether to 12 | display a loading spinner indicator not, you have to take care of scenarios like: 13 | 14 | - You could need to track several ajax calls being performed in parallel. 15 | - Some of them you want to be tracked some others to be executed silently in background. 16 | - You may want to have several spinners blocking only certain areas of the screen. 17 | - For high speed connection you may want to show the loading spinner after an small delay of time 18 | to avoid having a flickering effect in your screen. 19 | 20 | This library implements: 21 | 22 | - A simple function that will allow a promise to be tracked. 23 | - A Hook + HOC component that will allow us wrap a loading spinner (it will be displayed when the number of tracked request are greater than zero, and hidden when not). 24 | 25 | # Installation 26 | 27 | ```cmd 28 | npm install react-promise-tracker --save 29 | ``` 30 | 31 | # Usage 32 | 33 | Whenever you want a promise to be tracked, just wrap it like in the code below: 34 | 35 | ```diff 36 | + import { trackPromise} from 'react-promise-tracker'; 37 | //... 38 | 39 | + trackPromise( 40 | fetchUsers(); // You function that returns a promise 41 | + ); 42 | ``` 43 | 44 | Then you only need to create a spinner component and make use of the _usePromiseTracker_, this 45 | hook will expose a boolean property that will let us decide whether to show or hide the loading 46 | spinner. 47 | 48 | ## Basic sample: 49 | 50 | ```diff 51 | import React, { Component } from 'react'; 52 | + import { usePromiseTracker } from "react-promise-tracker"; 53 | 54 | export const LoadingSpinerComponent = (props) => { 55 | + const { promiseInProgress } = usePromiseTracker(); 56 | 57 | return ( 58 |
59 | { 60 | + (promiseInProgress === true) ? 61 |

Hey I'm a spinner loader wannabe !!!

62 | : 63 | null 64 | } 65 |
66 | ) 67 | }; 68 | ``` 69 | 70 | - To add a cool spinner component you can make use of _react-spinners_: 71 | 72 | - [Demo page](http://www.davidhu.io/react-spinners/) 73 | - [Github page](https://github.com/davidhu2000/react-spinners) 74 | 75 | * Then in your application entry point (main / app / ...) just add this loading spinner to be displayed: 76 | 77 | ```diff 78 | import React from 'react'; 79 | + import { LoadingSpinnerComponent} from './loadingSpinner'; 80 | 81 | export const AppComponent = (props) => ( 82 |
83 |

Hello App!

84 | + 85 |
86 | ); 87 | ``` 88 | 89 | ## Sample with areas: 90 | 91 | Using react-promise-tracker as is will just display a single spinner in your page, there are cases where you want to display a given spinner only blocking certain area of the screen (e.g.: a product list app with a shopping cart section. 92 | We would like to block the ui (show spinner) while is loading the product, but not the rest of the user interface, and the same thing with the shopping cart pop-up section. 93 | 94 | ![Shopping cart sample](/resources/00-shopping-cart-sample.png) 95 | 96 | The _promiseTracker_ hooks exposes a config parameter, here we can define the area that we want to setup 97 | (by default o area). We could just feed the area in the props of the common spinner we have created 98 | 99 | ```diff 100 | export const Spinner = (props) => { 101 | + const { promiseInProgress } = usePromiseTracker({area: props.area}); 102 | 103 | return ( 104 | promiseInProgress && ( 105 |
106 | 107 |
108 | ) 109 | ); 110 | }; 111 | ``` 112 | 113 | We could add the `default-area` to show product list spinner (no params means just default area): 114 | 115 | ```diff 116 | import React from 'react'; 117 | + import { LoadingSpinnerComponent} from './loadingSpinner'; 118 | 119 | export const ProductListComponent = (props) => ( 120 |
121 | ... 122 | + // default-area 123 |
124 | ); 125 | ``` 126 | 127 | And we add the `shopping-cart-area` to show shopping cart spinner: 128 | 129 | ```diff 130 | import React from 'react'; 131 | + import { LoadingSpinnerComponent} from './loadingSpinner'; 132 | 133 | export const ShoppingCartModal = (props) => ( 134 |
135 | + 136 |
137 | ); 138 | ``` 139 | 140 | The when we track a given promise we can choose the area that would be impacted. 141 | 142 | ```diff 143 | + import { trackPromise} from 'react-promise-tracker'; 144 | ... 145 | + trackPromise( 146 | fetchSelectedProducts(); 147 | + ,'shopping-cart-area'); 148 | ``` 149 | 150 | ## Sample with delay: 151 | 152 | You can add as well a delay to display the spinner, When is this useful? if your users are connected on 153 | high speed connections it would be worth to show the spinner right after 500 Ms (checking that the 154 | ajax request hasn't been completed), this will avoid having undesired screen flickering on high speed 155 | connection scenarios. 156 | 157 | ```diff 158 | export const Spinner = (props) => { 159 | + const { promiseInProgress } = usePromiseTracker({delay: 500}); 160 | ``` 161 | 162 | # Demos 163 | 164 | Full examples: 165 | 166 | > NOTE: If you are going to modify the following examples in Codesandbox, you must first do a fork 167 | 168 | - **00 Basic Example**: minimum sample to get started. 169 | - [Stackblitz](https://stackblitz.com/github/lemoncode/react-promise-tracker/tree/master/examples/00-example-basic) 170 | - [Codesandbox](https://codesandbox.io/s/github/lemoncode/react-promise-tracker/tree/master/examples/00-example-basic) 171 | 172 | - **01 Example Areas**: defining more than one spinner to be displayed in separate screen areas. 173 | - [Stackblitz](https://stackblitz.com/github/lemoncode/react-promise-tracker/tree/master/examples/01-example-areas) 174 | - [Codesandbox](https://codesandbox.io/s/github/lemoncode/react-promise-tracker/tree/master/examples/01-example-areas) 175 | 176 | - **02 Example Delay**: displaying the spinner after some miliseconds delay (useful when your users havbe high speed connections). 177 | - [Stackblitz](https://stackblitz.com/github/lemoncode/react-promise-tracker/tree/master/examples/02-example-delay) 178 | - [Codesandbox](https://codesandbox.io/s/github/lemoncode/react-promise-tracker/tree/master/examples/02-example-delay) 179 | 180 | - **03 Example Hoc**: using legacy high order component approach (useful if your spinner is a class based component). 181 | - [Stackblitz](https://stackblitz.com/github/lemoncode/react-promise-tracker/tree/master/examples/03-example-hoc) 182 | - [Codesandbox](https://codesandbox.io/s/github/lemoncode/react-promise-tracker/tree/master/examples/03-example-hoc) 183 | 184 | - **04 Initial load**: launching ajax request just on application startup before the spinner is being mounted. 185 | - [Stackblitz](https://stackblitz.com/github/lemoncode/react-promise-tracker/tree/master/examples/04-initial-load) 186 | - [Codesandbox](https://codesandbox.io/s/github/lemoncode/react-promise-tracker/tree/master/examples/04-initial-load) 187 | 188 | - **05 Typescript**: full sample using typescript (using library embedded typings). 189 | - [Stackblitz](https://stackblitz.com/github/lemoncode/react-promise-tracker/tree/master/examples/05-typescript) 190 | - [Codesandbox](https://codesandbox.io/s/github/lemoncode/react-promise-tracker/tree/master/examples/05-typescript) 191 | 192 | - **06 Suspense Like**: sample implementing a suspense-like component (typescript). 193 | - [Stackblitz](https://stackblitz.com/github/lemoncode/react-promise-tracker/tree/master/examples/06-suspense-like) 194 | - [Codesandbox](https://codesandbox.io/s/github/lemoncode/react-promise-tracker/tree/master/examples/06-suspense-like) 195 | 196 | - **07 Suspense Custom**: sample implementing a suspense-like component that can be customized by passing a spinner component of your choice (typescript). 197 | - [Stackblitz](https://stackblitz.com/github/lemoncode/react-promise-tracker/tree/master/examples/07-suspense-custom) 198 | - [Codesandbox](https://codesandbox.io/s/github/lemoncode/react-promise-tracker/tree/master/examples/07-suspense-custom) 199 | 200 | # About Basefactor + Lemoncode 201 | 202 | We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. 203 | 204 | [Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. 205 | 206 | [Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services. 207 | 208 | For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend 209 | -------------------------------------------------------------------------------- /readme_es.md: -------------------------------------------------------------------------------- 1 | # react-promise-tracker 2 | 3 | Componente React Hoc, rastreador de promesas. 4 | Puedes verlo en acción: [Demo](https://stackblitz.com/edit/react-promise-tracker-default-area-sample) 5 | 6 | ## ¿Por qué necesito esto? 7 | 8 | Algunas veces necesitas rastrear promesas bloqueantes (ejemplo: fetch http calls), 9 | para escoger entre mostrar un spinner de cargando... o no. 10 | 11 | Esta librería implementa: 12 | 13 | - Una función simple que te permitirá rastrear una promesa. 14 | - Un componente HOC, que nos permitirá usar un wrapper como spinner de cargando... (se mostrará cuando el número de peticiones rastreadas sea mayor que cero, y estará oculto cuando no). 15 | 16 | ## Instalación 17 | 18 | ```cmd 19 | npm install react-promise-tracker --save 20 | ``` 21 | 22 | ## Uso 23 | 24 | Siempre que quieras rastrear una promesa, simplemente usa el componente como wrapper tal como se muestra en el siguiente código: 25 | 26 | ```diff 27 | + import { trackPromise} from 'react-promise-tracker'; 28 | //... 29 | 30 | + trackPromise( 31 | fetchUsers(); // You function that returns a promise 32 | + ); 33 | ``` 34 | 35 | Entonces solo necesitas crear el componente que define una propiedad llamada _trackedPromiseInProgress_ 36 | 37 | Y envolverlo con el _promiseTrackerHoc_ 38 | 39 | ## Ejemplo básico 40 | 41 | ```diff 42 | import React, { Component } from 'react'; 43 | import PropTypes from 'prop-types'; 44 | + import { promiseTrackerHoc} from 'react-promise-tracker'; 45 | 46 | const InnerLoadingSpinerComponent = (props) => { 47 | return ( 48 |
49 | { 50 | (props.trackedPromiseInProgress === true) ? 51 |

Hey I'm a spinner loader wannabe !!!

52 | : 53 | null 54 | } 55 |
56 | ) 57 | }; 58 | 59 | InnerLoadingSpinerComponent.propTypes = { 60 | trackedPromiseInProgress : PropTypes.bool.isRequired, 61 | }; 62 | 63 | + export const LoadingSpinnerComponent = promiseTrackerHoc(InnerLoadingSpinerComponent); 64 | ``` 65 | 66 | - Para añadir un component spinner atractivo, puedes hacer uso de _react-spinners_: 67 | 68 | - [Demo page](http://www.davidhu.io/react-spinners/) 69 | - [Github page](https://github.com/davidhu2000/react-spinners) 70 | 71 | - Luego en el punto de entrada de tu apliación (main / app / ...) solo añade este componente loading spinner, para que sea renderizado: 72 | 73 | ```diff 74 | import React from 'react'; 75 | + import { LoadingSpinnerComponent} from './loadingSpinner'; 76 | 77 | export const AppComponent = (props) => ( 78 |
79 |

Hello App!

80 | + 81 |
82 | ); 83 | ``` 84 | 85 | ## Ejemplo con áreas 86 | 87 | Es posible usar react-promise-tracker como si se mostrara un solo spinner en la página. Hay casos en los que desea mostrar un spinner solo bloqueando cierta área de la pantalla (por ejemplo, una aplicación de lista de productos con una sección de carrito de la compra). 88 | Nos gustaría bloquear esa área de la UI (mostrar sólo el spinner) mientras carga el producto, pero no el resto de la interfaz de usuario, y lo mismo con la sección pop-up del carro de compras. 89 | 90 | ![Shopping cart sample](./resources/00-shopping-cart-sample.png) 91 | 92 | Podemos añadir el área `default-area` para mostrar el spinner de la lista de productos: 93 | 94 | ```diff 95 | import React from 'react'; 96 | + import { LoadingSpinnerComponent} from './loadingSpinner'; 97 | 98 | export const ProductListComponent = (props) => ( 99 |
100 | ... 101 | + // default-area 102 |
103 | ); 104 | ``` 105 | 106 | Si añadimos el área, `shopping-cart-area` mostraremos el spinner del carro de compra: 107 | 108 | ```diff 109 | import React from 'react'; 110 | + import { LoadingSpinnerComponent} from './loadingSpinner'; 111 | 112 | export const ShoppingCartModal = (props) => ( 113 |
114 | + 115 |
116 | ); 117 | ``` 118 | 119 | Con este enfoque, no necesitamos definir diferentes componentes spinners, con uno solo podemos renderizarlo cuando queramos rastrear promises en una determinada área: 120 | 121 | ```diff 122 | + import { trackPromise} from 'react-promise-tracker'; 123 | ... 124 | + trackPromise( 125 | fetchSelectedProducts(); 126 | + ,'shopping-cart-area'); 127 | ``` 128 | 129 | ## Demos 130 | 131 | Si quieres verlo en acción puedes visitar: 132 | 133 | - [Ejemplo del área por defecto](https://stackblitz.com/edit/react-promise-tracker-default-area-sample) 134 | 135 | - [Ejemplo de dos áreas](https://stackblitz.com/edit/react-promise-tracker-two-areas-sample) 136 | 137 | ## Sobre Lemoncode 138 | 139 | Somos un equipo de una larga experiencia como desarrolladores freelance, establecidos como grupo en 2010. 140 | Estamos especializados en tecnologías Front End y .NET. [Click aquí](http://lemoncode.net/services/en/#en-home) para más info sobre nosotros. 141 | 142 | Para la audiencia LATAM/Español estamos desarrollando un máster Front End Online, más info: [http://lemoncode.net/master-frontend](http://lemoncode.net/master-frontend) 143 | -------------------------------------------------------------------------------- /resources/00-shopping-cart-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemoncode/react-promise-tracker/fb6949a4a0ac2d7d9f187434f67456251655a26b/resources/00-shopping-cart-sample.png -------------------------------------------------------------------------------- /src/__snapshots__/trackerHoc.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false and area equals "default-area" when render promiseTrackerHoc without props 1`] = ` 4 | 5 |
  6 |     props: {
  7 |     "config": {
  8 |         "area": "default-area",
  9 |         "delay": 0
 10 |     },
 11 |     "promiseInProgress": false
 12 | }
 13 |   
14 |
15 | `; 16 | 17 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false and area equals "testArea" when feeding area equals "testArea" 1`] = ` 18 | 19 |
 20 |     props: {
 21 |     "config": {
 22 |         "area": "testArea",
 23 |         "delay": 0
 24 |     },
 25 |     "promiseInProgress": false
 26 | }
 27 |   
28 |
29 | `; 30 | 31 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false and area equals "testArea" when feeding area equals "testArea" and delay equals 300 1`] = ` 32 | 33 |
 34 |     props: {
 35 |     "config": {
 36 |         "area": "testArea",
 37 |         "delay": 300
 38 |     },
 39 |     "promiseInProgress": false
 40 | }
 41 |   
42 |
43 | `; 44 | 45 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 1`] = ` 46 | 47 |
 48 |     props: {
 49 |     "config": {
 50 |         "area": "default-area",
 51 |         "delay": 0
 52 |     },
 53 |     "promiseInProgress": false
 54 | }
 55 |   
56 |
57 | `; 58 | 59 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 and delay equals 300 1`] = ` 60 | 61 |
 62 |     props: {
 63 |     "config": {
 64 |         "area": "default-area",
 65 |         "delay": 300
 66 |     },
 67 |     "promiseInProgress": false
 68 | }
 69 |   
70 |
71 | `; 72 | 73 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false 1`] = ` 74 | 75 |
 76 |     props: {
 77 |     "config": {
 78 |         "area": "default-area",
 79 |         "delay": 0
 80 |     },
 81 |     "promiseInProgress": false
 82 | }
 83 |   
84 |
85 | `; 86 | 87 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false and delay equals 300 1`] = ` 88 | 89 |
 90 |     props: {
 91 |     "config": {
 92 |         "area": "default-area",
 93 |         "delay": 300
 94 |     },
 95 |     "promiseInProgress": false
 96 | }
 97 |   
98 |
99 | `; 100 | 101 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false to different area 1`] = ` 102 | 103 |
104 |     props: {
105 |     "config": {
106 |         "area": "default-area",
107 |         "delay": 0
108 |     },
109 |     "promiseInProgress": false
110 | }
111 |   
112 |
113 | `; 114 | 115 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false to different area and delay equals 300 1`] = ` 116 | 117 |
118 |     props: {
119 |     "config": {
120 |         "area": "default-area",
121 |         "delay": 300
122 |     },
123 |     "promiseInProgress": false
124 | }
125 |   
126 |
127 | `; 128 | 129 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals true to different area 1`] = ` 130 | 131 |
132 |     props: {
133 |     "config": {
134 |         "area": "default-area",
135 |         "delay": 0
136 |     },
137 |     "promiseInProgress": false
138 | }
139 |   
140 |
141 | `; 142 | 143 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals true to different area and delay equals 300 1`] = ` 144 | 145 |
146 |     props: {
147 |     "config": {
148 |         "area": "default-area",
149 |         "delay": 300
150 |     },
151 |     "promiseInProgress": false
152 | }
153 |   
154 |
155 | `; 156 | 157 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 1 and delay equals 300 1`] = ` 158 | 159 |
160 |     props: {
161 |     "config": {
162 |         "area": "default-area",
163 |         "delay": 300
164 |     },
165 |     "promiseInProgress": false
166 | }
167 |   
168 |
169 | `; 170 | 171 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 1 and emit event with progress equals false 1`] = ` 172 | 173 |
174 |     props: {
175 |     "config": {
176 |         "area": "default-area",
177 |         "delay": 0
178 |     },
179 |     "promiseInProgress": false
180 | }
181 |   
182 |
183 | `; 184 | 185 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 1 and emit event with progress equals false and delay equals 300 1`] = ` 186 | 187 |
188 |     props: {
189 |     "config": {
190 |         "area": "default-area",
191 |         "delay": 300
192 |     },
193 |     "promiseInProgress": false
194 | }
195 |   
196 |
197 | `; 198 | 199 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 1 and emit event with progress equals false to different area and delay equals 300 1`] = ` 200 | 201 |
202 |     props: {
203 |     "config": {
204 |         "area": "default-area",
205 |         "delay": 300
206 |     },
207 |     "promiseInProgress": false
208 | }
209 |   
210 |
211 | `; 212 | 213 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 1 and emit event with progress equals true and delay equals 300 1`] = ` 214 | 215 |
216 |     props: {
217 |     "config": {
218 |         "area": "default-area",
219 |         "delay": 300
220 |     },
221 |     "promiseInProgress": false
222 | }
223 |   
224 |
225 | `; 226 | 227 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false when counter is 1 and emit event with progress equals true to different area and delay equals 300 1`] = ` 228 | 229 |
230 |     props: {
231 |     "config": {
232 |         "area": "default-area",
233 |         "delay": 300
234 |     },
235 |     "promiseInProgress": false
236 | }
237 |   
238 |
239 | `; 240 | 241 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals false, area equals "default-area" and customProp equals "test" when feeding customProp equals "test" 1`] = ` 242 | 243 |
244 |     props: {
245 |     "customProp": "test",
246 |     "config": {
247 |         "area": "default-area",
248 |         "delay": 0
249 |     },
250 |     "promiseInProgress": false
251 | }
252 |   
253 |
254 | `; 255 | 256 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals true when counter is 0 and emit event with progress equals true 1`] = ` 257 | 258 |
259 |     props: {
260 |     "config": {
261 |         "area": "default-area",
262 |         "delay": 0
263 |     },
264 |     "promiseInProgress": true
265 | }
266 |   
267 |
268 | `; 269 | 270 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals true when counter is 0 and emit event with progress equals true and delay equals 300 1`] = ` 271 | 272 |
273 |     props: {
274 |     "config": {
275 |         "area": "default-area",
276 |         "delay": 300
277 |     },
278 |     "promiseInProgress": true
279 | }
280 |   
281 |
282 | `; 283 | 284 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals true when counter is 1 1`] = ` 285 | 286 |
287 |     props: {
288 |     "config": {
289 |         "area": "default-area",
290 |         "delay": 0
291 |     },
292 |     "promiseInProgress": true
293 | }
294 |   
295 |
296 | `; 297 | 298 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals false to different area 1`] = ` 299 | 300 |
301 |     props: {
302 |     "config": {
303 |         "area": "default-area",
304 |         "delay": 0
305 |     },
306 |     "promiseInProgress": true
307 | }
308 |   
309 |
310 | `; 311 | 312 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals true 1`] = ` 313 | 314 |
315 |     props: {
316 |     "config": {
317 |         "area": "default-area",
318 |         "delay": 0
319 |     },
320 |     "promiseInProgress": true
321 | }
322 |   
323 |
324 | `; 325 | 326 | exports[`trackerHoc Initial Status should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals true to different area 1`] = ` 327 | 328 |
329 |     props: {
330 |     "config": {
331 |         "area": "default-area",
332 |         "delay": 0
333 |     },
334 |     "promiseInProgress": true
335 | }
336 |   
337 |
338 | `; 339 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const defaultArea = 'default-area'; 2 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for react-promise-tracker 2 | // Project: https://github.com/Lemoncode/react-promise-tracker 3 | // Definitions by: Lemoncode 4 | 5 | import * as React from "react"; 6 | 7 | /** 8 | * It tracks a promise while in pending state. 9 | * @param promise Input promise to be tracked. 10 | * @returns It returns the same promise as input. 11 | */ 12 | export function trackPromise(promise: Promise, area?: string): Promise; 13 | 14 | /** 15 | * Perform a reset for the area counter (default-area by default). 16 | */ 17 | export function manuallyResetPromiseCounter(area?: string): void; 18 | 19 | /** 20 | * Decrement the area counter (default-area by default). 21 | */ 22 | export function manuallyDecrementPromiseCounter(area?: string): void; 23 | 24 | /** 25 | * Increment the area counter (default-area by default). 26 | */ 27 | export function manuallyIncrementPromiseCounter(area?: string): void; 28 | 29 | /** 30 | * Configuration contract: user can setup areas (display more than one spinner) or delay when 31 | * the spinner is shown (this is useful when a user has a fast connection, to avoid unneccessary flickering) 32 | */ 33 | 34 | interface Config { 35 | area?: string; 36 | delay?: number; 37 | } 38 | 39 | /** 40 | * It wraps a given React component into a new component that adds properties to watch 41 | * pending promises (HOC). 42 | * @param component Input component to be wrapped. 43 | * @returns It returns a new component that extends the input one. 44 | */ 45 | 46 | export interface ComponentToWrapProps { 47 | config: Config; 48 | promiseInProgress: boolean; 49 | } 50 | 51 | export interface TrackerHocProps { 52 | config?: Config; 53 | } 54 | 55 | export function promiseTrackerHoc

( 56 | component: React.ComponentType

57 | ): React.ComponentType

; 58 | 59 | /** 60 | * React Promise Tracker custom hook, this hook will expose a promiseInProgress boolean flag. 61 | * 62 | * @param configuration (optional can be null). 63 | * @returns promiseInProgressFlag. 64 | */ 65 | export function usePromiseTracker( 66 | outerConfig?: Config 67 | ): { promiseInProgress: boolean }; 68 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | trackPromise, 3 | manuallyResetPromiseCounter, 4 | manuallyDecrementPromiseCounter, 5 | manuallyIncrementPromiseCounter, 6 | } from './trackPromise'; 7 | export { promiseTrackerHoc } from './trackerHoc'; 8 | export { usePromiseTracker } from './trackerHook'; 9 | -------------------------------------------------------------------------------- /src/setupConfig.js: -------------------------------------------------------------------------------- 1 | import { defaultArea } from "./constants"; 2 | 3 | export const defaultConfig = { area: defaultArea, delay: 0 }; 4 | 5 | // Defensive config setup, fulfill default values 6 | export const setupConfig = (outerConfig) => ({ 7 | area: (!outerConfig || !outerConfig.area) ? defaultArea : outerConfig.area, 8 | delay: (!outerConfig || !outerConfig.delay) ? 0 : outerConfig.delay, 9 | }) 10 | -------------------------------------------------------------------------------- /src/setupConfig.test.js: -------------------------------------------------------------------------------- 1 | import { defaultArea } from "./constants"; 2 | import { setupConfig } from "./setupConfig"; 3 | 4 | describe("setupConfig", () => { 5 | it("should return same config as param if all values are informed", () => { 6 | // Arrange 7 | const userConfig = { area: "myarea", delay: 200 }; 8 | // Act 9 | const result = setupConfig(userConfig); 10 | 11 | // Assert 12 | expect(result.area).toBe("myarea"); 13 | expect(result.delay).toBe(200); 14 | }); 15 | 16 | it("should return default config (default are, 0 delay) if input param is undefined", () => { 17 | // Arrange 18 | const userConfig = void 0; 19 | // Act 20 | const result = setupConfig(userConfig); 21 | 22 | // Assert 23 | expect(result.area).toBe(defaultArea); 24 | expect(result.delay).toBe(0); 25 | }); 26 | 27 | it("should return default config (default area, 0 delay) if input para is null", () => { 28 | // Arrange 29 | const userConfig = null; 30 | // Act 31 | const result = setupConfig(userConfig); 32 | 33 | // Assert 34 | expect(result.area).toBe(defaultArea); 35 | expect(result.delay).toBe(0); 36 | }); 37 | 38 | it("should fullfill default config and area if input param informed but empty object {}", () => { 39 | // Arrange 40 | const userConfig = null; 41 | // Act 42 | const result = setupConfig(userConfig); 43 | 44 | // Assert 45 | expect(result.area).toBe(defaultArea); 46 | expect(result.delay).toBe(0); 47 | }); 48 | 49 | it("should fullfill defaultArea param if undefined but delay informed", () => { 50 | // Arrange 51 | const userConfig = { area: void 0, delay: 200 }; 52 | // Act 53 | const result = setupConfig(userConfig); 54 | 55 | // Assert 56 | expect(result.area).toBe(defaultArea); 57 | expect(result.delay).toBe(200); 58 | }); 59 | 60 | it("should fullfill defaultArea param if null but delay informed", () => { 61 | // Arrange 62 | const userConfig = { area: null, delay: 200 }; 63 | // Act 64 | const result = setupConfig(userConfig); 65 | 66 | // Assert 67 | expect(result.area).toBe(defaultArea); 68 | expect(result.delay).toBe(200); 69 | }); 70 | 71 | it("should fullfill delay param (0) if undefined but area informed", () => { 72 | // Arrange 73 | const userConfig = { area: "myarea", delay: void 0 }; 74 | // Act 75 | const result = setupConfig(userConfig); 76 | 77 | // Assert 78 | expect(result.area).toBe("myarea"); 79 | expect(result.delay).toBe(0); 80 | }); 81 | 82 | it("should fullfill delay param (0) if null but area informed", () => { 83 | // Arrange 84 | const userConfig = { area: "myarea", delay: null }; 85 | // Act 86 | const result = setupConfig(userConfig); 87 | 88 | // Assert 89 | expect(result.area).toBe("myarea"); 90 | expect(result.delay).toBe(0); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /src/tinyEmmiter.js: -------------------------------------------------------------------------------- 1 | 2 | // Based on: 3 | // https://github.com/scottcorgan/tiny-emitter 4 | // class based 5 | export class Emitter { 6 | emit(event, ...args) { 7 | if (!event) return this; 8 | for (const fn of this._e(event)) { 9 | fn.apply(fn.ctx, [...args]); 10 | if (fn.off_event == true) this.off(event, fn); 11 | } 12 | return this; 13 | } 14 | on(event, fn, ctx) { 15 | if (!event) return this; 16 | fn.ctx = ctx; 17 | this._e(event).push(fn); 18 | return this; 19 | } 20 | once(event, fn, ctx) { 21 | if (!event) return this; 22 | fn.ctx = ctx; 23 | fn.off_event = true; 24 | return this.on(event, fn); 25 | } 26 | off(event, fn) { 27 | if (!event) return this; 28 | if (!this[event]) return this; 29 | const e = this._e(event); 30 | if (!fn) { 31 | delete this[event]; 32 | return this; 33 | } 34 | this[event] = e.filter((f) => f != fn); 35 | return this; 36 | } 37 | _e(e) { 38 | return this[e] || (this[e] = []); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/trackPromise.js: -------------------------------------------------------------------------------- 1 | import { Emitter } from "./tinyEmmiter"; 2 | import { defaultArea } from "./constants"; 3 | 4 | export const emitter = new Emitter(); 5 | export const promiseCounterUpdateEventId = "promise-counter-update"; 6 | 7 | let counter = { 8 | [defaultArea]: 0 9 | }; 10 | 11 | export const getCounter = area => counter[area]; 12 | 13 | export const trackPromise = (promise, area) => { 14 | area = area || defaultArea; 15 | incrementPromiseCounter(area); 16 | 17 | const onResolveHandler = () => decrementPromiseCounter(area); 18 | promise.then(onResolveHandler, onResolveHandler); 19 | 20 | return promise; 21 | }; 22 | 23 | const incrementPromiseCounter = area => { 24 | incrementCounter(area); 25 | const promiseInProgress = anyPromiseInProgress(area); 26 | emitter.emit(promiseCounterUpdateEventId, promiseInProgress, area); 27 | }; 28 | 29 | const incrementCounter = area => { 30 | if (Boolean(counter[area])) { 31 | counter[area]++; 32 | } else { 33 | counter[area] = 1; 34 | } 35 | }; 36 | 37 | const anyPromiseInProgress = area => counter[area] > 0; 38 | 39 | const decrementPromiseCounter = area => { 40 | counter[area] > 0 && decrementCounter(area); 41 | const promiseInProgress = anyPromiseInProgress(area); 42 | emitter.emit(promiseCounterUpdateEventId, promiseInProgress, area); 43 | }; 44 | 45 | const decrementCounter = area => { 46 | counter[area]--; 47 | }; 48 | 49 | export const manuallyResetPromiseCounter = area => { 50 | area = area || defaultArea; 51 | counter[area] = 0; 52 | emitter.emit(promiseCounterUpdateEventId, false, area); 53 | }; 54 | 55 | export const manuallyDecrementPromiseCounter = area => { 56 | area = area || defaultArea; 57 | decrementPromiseCounter(area); 58 | }; 59 | 60 | export const manuallyIncrementPromiseCounter = area => { 61 | area = area || defaultArea; 62 | incrementPromiseCounter(area); 63 | }; 64 | // TODO: Enhancement we could catch here errors and throw an Event in case there's an HTTP Error 65 | // then the consumer of this event can be listening and decide what to to in case of error 66 | -------------------------------------------------------------------------------- /src/trackerHoc.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | emitter, 4 | getCounter, 5 | promiseCounterUpdateEventId, 6 | } from './trackPromise'; 7 | import { setupConfig } from './setupConfig'; 8 | 9 | // Props: 10 | // config: { 11 | // area: // can be null|undefined|'' (will default to DefaultArea) or area name 12 | // delay: // Wait Xms to display the spinner (fast connections scenario avoid blinking) 13 | // default value 0ms 14 | // } 15 | export const promiseTrackerHoc = ComponentToWrap => { 16 | return class promiseTrackerComponent extends Component { 17 | constructor(props) { 18 | super(props); 19 | 20 | this.state = { 21 | promiseInProgress: false, 22 | internalPromiseInProgress: false, 23 | config: setupConfig(props.config), 24 | }; 25 | 26 | this.notifyPromiseInProgress = this.notifyPromiseInProgress.bind(this); 27 | this.updateProgress = this.updateProgress.bind(this); 28 | this.subscribeToCounterUpdate = this.subscribeToCounterUpdate.bind(this); 29 | } 30 | 31 | notifyPromiseInProgress() { 32 | this.state.config.delay === 0 33 | ? this.setState({ promiseInProgress: true }) 34 | : setTimeout(() => { 35 | const progress = Boolean(getCounter(this.state.config.area) > 0); 36 | this.setState({ promiseInProgress: progress }); 37 | }, this.state.config.delay); 38 | } 39 | 40 | updateProgress(progress, afterUpdateCallback) { 41 | this.setState( 42 | { internalPromiseInProgress: progress }, 43 | afterUpdateCallback 44 | ); 45 | 46 | !progress 47 | ? this.setState({ promiseInProgress: false }) 48 | : this.notifyPromiseInProgress(); 49 | } 50 | 51 | subscribeToCounterUpdate() { 52 | emitter.on(promiseCounterUpdateEventId, (anyPromiseInProgress, area) => { 53 | if (this.state.config.area === area) { 54 | this.updateProgress(anyPromiseInProgress); 55 | } 56 | }); 57 | } 58 | 59 | componentDidMount() { 60 | this.updateProgress( 61 | Boolean(getCounter(this.state.config.area) > 0), 62 | this.subscribeToCounterUpdate 63 | ); 64 | } 65 | 66 | componentWillUnmount() { 67 | emitter.off(promiseCounterUpdateEventId); 68 | } 69 | 70 | render() { 71 | return ( 72 | 77 | ); 78 | } 79 | }; 80 | }; 81 | -------------------------------------------------------------------------------- /src/trackerHoc.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { promiseTrackerHoc } from './trackerHoc'; 4 | import * as trackPromiseAPI from './trackPromise'; 5 | import { defaultArea } from './constants'; 6 | 7 | const TestSpinnerComponent = (props) => ( 8 |

props: {JSON.stringify(props, null, 4)}
9 | ); 10 | 11 | describe('trackerHoc', () => { 12 | describe('Initial Status', () => { 13 | it('should render component with trackedPromiseInProgress equals false and area equals "default-area" when render promiseTrackerHoc without props', () => { 14 | // Arrange 15 | 16 | // Act 17 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 18 | 19 | // Assert 20 | const { asFragment } = render(); 21 | 22 | expect(asFragment()).toMatchSnapshot(); 23 | }); 24 | 25 | it('should render component with trackedPromiseInProgress equals false, area equals "default-area" and customProp equals "test" when feeding customProp equals "test"', () => { 26 | // Arrange 27 | 28 | // Act 29 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 30 | 31 | // Assert 32 | const { asFragment } = render(); 33 | 34 | expect(asFragment()).toMatchSnapshot(); 35 | }); 36 | 37 | it('should render component with trackedPromiseInProgress equals false and area equals "testArea" when feeding area equals "testArea"', () => { 38 | // Arrange 39 | 40 | // Act 41 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 42 | 43 | // Assert 44 | const { asFragment } = render( 45 | 46 | ); 47 | 48 | expect(asFragment()).toMatchSnapshot(); 49 | }); 50 | 51 | it('should render component with trackedPromiseInProgress equals false when counter is 0', () => { 52 | // Arrange 53 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(0); 54 | 55 | // Act 56 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 57 | 58 | // Assert 59 | const { asFragment } = render(); 60 | 61 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 62 | expect(asFragment()).toMatchSnapshot(); 63 | }); 64 | 65 | it('should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false', () => { 66 | // Arrange 67 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(0); 68 | 69 | const progress = false; 70 | const area = defaultArea; 71 | const emitterStub = jest 72 | .spyOn(trackPromiseAPI.emitter, 'on') 73 | .mockImplementation((id, callback) => callback(progress, area)); 74 | 75 | // Act 76 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 77 | 78 | // Assert 79 | const { asFragment } = render(); 80 | 81 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 82 | expect(asFragment()).toMatchSnapshot(); 83 | }); 84 | 85 | it('should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false to different area', () => { 86 | // Arrange 87 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(0); 88 | 89 | const progress = false; 90 | const area = 'otherArea'; 91 | const emitterStub = jest 92 | .spyOn(trackPromiseAPI.emitter, 'on') 93 | .mockImplementation((id, callback) => callback(progress, area)); 94 | 95 | // Act 96 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 97 | 98 | // Assert 99 | const { asFragment } = render(); 100 | 101 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 102 | expect(asFragment()).toMatchSnapshot(); 103 | }); 104 | 105 | it('should render component with trackedPromiseInProgress equals true when counter is 0 and emit event with progress equals true', () => { 106 | // Arrange 107 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(0); 108 | 109 | const progress = true; 110 | const area = defaultArea; 111 | const emitterStub = jest 112 | .spyOn(trackPromiseAPI.emitter, 'on') 113 | .mockImplementation((id, callback) => callback(progress, area)); 114 | 115 | // Act 116 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 117 | 118 | // Assert 119 | const { asFragment } = render(); 120 | 121 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 122 | expect(asFragment()).toMatchSnapshot(); 123 | }); 124 | 125 | it('should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals true to different area', () => { 126 | // Arrange 127 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(0); 128 | 129 | const progress = true; 130 | const area = 'otherArea'; 131 | const emitterStub = jest 132 | .spyOn(trackPromiseAPI.emitter, 'on') 133 | .mockImplementation((id, callback) => callback(progress, area)); 134 | 135 | // Act 136 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 137 | 138 | // Assert 139 | const { asFragment } = render(); 140 | 141 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 142 | expect(asFragment()).toMatchSnapshot(); 143 | }); 144 | 145 | it('should render component with trackedPromiseInProgress equals true when counter is 1', () => { 146 | // Arrange 147 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(1); 148 | 149 | // Act 150 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 151 | 152 | // Assert 153 | const { asFragment } = render(); 154 | 155 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 156 | expect(asFragment()).toMatchSnapshot(); 157 | }); 158 | 159 | it('should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals true', () => { 160 | // Arrange 161 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(1); 162 | 163 | const progress = true; 164 | const area = defaultArea; 165 | const emitterStub = jest 166 | .spyOn(trackPromiseAPI.emitter, 'on') 167 | .mockImplementation((id, callback) => callback(progress, area)); 168 | 169 | // Act 170 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 171 | 172 | // Assert 173 | const { asFragment } = render(); 174 | 175 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 176 | expect(asFragment()).toMatchSnapshot(); 177 | }); 178 | 179 | it('should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals true to different area', () => { 180 | // Arrange 181 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(1); 182 | 183 | const progress = true; 184 | const area = 'otherArea'; 185 | const emitterStub = jest 186 | .spyOn(trackPromiseAPI.emitter, 'on') 187 | .mockImplementation((id, callback) => callback(progress, area)); 188 | 189 | // Act 190 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 191 | 192 | // Assert 193 | const { asFragment } = render(); 194 | 195 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 196 | expect(asFragment()).toMatchSnapshot(); 197 | }); 198 | 199 | it('should render component with trackedPromiseInProgress equals false when counter is 1 and emit event with progress equals false', () => { 200 | // Arrange 201 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(1); 202 | 203 | const progress = false; 204 | const area = defaultArea; 205 | const emitterStub = jest 206 | .spyOn(trackPromiseAPI.emitter, 'on') 207 | .mockImplementation((id, callback) => callback(progress, area)); 208 | 209 | // Act 210 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 211 | 212 | // Assert 213 | const { asFragment } = render(); 214 | 215 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 216 | expect(asFragment()).toMatchSnapshot(); 217 | }); 218 | 219 | it('should render component with trackedPromiseInProgress equals true when counter is 1 and emit event with progress equals false to different area', () => { 220 | // Arrange 221 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(1); 222 | 223 | const progress = false; 224 | const area = 'otherArea'; 225 | const emitterStub = jest 226 | .spyOn(trackPromiseAPI.emitter, 'on') 227 | .mockImplementation((id, callback) => callback(progress, area)); 228 | 229 | // Act 230 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 231 | 232 | // Assert 233 | const { asFragment } = render(); 234 | 235 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 236 | expect(asFragment()).toMatchSnapshot(); 237 | }); 238 | 239 | it('should render component with trackedPromiseInProgress equals false and area equals "testArea" when feeding area equals "testArea" and delay equals 300', () => { 240 | // Arrange 241 | 242 | // Act 243 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 244 | 245 | // Assert 246 | const { asFragment } = render( 247 | 248 | ); 249 | 250 | expect(asFragment()).toMatchSnapshot(); 251 | }); 252 | 253 | it('should render component with trackedPromiseInProgress equals false when counter is 0 and delay equals 300', () => { 254 | // Arrange 255 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(0); 256 | 257 | // Act 258 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 259 | 260 | // Assert 261 | const { asFragment } = render( 262 | 263 | ); 264 | 265 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 266 | expect(asFragment()).toMatchSnapshot(); 267 | }); 268 | 269 | it('should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false and delay equals 300', () => { 270 | // Arrange 271 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(0); 272 | 273 | const progress = false; 274 | const area = defaultArea; 275 | const emitterStub = jest 276 | .spyOn(trackPromiseAPI.emitter, 'on') 277 | .mockImplementation((id, callback) => callback(progress, area)); 278 | 279 | // Act 280 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 281 | 282 | // Assert 283 | const { asFragment } = render( 284 | 285 | ); 286 | 287 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 288 | expect(asFragment()).toMatchSnapshot(); 289 | }); 290 | 291 | it('should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals false to different area and delay equals 300', () => { 292 | // Arrange 293 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(0); 294 | 295 | const progress = false; 296 | const area = 'otherArea'; 297 | const emitterStub = jest 298 | .spyOn(trackPromiseAPI.emitter, 'on') 299 | .mockImplementation((id, callback) => callback(progress, area)); 300 | 301 | // Act 302 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 303 | 304 | // Assert 305 | const { asFragment } = render( 306 | 307 | ); 308 | 309 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 310 | expect(asFragment()).toMatchSnapshot(); 311 | }); 312 | 313 | it('should render component with trackedPromiseInProgress equals true when counter is 0 and emit event with progress equals true and delay equals 300', () => { 314 | // Arrange 315 | trackPromiseAPI.getCounter = jest 316 | .fn() 317 | .mockReturnValueOnce(0) 318 | .mockReturnValueOnce(1); 319 | 320 | const progress = true; 321 | const area = defaultArea; 322 | const emitterStub = jest 323 | .spyOn(trackPromiseAPI.emitter, 'on') 324 | .mockImplementation((id, callback) => callback(progress, area)); 325 | const setTimeoutStub = jest 326 | .spyOn(window, 'setTimeout') 327 | .mockImplementation((callback) => callback()); 328 | 329 | // Act 330 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 331 | 332 | // Assert 333 | const { asFragment } = render( 334 | 335 | ); 336 | 337 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 338 | expect(asFragment()).toMatchSnapshot(); 339 | }); 340 | 341 | it('should render component with trackedPromiseInProgress equals false when counter is 0 and emit event with progress equals true to different area and delay equals 300', () => { 342 | // Arrange 343 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(0); 344 | 345 | const progress = true; 346 | const area = 'otherArea'; 347 | const emitterStub = jest 348 | .spyOn(trackPromiseAPI.emitter, 'on') 349 | .mockImplementation((id, callback) => callback(progress, area)); 350 | 351 | // Act 352 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 353 | 354 | // Assert 355 | const { asFragment } = render( 356 | 357 | ); 358 | 359 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 360 | expect(asFragment()).toMatchSnapshot(); 361 | }); 362 | 363 | it('should render component with trackedPromiseInProgress equals false when counter is 1 and delay equals 300', () => { 364 | // Arrange 365 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(1); 366 | 367 | // Act 368 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 369 | 370 | // Assert 371 | const { asFragment } = render( 372 | 373 | ); 374 | 375 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 376 | expect(asFragment()).toMatchSnapshot(); 377 | }); 378 | 379 | it('should render component with trackedPromiseInProgress equals false when counter is 1 and emit event with progress equals true and delay equals 300', () => { 380 | // Arrange 381 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(1); 382 | 383 | const progress = true; 384 | const area = defaultArea; 385 | const emitterStub = jest 386 | .spyOn(trackPromiseAPI.emitter, 'on') 387 | .mockImplementation((id, callback) => callback(progress, area)); 388 | 389 | // Act 390 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 391 | 392 | // Assert 393 | const { asFragment } = render( 394 | 395 | ); 396 | 397 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 398 | expect(asFragment()).toMatchSnapshot(); 399 | }); 400 | 401 | it('should render component with trackedPromiseInProgress equals false when counter is 1 and emit event with progress equals true to different area and delay equals 300', () => { 402 | // Arrange 403 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(1); 404 | 405 | const progress = true; 406 | const area = 'otherArea'; 407 | const emitterStub = jest 408 | .spyOn(trackPromiseAPI.emitter, 'on') 409 | .mockImplementation((id, callback) => callback(progress, area)); 410 | 411 | // Act 412 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 413 | 414 | // Assert 415 | const { asFragment } = render( 416 | 417 | ); 418 | 419 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 420 | expect(asFragment()).toMatchSnapshot(); 421 | }); 422 | 423 | it('should render component with trackedPromiseInProgress equals false when counter is 1 and emit event with progress equals false and delay equals 300', () => { 424 | // Arrange 425 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(1); 426 | 427 | const progress = false; 428 | const area = defaultArea; 429 | const emitterStub = jest 430 | .spyOn(trackPromiseAPI.emitter, 'on') 431 | .mockImplementation((id, callback) => callback(progress, area)); 432 | 433 | // Act 434 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 435 | 436 | // Assert 437 | const { asFragment } = render( 438 | 439 | ); 440 | 441 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 442 | expect(asFragment()).toMatchSnapshot(); 443 | }); 444 | 445 | it('should render component with trackedPromiseInProgress equals false when counter is 1 and emit event with progress equals false to different area and delay equals 300', () => { 446 | // Arrange 447 | trackPromiseAPI.getCounter = jest.fn().mockReturnValue(1); 448 | 449 | const progress = false; 450 | const area = 'otherArea'; 451 | const emitterStub = jest 452 | .spyOn(trackPromiseAPI.emitter, 'on') 453 | .mockImplementation((id, callback) => callback(progress, area)); 454 | 455 | // Act 456 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 457 | 458 | // Assert 459 | const { asFragment } = render( 460 | 461 | ); 462 | 463 | expect(trackPromiseAPI.getCounter).toHaveBeenCalled(); 464 | expect(asFragment()).toMatchSnapshot(); 465 | }); 466 | }); 467 | 468 | describe('Handling delay timeouts', () => { 469 | beforeEach(() => { 470 | jest.useFakeTimers(); 471 | }); 472 | 473 | afterAll(() => { 474 | jest.useRealTimers(); 475 | }); 476 | 477 | it('should render

NO SPINNER

when counter is 1 but delay is set to 300 (before timing out)', async () => { 478 | // Arrange 479 | const TestSpinnerComponent = (props) => { 480 | return ( 481 |
482 | {props.promiseInProgress ?

SPINNER

:

NO SPINNER

} 483 |
484 | ); 485 | }; 486 | 487 | const getCounterStub = jest.spyOn(trackPromiseAPI, 'getCounter'); 488 | 489 | // Act 490 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 491 | render(); 492 | 493 | // Check very beginning (no promises going on) NO SPINNER is shown 494 | // TODO: this assert could be skipped (move to another test) 495 | expect(screen.getByText('NO SPINNER')).toBeInTheDocument(); 496 | expect(getCounterStub).toHaveBeenCalled(); 497 | 498 | // Assert 499 | // This promise will resolved after 1 seconds, by doing this 500 | // we will be able to test 2 scenarios: 501 | // [0] first 200ms spinner won't be shown (text NOSPINNER) 502 | // [1] after 200ms spinner will be shown (text SPINNER) 503 | // [2] after 1000ms spinner will be hidded again (text NOSPINNER) 504 | 505 | const myFakePromise = new Promise((resolve) => { 506 | setTimeout(() => { 507 | resolve(true); 508 | }, 1000); 509 | }); 510 | 511 | trackPromiseAPI.trackPromise(myFakePromise); 512 | 513 | jest.advanceTimersByTime(100); 514 | 515 | // [0] first 200ms spinner won't be shown (text NOSPINNER) 516 | expect(await screen.findByText('NO SPINNER')).toBeInTheDocument(); 517 | 518 | jest.advanceTimersByTime(300); 519 | 520 | // Before the promise get's resolved 521 | // [1] after 200ms spinner will be shown (text SPINNER) 522 | expect(await screen.findByText('SPINNER')).toBeInTheDocument(); 523 | 524 | // After the promise get's resolved 525 | jest.runAllTimers(); 526 | 527 | // [2] after 1000ms spinner will be hidded again (text NOSPINNER) 528 | // Wait for fakePromise (simulated ajax call) to be completed 529 | // no spinner should be shown 530 | 531 | await myFakePromise; 532 | expect(await screen.findByText('NO SPINNER')).toBeInTheDocument(); 533 | expect(screen.queryByText('SPINNER')).not.toBeInTheDocument(); 534 | }); 535 | 536 | it('should render

SPINNER

when counter is 1, delay is set to 1000 and promise has 2000 timeout', async () => { 537 | // Arrange 538 | const TestSpinnerComponent = (props) => { 539 | return ( 540 |
541 | {props.promiseInProgress ?

SPINNER

:

NO SPINNER

} 542 |
543 | ); 544 | }; 545 | 546 | const getCounterStub = jest.spyOn(trackPromiseAPI, 'getCounter'); 547 | 548 | // Act 549 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 550 | render(); 551 | 552 | expect(await screen.findByText('NO SPINNER')).toBeInTheDocument(); 553 | expect(getCounterStub).toHaveBeenCalled(); 554 | 555 | // Assert 556 | const myFakePromise = new Promise((resolve) => { 557 | setTimeout(() => { 558 | resolve(true); 559 | }, 2000); 560 | }); 561 | 562 | trackPromiseAPI.trackPromise(myFakePromise); 563 | 564 | jest.advanceTimersByTime(500); 565 | expect(await screen.findByText('NO SPINNER')).toBeInTheDocument(); 566 | 567 | // Total advance 1000 568 | jest.advanceTimersByTime(500); 569 | expect(await screen.findByText('SPINNER')).toBeInTheDocument(); 570 | 571 | // Total advance 1999 572 | jest.advanceTimersByTime(999); 573 | expect(await screen.findByText('SPINNER')).toBeInTheDocument(); 574 | 575 | // After the promise get's resolved 576 | jest.runAllTimers(); 577 | 578 | await myFakePromise; 579 | expect(await screen.findByText('NO SPINNER')).toBeInTheDocument(); 580 | 581 | // Total advance 2010 582 | jest.advanceTimersByTime(11); 583 | expect(await screen.findByText('NO SPINNER')).toBeInTheDocument(); 584 | }); 585 | 586 | it('should render

NO SPINNER

when counter is 1, delay is set to 2000 and promise has 1000 timeout', async () => { 587 | // Arrange 588 | const TestSpinnerComponent = (props) => { 589 | return ( 590 |
591 | {props.promiseInProgress ?

SPINNER

:

NO SPINNER

} 592 |
593 | ); 594 | }; 595 | 596 | const getCounterStub = jest.spyOn(trackPromiseAPI, 'getCounter'); 597 | 598 | // Act 599 | const TrackedComponent = promiseTrackerHoc(TestSpinnerComponent); 600 | render(); 601 | 602 | expect(await screen.findByText('NO SPINNER')).toBeInTheDocument(); 603 | expect(getCounterStub).toHaveBeenCalled(); 604 | 605 | // Assert 606 | const myFakePromise = new Promise((resolve) => { 607 | setTimeout(() => { 608 | resolve(true); 609 | }, 1000); 610 | }); 611 | 612 | trackPromiseAPI.trackPromise(myFakePromise); 613 | 614 | jest.advanceTimersByTime(500); 615 | expect(await screen.findByText('NO SPINNER')).toBeInTheDocument(); 616 | 617 | // Total advance 1000 618 | jest.advanceTimersByTime(500); 619 | expect(await screen.findByText('NO SPINNER')).toBeInTheDocument(); 620 | 621 | await myFakePromise; 622 | expect(await screen.findByText('NO SPINNER')).toBeInTheDocument(); 623 | 624 | // Total advance 1999 625 | jest.advanceTimersByTime(999); 626 | expect(await screen.findByText('NO SPINNER')).toBeInTheDocument(); 627 | 628 | // After the promise get's resolved 629 | jest.runAllTimers(); 630 | }); 631 | }); 632 | }); 633 | -------------------------------------------------------------------------------- /src/trackerHook.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | emitter, 4 | promiseCounterUpdateEventId, 5 | getCounter 6 | } from "./trackPromise"; 7 | import { defaultConfig, setupConfig } from "./setupConfig"; 8 | 9 | export const usePromiseTracker = (outerConfig = defaultConfig) => { 10 | let isMounted = React.useRef(false); 11 | 12 | React.useEffect(() => { 13 | isMounted.current = true; 14 | return () => (isMounted.current = false); 15 | }, []); 16 | 17 | // Included in state, it will be evaluated just the first time, 18 | // TODO: discuss if this is a good approach 19 | // We need to apply defensive programming, ensure area and delay default to secure data 20 | // cover cases like not all params informed, set secure defaults 21 | const [config] = React.useState(setupConfig(outerConfig)); 22 | 23 | // Edge case, when we start the application if we are loading just onComponentDidMount 24 | // data, event emitter could have already emitted the event but subscription is not yet 25 | // setup 26 | React.useEffect(() => { 27 | if ( 28 | isMounted.current && 29 | config && 30 | config.area && 31 | getCounter(config.area) > 0 32 | ) { 33 | setInternalPromiseInProgress(true); 34 | setPromiseInProgress(true); 35 | } 36 | }, [config]); 37 | 38 | // Internal will hold the current value 39 | const [ 40 | internalPromiseInProgress, 41 | setInternalPromiseInProgress 42 | ] = React.useState(false); 43 | // Promise in progress is 'public', it can be affected by the _delay_ parameter 44 | // it may not show the current state 45 | const [promiseInProgress, setPromiseInProgress] = React.useState(false); 46 | 47 | // We need to hold a ref to latestInternal, to check the real value on 48 | // callbacks (if not we would get always the same value) 49 | // more info: https://overreacted.io/a-complete-guide-to-useeffect/ 50 | const latestInternalPromiseInProgress = React.useRef( 51 | internalPromiseInProgress 52 | ); 53 | 54 | const notifyPromiseInProgress = () => { 55 | !config || !config.delay || config.delay === 0 56 | ? setPromiseInProgress(true) 57 | : setTimeout(() => { 58 | // Check here ref to internalPromiseInProgress 59 | if (isMounted.current && latestInternalPromiseInProgress.current) { 60 | setPromiseInProgress(true); 61 | } 62 | }, config.delay); 63 | }; 64 | 65 | const updatePromiseTrackerStatus = (anyPromiseInProgress, areaAffected) => { 66 | if (isMounted.current && config.area === areaAffected) { 67 | setInternalPromiseInProgress(anyPromiseInProgress); 68 | // Update the ref object as well, we will check it when we need to 69 | // cover the _delay_ case (setTimeout) 70 | latestInternalPromiseInProgress.current = anyPromiseInProgress; 71 | if (!anyPromiseInProgress) { 72 | setPromiseInProgress(false); 73 | } else { 74 | notifyPromiseInProgress(); 75 | } 76 | } 77 | }; 78 | 79 | React.useEffect(() => { 80 | latestInternalPromiseInProgress.current = internalPromiseInProgress; 81 | emitter.on(promiseCounterUpdateEventId, updatePromiseTrackerStatus); 82 | 83 | return () => 84 | emitter.off(promiseCounterUpdateEventId, updatePromiseTrackerStatus); 85 | }, []); 86 | 87 | return { promiseInProgress }; 88 | }; 89 | -------------------------------------------------------------------------------- /src/trackerHook.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderHook, render, screen, act } from '@testing-library/react'; 3 | import * as trackPromiseAPI from './trackPromise'; 4 | import { usePromiseTracker } from './trackerHook'; 5 | 6 | describe('trackerHook', () => { 7 | describe('Initial Status', () => { 8 | it('should return promiseInProgress equals false on calls it', () => { 9 | // Arrange 10 | 11 | // Act 12 | const { result } = renderHook(() => usePromiseTracker()); 13 | 14 | // Assert 15 | expect(result.current).toEqual({ promiseInProgress: false }); 16 | }); 17 | 18 | it('should return promiseInProgress equals false when counter is 0', () => { 19 | // Arrange 20 | const getCounterStub = jest 21 | .spyOn(trackPromiseAPI, 'getCounter') 22 | .mockImplementation(() => 0); 23 | 24 | // Act 25 | const { result } = renderHook(() => usePromiseTracker()); 26 | 27 | // Assert 28 | expect(getCounterStub).toHaveBeenCalled(); 29 | expect(result.current).toEqual({ promiseInProgress: false }); 30 | }); 31 | 32 | it('should return promiseInProgress equals false when counter is 0 and emit event with progress equals false to different area', () => { 33 | // Arrange 34 | const getCounterStub = jest 35 | .spyOn(trackPromiseAPI, 'getCounter') 36 | .mockImplementation(() => 0); 37 | 38 | const progress = false; 39 | const area = 'otherArea'; 40 | const emitterStub = jest 41 | .spyOn(trackPromiseAPI.emitter, 'on') 42 | .mockImplementation((id, callback) => callback(progress, area)); 43 | 44 | // Act 45 | const { result } = renderHook(() => usePromiseTracker()); 46 | 47 | // Assert 48 | expect(getCounterStub).toHaveBeenCalled(); 49 | expect(result.current).toEqual({ promiseInProgress: false }); 50 | }); 51 | 52 | it('should return promiseInProgress equals false when counter is 0 and emit event with progress equals true to different area', () => { 53 | // Arrange 54 | const getCounterStub = jest 55 | .spyOn(trackPromiseAPI, 'getCounter') 56 | .mockImplementation(() => 0); 57 | 58 | const progress = true; 59 | const area = 'otherArea'; 60 | const emitterStub = jest 61 | .spyOn(trackPromiseAPI.emitter, 'on') 62 | .mockImplementation((id, callback) => callback(progress, area)); 63 | 64 | // Act 65 | const { result } = renderHook(() => usePromiseTracker()); 66 | 67 | // Assert 68 | expect(getCounterStub).toHaveBeenCalled(); 69 | expect(result.current).toEqual({ promiseInProgress: false }); 70 | }); 71 | 72 | it('should return promiseInProgress equals true when counter is 1', () => { 73 | // Arrange 74 | const getCounterStub = jest 75 | .spyOn(trackPromiseAPI, 'getCounter') 76 | .mockImplementation(() => 1); 77 | 78 | // Act 79 | const { result } = renderHook(() => usePromiseTracker()); 80 | 81 | // Assert 82 | expect(getCounterStub).toHaveBeenCalled(); 83 | expect(result.current).toEqual({ promiseInProgress: true }); 84 | }); 85 | 86 | it('should return promiseInProgress equals false and area equals "testArea" when feeding area equals "testArea" and delay equals 300', () => { 87 | // Arrange 88 | 89 | // Act 90 | const { result } = renderHook(() => 91 | usePromiseTracker({ 92 | area: 'testArea', 93 | delay: 300, 94 | }) 95 | ); 96 | 97 | // Assert 98 | expect(result.current).toEqual({ promiseInProgress: false }); 99 | }); 100 | }); 101 | 102 | describe('Handling delay timeouts', () => { 103 | beforeEach(() => { 104 | jest.useFakeTimers(); 105 | }); 106 | 107 | afterAll(() => { 108 | jest.useRealTimers(); 109 | }); 110 | 111 | it('should return promiseInProgress equals false when counter is 1 but delay is set to 200 (before timing out)', async () => { 112 | // Arrange 113 | let TestSpinnerComponent = null; 114 | act(() => { 115 | TestSpinnerComponent = (props) => { 116 | // Do not show spinner in the first 200 milliseconds (delay) 117 | const { promiseInProgress } = usePromiseTracker({ delay: 200 }); 118 | 119 | return ( 120 |
121 | {promiseInProgress ?

SPINNER

:

NO SPINNER

} 122 |
123 | ); 124 | }; 125 | }); 126 | 127 | // Act 128 | render(); 129 | 130 | // Check very beginning (no promises going on) NO SPINNER is shown 131 | // TODO: this assert could be skipped (move to another test) 132 | expect(await screen.findByText('NO SPINNER')).toBeInTheDocument(); 133 | 134 | // Assert 135 | // This promise will resolved after 1 seconds, by doing this 136 | // we will be able to test 2 scenarios: 137 | // [0] first 200ms spinner won't be shown (text NOSPINNER) 138 | // [1] after 200ms spinner will be shown (text SPINNER) 139 | // [2] after 1000ms spinner will be hidded again (text NOSPINNER) 140 | const myFakePromise = new Promise((resolve) => { 141 | setTimeout(() => { 142 | resolve(true); 143 | }, 1000); 144 | }); 145 | 146 | trackPromiseAPI.trackPromise(myFakePromise); 147 | 148 | // Runs all pending timers. whether it's a second from now or a year. 149 | // https://jestjs.io/docs/en/timer-mocks.html 150 | jest.advanceTimersByTime(100); 151 | 152 | // [0] first 200ms spinner won't be shown (text NOSPINNER) 153 | expect(await screen.findByText('NO SPINNER')).toBeInTheDocument(); 154 | 155 | // Runs all pending timers. whether it's a second from now or a year. 156 | // https://jestjs.io/docs/en/timer-mocks.html 157 | jest.advanceTimersByTime(300); 158 | 159 | // Before the promise get's resolved 160 | // [1] after 200ms spinner will be shown (text SPINNER) 161 | expect(await screen.findByText('SPINNER')).toBeInTheDocument(); 162 | 163 | // After the promise get's resolved 164 | jest.runAllTimers(); 165 | 166 | // [2] after 1000ms spinner will be hidded again (text NOSPINNER) 167 | // Wait for fakePromise (simulated ajax call) to be completed 168 | // no spinner should be shown 169 | await myFakePromise; 170 | expect(await screen.findByText('NO SPINNER')).toBeInTheDocument(); 171 | }); 172 | }); 173 | }); 174 | --------------------------------------------------------------------------------