├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── example.gif ├── examples ├── react-router-5 │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.jsx │ │ ├── main.jsx │ │ └── pages │ │ │ ├── Main.jsx │ │ │ ├── Page1.jsx │ │ │ ├── Page2.jsx │ │ │ └── fetchers.js │ └── vite.config.js └── react-router-6 │ ├── index.html │ ├── package.json │ ├── src │ ├── App.jsx │ ├── main.jsx │ └── pages │ │ ├── Main.jsx │ │ ├── Page1.jsx │ │ ├── Page2.jsx │ │ └── fetchers.js │ └── vite.config.js ├── package.json ├── packages └── react-router-loading │ ├── LICENSE │ ├── lib │ ├── LoadingContext.ts │ ├── Route.tsx │ ├── Routes.tsx │ ├── _DefaultLoadingScreen.tsx │ ├── _LoadingMiddleware.tsx │ ├── _LoadingRoutes.tsx │ ├── _RouteWrapper.tsx │ ├── index.ts │ ├── topbar.d.ts │ └── utils.ts │ ├── package.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.{yml,yaml,json,xml,html,htm}] 16 | indent_size = 2 17 | 18 | [*.md] 19 | insert_final_newline = false 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | 'env': { 4 | 'browser': true, 5 | 'es2021': true 6 | }, 7 | ignorePatterns: [ 8 | 'node_modules/**', 9 | '**/dist/**', 10 | ], 11 | 'extends': [ 12 | 'eslint:recommended', 13 | 'plugin:react/recommended', 14 | 'plugin:@typescript-eslint/recommended' 15 | ], 16 | 'parser': '@typescript-eslint/parser', 17 | 'parserOptions': { 18 | 'ecmaFeatures': { 19 | 'jsx': true 20 | }, 21 | 'ecmaVersion': 'latest', 22 | 'sourceType': 'module' 23 | }, 24 | 'plugins': [ 25 | 'react', 26 | '@typescript-eslint' 27 | ], 28 | 'rules': { 29 | 'no-var': 'error', 30 | 'no-undef': 'off', 31 | 'no-empty': 'off', 32 | 'no-console': 'warn', 33 | 'no-debugger': 'warn', 34 | 'prefer-const': 'warn', 35 | 'camelcase': 'warn', 36 | 'indent': [ 37 | 'warn', 38 | 2, 39 | { 40 | 'SwitchCase': 1 41 | } 42 | ], 43 | 'linebreak-style': [ 44 | 'warn', 45 | 'unix' 46 | ], 47 | 'quotes': [ 48 | 'warn', 49 | 'single', 50 | { 51 | 'avoidEscape': true 52 | } 53 | ], 54 | 'semi': [ 55 | 'warn', 56 | 'always' 57 | ], 58 | 'comma-dangle': [ 59 | 'warn', 60 | 'only-multiline' 61 | ], 62 | 'key-spacing': 'warn', 63 | '@typescript-eslint/interface-name-prefix': 'off', 64 | '@typescript-eslint/explicit-function-return-type': 'off', 65 | '@typescript-eslint/no-unused-vars': 'warn', 66 | '@typescript-eslint/no-empty-interface': 'off', 67 | '@typescript-eslint/no-empty-function': 'off', 68 | '@typescript-eslint/member-delimiter-style': 'warn', 69 | '@typescript-eslint/type-annotation-spacing': 'warn', 70 | '@typescript-eslint/comma-spacing': 'warn', 71 | '@typescript-eslint/func-call-spacing': 'warn', 72 | '@typescript-eslint/keyword-spacing': 'warn', 73 | '@typescript-eslint/object-curly-spacing': [ 74 | 'warn', 75 | 'always' 76 | ], 77 | '@typescript-eslint/space-before-function-paren': ['warn', { 78 | 'anonymous': 'never', 79 | 'named': 'never', 80 | 'asyncArrow': 'always' 81 | }], 82 | 'react/prop-types': 'off' 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # extra README for npm 107 | packages/react-router-loading/README.md 108 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/node_modules": true, 4 | "**/dist": true, 5 | "**/.git": true, 6 | }, 7 | "search.exclude": { 8 | "**/node_modules": true, 9 | "**/dist": true, 10 | "**/.git": true, 11 | }, 12 | "editor.insertSpaces": true, 13 | "editor.tabSize": 4, 14 | "editor.detectIndentation": false, 15 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-router-loading · [![npm version](https://badge.fury.io/js/react-router-loading.svg)](https://badge.fury.io/js/react-router-loading) 2 | 3 | Wrapper for `react-router` that allows you to load data before switching the screen 4 | \ 5 | ![](example.gif)\ 6 | DEMO (React Router 6) 7 | DEMO 0.x.x (React Router 5) 8 | 9 | ## Requirements 10 | ### ‼️ Version `1.x.x` supports React Router 6 only, please use version `0.x.x` for React Router 5 ‼️ 11 | 12 | | | | | 13 | | ------------ | ------- | --- | 14 | | react | >= 16.8 | | 15 | | react-router | **^5.0.0** | **Package version 0.x.x** | 16 | | react-router | **^6.0.0** | **Package version 1.x.x** | 17 | 18 | This package uses `react-router` (`react-router-dom` or `react-router-native`) as main router so you should implement it in your project first. 19 | 20 | ## Installation 21 | 22 | ```console 23 | npm install react-router-loading 24 | ## or 25 | yarn add react-router-loading 26 | ``` 27 | # Usage 28 | ## React Router 6 (package version 1.x.x) 29 | 30 | In your router section import `Routes` and `Route` from `react-router-loading` instead of `react-router-dom` or `react-router-native` 31 | ```js 32 | import { Routes, Route } from "react-router-loading"; 33 | 34 | 35 | } /> 36 | } /> 37 | ... 38 | 39 | ``` 40 | 41 | Add `loading` prop to every route that needs to be loaded before switching 42 | ```js 43 | 44 | // data will be loaded before switching 45 | } loading /> 46 | 47 | // instant switch as before 48 | } /> 49 | ... 50 | 51 | ``` 52 | 53 | Add `loadingContext.done()` at the end of your initial loading method in components that mentioned in routes with `loading` prop (in this case it's `Page1`) 54 | ```js 55 | import { useLoadingContext } from "react-router-loading"; 56 | const loadingContext = useLoadingContext(); 57 | 58 | const loading = async () => { 59 | // loading some data 60 | 61 | // call method to indicate that loading is done and we are ready to switch 62 | loadingContext.done(); 63 | }; 64 | ``` 65 | 66 | ## React Router 5 (package version 0.x.x) 67 | 68 | In your router section import `Switch` and `Route` from `react-router-loading` instead of `react-router-dom` 69 | ```js 70 | import { Switch, Route } from "react-router-loading"; 71 | 72 | 73 | 74 | 75 | ... 76 | 77 | ``` 78 | 79 | Add `loading` prop to every route that needs to be loaded before switching 80 | ```js 81 | 82 | // data will be loaded before switching 83 | 84 | 85 | // instant switch as before 86 | 87 | ... 88 | 89 | ``` 90 | 91 | Add `loadingContext.done()` at the end of your initial loading method in components that mentioned in routes with `loading` prop (in this case it's `Page1`) 92 | ```js 93 | import { LoadingContext } from "react-router-loading"; 94 | const loadingContext = useContext(LoadingContext); 95 | 96 | const loading = async () => { 97 | // loading some data 98 | 99 | // call method to indicate that loading is done and we are ready to switch 100 | loadingContext.done(); 101 | }; 102 | ``` 103 | ## Class components 104 | ```js 105 | import { LoadingContext } from "react-router-loading"; 106 | 107 | class ClassComponent extends React.Component { 108 | ... 109 | loading = async () => { 110 | // loading some data 111 | 112 | // call method from props to indicate that loading is done 113 | this.props.loadingContext.done(); 114 | }; 115 | ... 116 | }; 117 | 118 | // we should wrap class component with Context Provider to get access to loading methods 119 | const ClassComponentWrapper = (props) => 120 | 121 | {loadingContext => } 122 | 123 | 124 | ``` 125 | 126 | ## Config 127 | 128 | You can specify loading screen that will be shown at the first loading of your app 129 | ```js 130 | const MyLoadingScreen = () =>
Loading...
131 | 132 | // or 133 | ... 134 | 135 | ``` 136 | 137 | Use `maxLoadingTime` property if you want to limit loading time. Pages will switch if loading takes more time than specified in this property (ms). 138 | ```js 139 | 140 | // or 141 | ... 142 | 143 | ``` 144 | 145 | If you want to change LoadingContext globally you can pass `isLoading` property to the `` or ``. This way you don't need to add extra `loadingContext.done();` in your page components after fetching is done. 146 | ```js 147 | import { useIsFetching } from 'react-query'; 148 | const isFetching = useIsFetching(); 149 | 150 | // or 151 | ... 152 | 153 | ``` 154 | 155 | Call `topbar.config()` if you want to change topbar configuration. More info here. 156 | ```js 157 | import { topbar } from "react-router-loading"; 158 | 159 | topbar.config({ 160 | autoRun: false, 161 | barThickness: 5, 162 | barColors: { 163 | 0: 'rgba(26, 188, 156, .7)', 164 | .3: 'rgba(41, 128, 185, .7)', 165 | 1.0: 'rgba(231, 76, 60, .7)' 166 | }, 167 | shadowBlur: 5, 168 | shadowColor: 'red', 169 | className: 'topbar' 170 | }); 171 | ``` 172 | # Development 173 | 174 | Clone repository and run 175 | ```sh 176 | # go to lib folder 177 | cd packages/react-router-loading 178 | 179 | # restore packages 180 | yarn 181 | 182 | # build lib 183 | yarn build 184 | 185 | # go to example folder 186 | cd ../../examples/react-router-6 187 | 188 | # restore packages 189 | yarn 190 | 191 | # run example 192 | yarn dev 193 | ``` 194 | 195 | run `yarn build` in lib folder each time you want to apply changes 196 | 197 | ## License 198 | 199 | [MIT](./LICENSE) 200 | -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victortrusov/react-router-loading/bf94a13619bd70134bc36b7bb5abfe2e1d4005ca/example.gif -------------------------------------------------------------------------------- /examples/react-router-5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React router loading example 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/react-router-5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-5", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "react": "^18.1.0", 12 | "react-dom": "^18.1.0", 13 | "react-router-dom": "^5.3.3", 14 | "react-router-loading": "0.4.2" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.0.9", 18 | "@types/react-dom": "^18.0.5", 19 | "@vitejs/plugin-react": "^1.3.2", 20 | "typescript": "^4.6.3", 21 | "vite": "^2.9.13" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/react-router-5/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Switch, Route, topbar } from 'react-router-loading'; 4 | import { Main } from './pages/Main'; 5 | import { Page1 } from './pages/Page1'; 6 | import { Page2 } from './pages/Page2'; 7 | 8 | // config topbar 9 | topbar.config({ 10 | barColors: { 11 | 0: 'rgba(26, 188, 156, .7)', 12 | .3: 'rgba(41, 128, 185, .7)', 13 | 1.0: 'rgba(231, 76, 60, .7)' 14 | }, 15 | shadowBlur: 0 16 | }); 17 | 18 | const Layout = ({ children }) => 19 | 20 |
21 | Main 22 | Page 1 23 | Page 2 24 |
25 | Page 1 without loading 26 |
27 |
28 |
29 | {children} 30 |
31 |
; 32 | 33 | const App = () => 34 | 35 | {/* using Switch from react-router-loading */} 36 | 37 | {/* func component with state */} 38 | 39 | 40 | {/* same component but without loading prop */} 41 | 42 | 43 | {/* class component: have to pass loadingContext in it */} 44 | 45 | 46 | 47 | 48 | ; 49 | 50 | export default App; 51 | -------------------------------------------------------------------------------- /examples/react-router-5/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './App'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /examples/react-router-5/src/pages/Main.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext } from 'react'; 2 | import { LoadingContext } from 'react-router-loading'; 3 | import loadData from './fetchers'; 4 | 5 | export const Main = () => { 6 | const [state, setState] = useState(); 7 | const loadingContext = useContext(LoadingContext); 8 | 9 | const loading = async () => { 10 | // loading some data 11 | const data = await loadData(); 12 | setState(data); 13 | 14 | // call method to indicate that loading is done 15 | loadingContext.done(); 16 | }; 17 | 18 | useEffect(() => { 19 | loading(); 20 | }, []); 21 | 22 | return
23 |

This is main page

24 | {state ? 'Loading done!' : 'loading...'} 25 |
; 26 | }; 27 | -------------------------------------------------------------------------------- /examples/react-router-5/src/pages/Page1.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext } from 'react'; 2 | import { LoadingContext } from 'react-router-loading'; 3 | import loadData from './fetchers'; 4 | 5 | export const Page1 = () => { 6 | const [state, setState] = useState(); 7 | const loadingContext = useContext(LoadingContext); 8 | 9 | const loading = async () => { 10 | // loading some data 11 | const data = await loadData(); 12 | setState(data); 13 | 14 | // call method to indicate that loading is done 15 | loadingContext.done(); 16 | }; 17 | 18 | useEffect(() => { loading(); }, []); 19 | return
20 |

This is page 1

21 | {state ? 'Loading done!' : 'loading...'} 22 |
; 23 | }; 24 | -------------------------------------------------------------------------------- /examples/react-router-5/src/pages/Page2.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LoadingContext } from 'react-router-loading'; 3 | import loadData from './fetchers'; 4 | 5 | // example with class react component 6 | class Page2Component extends React.Component { 7 | state = { data: undefined }; 8 | 9 | loading = async () => { 10 | // loading some data 11 | const data = await loadData(); 12 | this.setState({ data }); 13 | 14 | // call method to indicate that loading is done 15 | this.props.loadingContext.done(); 16 | }; 17 | 18 | componentDidMount() { 19 | this.loading(); 20 | } 21 | 22 | render() { 23 | return ( 24 |
25 |

This is page 2 - class component

26 | {this.state.data ? 'Loading done!' : 'loading...'} 27 |
28 | ); 29 | } 30 | } 31 | 32 | // we should wrap class component with Context Provider to get access to loading methods 33 | export const Page2 = (props) => 34 | 35 | {loadingContext => } 36 | ; 37 | -------------------------------------------------------------------------------- /examples/react-router-5/src/pages/fetchers.js: -------------------------------------------------------------------------------- 1 | // here we imitate loading data 2 | const loadData = async () => { 3 | // waiting one second 4 | await new Promise(r => setTimeout(r, 1000)); 5 | 6 | // return data 7 | const data = 'this is loaded data'; 8 | return data; 9 | }; 10 | export default loadData; 11 | -------------------------------------------------------------------------------- /examples/react-router-5/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }); 8 | -------------------------------------------------------------------------------- /examples/react-router-6/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React router loading example 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/react-router-6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-6", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "react": "^18.1.0", 12 | "react-dom": "^18.1.0", 13 | "react-router-dom": "^6.3.0", 14 | "react-router-loading": "1.0.0-beta.3" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.0.9", 18 | "@types/react-dom": "^18.0.5", 19 | "@vitejs/plugin-react": "^1.3.2", 20 | "typescript": "^4.6.3", 21 | "vite": "^2.9.13" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/react-router-6/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Routes, Route, topbar } from 'react-router-loading'; 4 | import { Main } from './pages/Main'; 5 | import { Page1 } from './pages/Page1'; 6 | import { Page2 } from './pages/Page2'; 7 | 8 | // config topbar 9 | topbar.config({ 10 | barColors: { 11 | 0: 'rgba(26, 188, 156, .7)', 12 | .3: 'rgba(41, 128, 185, .7)', 13 | 1.0: 'rgba(231, 76, 60, .7)' 14 | }, 15 | shadowBlur: 0 16 | }); 17 | 18 | const Layout = ({ children }) => 19 | 20 |
21 | Main 22 | Page 1 23 | Page 2 24 | Page 2 with query 25 |
26 | Page 1 without loading 27 |
28 | Nested Main 29 | Nested Page 1 30 | Nested Page 2 31 |
32 |
33 |
34 | {children} 35 |
36 |
; 37 | 38 | const App = () => 39 | 40 | {/* using Routes from react-router-loading */} 41 | 42 | } loading /> 43 | 44 | {/* func component with state */} 45 | } loading /> 46 | 47 | {/* same component but without loading prop */} 48 | } /> 49 | 50 | {/* class component: have to pass loadingContext in it */} 51 | } loading /> 52 | 53 | {/* works with nested rows */} 54 | } loading> 55 | } loading /> 56 | } loading /> 57 | 58 | 59 | ; 60 | 61 | export default App; 62 | -------------------------------------------------------------------------------- /examples/react-router-6/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './App'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /examples/react-router-6/src/pages/Main.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useLoadingContext } from 'react-router-loading'; 3 | import loadData from './fetchers'; 4 | 5 | export const Main = () => { 6 | const [state, setState] = useState(); 7 | const loadingContext = useLoadingContext(); 8 | 9 | const loading = async () => { 10 | // loading some data 11 | const data = await loadData(); 12 | setState(data); 13 | 14 | // call method to indicate that loading is done 15 | loadingContext.done(); 16 | }; 17 | 18 | useEffect(() => { 19 | loading(); 20 | }, []); 21 | 22 | return
23 |

This is main page

24 | {state ? 'Loading done!' : 'loading...'} 25 |
; 26 | }; 27 | -------------------------------------------------------------------------------- /examples/react-router-6/src/pages/Page1.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useLoadingContext } from 'react-router-loading'; 3 | import loadData from './fetchers'; 4 | 5 | export const Page1 = () => { 6 | const [state, setState] = useState(); 7 | const loadingContext = useLoadingContext(); 8 | 9 | const loading = async () => { 10 | // loading some data 11 | const data = await loadData(); 12 | setState(data); 13 | 14 | // call method to indicate that loading is done 15 | loadingContext.done(); 16 | }; 17 | 18 | useEffect(() => { loading(); }, []); 19 | return
20 |

This is page 1

21 | {state ? 'Loading done!' : 'loading...'} 22 |
; 23 | }; 24 | -------------------------------------------------------------------------------- /examples/react-router-6/src/pages/Page2.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LoadingContext } from 'react-router-loading'; 3 | import loadData from './fetchers'; 4 | 5 | // example with class react component 6 | class Page2Component extends React.Component { 7 | state = { data: undefined }; 8 | 9 | loading = async () => { 10 | // loading some data 11 | const data = await loadData(); 12 | this.setState({ data }); 13 | 14 | // call method to indicate that loading is done 15 | this.props.loadingContext.done(); 16 | }; 17 | 18 | componentDidMount() { 19 | this.loading(); 20 | } 21 | 22 | render() { 23 | return ( 24 |
25 |

This is page 2 - class component

26 | {this.state.data ? 'Loading done!' : 'loading...'} 27 |
28 | ); 29 | } 30 | } 31 | 32 | // we should wrap class component with Context Provider to get access to loading methods 33 | export const Page2 = (props) => 34 | 35 | {loadingContext => } 36 | ; 37 | -------------------------------------------------------------------------------- /examples/react-router-6/src/pages/fetchers.js: -------------------------------------------------------------------------------- 1 | // here we imitate loading data 2 | const loadData = async () => { 3 | // waiting one second 4 | await new Promise(r => setTimeout(r, 1000)); 5 | 6 | // return data 7 | const data = 'this is loaded data'; 8 | return data; 9 | }; 10 | export default loadData; 11 | -------------------------------------------------------------------------------- /examples/react-router-6/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }); 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-loading-workspace", 3 | "private": true, 4 | "workspaces": { 5 | "packages": [ 6 | "packages/*", 7 | "examples/*" 8 | ] 9 | }, 10 | "devDependencies": { 11 | "@typescript-eslint/eslint-plugin": "^5.26.0", 12 | "@typescript-eslint/parser": "^5.26.0", 13 | "eslint": "^8.16.0", 14 | "eslint-plugin-react": "^7.30.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/react-router-loading/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Victor Trusov 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 | -------------------------------------------------------------------------------- /packages/react-router-loading/lib/LoadingContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | // Interface 4 | 5 | export interface LoadingContextActions { 6 | start: () => void; 7 | done: () => void; 8 | restart: () => void; 9 | } 10 | 11 | // Actions 12 | 13 | const LoadingContext = createContext({ 14 | start: () => { }, 15 | done: () => { }, 16 | restart: () => { } 17 | }); 18 | LoadingContext.displayName = 'LoadingContext'; 19 | 20 | // Value 21 | 22 | const LoadingGetterContext = createContext(false); 23 | LoadingGetterContext.displayName = 'LoadingGetterContext'; 24 | 25 | export { 26 | LoadingContext, 27 | LoadingGetterContext 28 | }; 29 | -------------------------------------------------------------------------------- /packages/react-router-loading/lib/Route.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Route as OriginalRoute, RouteProps as OriginalRouteProps } from 'react-router'; 3 | 4 | type RouteProps = OriginalRouteProps & { 5 | loading?: boolean; 6 | } 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 9 | const Route: FC = ({ loading, ...props }) => ; 10 | 11 | export default Route; 12 | -------------------------------------------------------------------------------- /packages/react-router-loading/lib/Routes.tsx: -------------------------------------------------------------------------------- 1 | import React, { ElementType, FC, ReactNode } from 'react'; 2 | import LoadingRoutes from './_LoadingRoutes'; 3 | import LoadingMiddleware from './_LoadingMiddleware'; 4 | 5 | interface RoutesProps { 6 | children: ReactNode; 7 | loadingScreen?: ElementType; 8 | maxLoadingTime?: number; 9 | isLoading?: boolean; 10 | } 11 | 12 | // combine topbar and switcher 13 | const Routes: FC = ({ children, loadingScreen, maxLoadingTime, isLoading }) => 14 | 15 | 16 | {children} 17 | 18 | ; 19 | 20 | export default Routes; 21 | -------------------------------------------------------------------------------- /packages/react-router-loading/lib/_DefaultLoadingScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | const DefaultLoadingScreen: FC = () => 4 |
; 14 | 15 | export default DefaultLoadingScreen; 16 | -------------------------------------------------------------------------------- /packages/react-router-loading/lib/_LoadingMiddleware.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo, useCallback, useEffect, useRef, FC, PropsWithChildren } from 'react'; 2 | import { LoadingContext, LoadingGetterContext } from './LoadingContext'; 3 | import { topbar } from '.'; 4 | 5 | const LoadingMiddleware: FC> = ({ children, isLoading = false }) => { 6 | const [loading, setLoading] = useState(isLoading); 7 | const isFirstRender = useRef(true); 8 | 9 | const start = useCallback(() => { 10 | topbar.show(); 11 | setLoading(true); 12 | }, []); 13 | 14 | const done = useCallback(() => { 15 | topbar.hide(); 16 | setLoading(false); 17 | }, []); 18 | 19 | const restart = useCallback(() => { 20 | topbar.hide(); 21 | topbar.show(); 22 | }, []); 23 | 24 | useEffect(() => { 25 | if (!isFirstRender.current) { 26 | if (isLoading && !loading) 27 | start(); 28 | else if (loading) 29 | done(); 30 | } else { 31 | isFirstRender.current = false; 32 | } 33 | }, [isLoading]); 34 | 35 | const loadingProvider = useMemo( 36 | () => 37 | {children} 38 | , 39 | [] 40 | ); 41 | 42 | return ( 43 | 44 | {loadingProvider} 45 | 46 | ); 47 | }; 48 | 49 | export default LoadingMiddleware; 50 | -------------------------------------------------------------------------------- /packages/react-router-loading/lib/_LoadingRoutes.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { useState, useContext, useEffect, useMemo, useRef, PropsWithChildren, FC } from 'react'; 3 | import { useLocation, Location, useNavigationType, NavigationType } from 'react-router'; 4 | import { LoadingContext, LoadingGetterContext } from './LoadingContext'; 5 | import DefaultLoadingScreen from './_DefaultLoadingScreen'; 6 | import { createRoutesFromChildren, isLoadable, isPathsDifferent, isPathsEqual, isSearchDifferent } from './utils'; 7 | import { RouteWrapper } from './_RouteWrapper'; 8 | 9 | interface LoadingRoutesProps { 10 | loadingScreen?: React.ElementType; 11 | maxLoadingTime?: number; 12 | } 13 | 14 | interface LoadingRoutesState { 15 | location: Location; 16 | navigationType: NavigationType; 17 | } 18 | 19 | const LOADING_PATHNAME = '__loading'; 20 | 21 | const LoadingRoutes: FC> = ({ 22 | children, 23 | loadingScreen: LoadingScreen, 24 | maxLoadingTime = 0 25 | }) => { 26 | 27 | // 🪝 Hooks 28 | const location = useLocation(); 29 | const navigationType = useNavigationType(); 30 | const loadingContext = useContext(LoadingContext); 31 | const isCurrentlyLoading = useContext(LoadingGetterContext); 32 | 33 | // 🗄 State 34 | const routes = useMemo( 35 | () => createRoutesFromChildren(children), 36 | [children] 37 | ); 38 | 39 | const [current, setCurrent] = useState(() => { 40 | const isFirstPageLoadable = isLoadable(location, routes); 41 | 42 | // if first page loadable showing loading screen 43 | const firstLocation = isFirstPageLoadable 44 | ? { ...location, pathname: LOADING_PATHNAME } 45 | : location; 46 | 47 | return { 48 | location: firstLocation, 49 | navigationType: navigationType 50 | }; 51 | }); 52 | const [next, setNext] = useState(current); 53 | 54 | const timeout: React.MutableRefObject = useRef(); 55 | 56 | // 🔄 Lifecycle 57 | // when location was changed 58 | useEffect(() => { 59 | // if not the same route mount it to start loading 60 | if (isPathsDifferent(location, next.location)) { 61 | const isPageLoadable = isLoadable(location, routes); 62 | 63 | setNext({ 64 | location: { ...location }, 65 | navigationType 66 | }); 67 | 68 | if (!isPageLoadable) { 69 | loadingContext.done(); 70 | setCurrent({ 71 | location: { ...location }, 72 | navigationType 73 | }); 74 | } else { 75 | if (!isCurrentlyLoading) 76 | loadingContext.start(); 77 | else 78 | loadingContext.restart(); 79 | } 80 | } 81 | 82 | // if same as the current location stop loading 83 | if (isPathsEqual(location, current.location)) { 84 | loadingContext.done(); 85 | 86 | if (isSearchDifferent(location, current.location)) 87 | setCurrent({ 88 | location: { ...location }, 89 | navigationType 90 | }); 91 | } 92 | }, [location]); 93 | 94 | // when loading is done 95 | useEffect(() => { 96 | if (!isCurrentlyLoading && isPathsDifferent(current.location, next.location)) 97 | setCurrent(next); 98 | }, [isCurrentlyLoading]); 99 | 100 | // setTimeout if maxLoadingTime is provided 101 | useEffect(() => { 102 | if (maxLoadingTime > 0) { 103 | if (timeout.current) { 104 | clearTimeout(timeout.current); 105 | timeout.current = undefined; 106 | } 107 | 108 | if (isPathsDifferent(current.location, next.location)) { 109 | timeout.current = setTimeout(() => { 110 | loadingContext.done(); 111 | }, maxLoadingTime); 112 | } 113 | } 114 | }, [current, next]); 115 | 116 | // memo current and next components 117 | return useMemo( 118 | () => <> 119 | {/* current */} 120 | { 121 | current.location.pathname !== LOADING_PATHNAME 122 | ? 128 | : LoadingScreen 129 | ? 130 | : 131 | } 132 | 133 | {/* hidden next */} 134 | { 135 | isPathsDifferent(current.location, next.location) && 136 |