├── .babelrc ├── .eslintrc ├── .fixpackrc ├── .gitignore ├── .jscsrc ├── AUTHORS ├── README.md ├── package-lock.json ├── package.json └── src ├── ReduxAsyncLoaderContext.js ├── actions.js ├── asyncLoader.js ├── computeChangedRoutes.js ├── deferLoader.js ├── flattenComponents.js ├── index.js ├── loadAsync.js ├── loadOnServer.js ├── names.js ├── reducer.js └── useAsyncLoader.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "ie": "11", 8 | "ios": "10" 9 | }, 10 | "useBuiltIns": false 11 | } 12 | ], 13 | "@babel/react" 14 | ], 15 | "env": { 16 | "development": { 17 | "presets": [ 18 | [ 19 | "@babel/env", 20 | { 21 | "targets": { 22 | "ie": "11", 23 | "ios": "10" 24 | } 25 | } 26 | ], 27 | "@babel/react", 28 | "power-assert" 29 | ] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module", 7 | "ecmaFeatures": { 8 | "jsx": true 9 | } 10 | }, 11 | 12 | "parser": "babel-eslint", 13 | 14 | "env": { 15 | "browser": true, 16 | "node": true, 17 | "mocha": true, 18 | "es6": true 19 | }, 20 | 21 | "plugins": [ 22 | "import", 23 | "react", 24 | "dependencies" 25 | ], 26 | 27 | "globals": { 28 | "__CLIENT__": true, 29 | "__SERVER__": true, 30 | "__DEVELOPMENT__": true, 31 | "__DISABLE_SSR__": true 32 | }, 33 | 34 | "rules": { 35 | //Possible Errors 36 | "no-console": "error", 37 | "no-unexpected-multiline": "error", 38 | 39 | // Best Practices 40 | "class-methods-use-this": "off", 41 | "consistent-return": ["error", { "treatUndefinedAsUnspecified": true }], 42 | "dot-location": ["error", "property"], 43 | "no-implicit-globals": "error", 44 | "no-invalid-this": "error", 45 | "no-param-reassign": ["error", { "props": false }], 46 | "no-unmodified-loop-condition": "error", 47 | "no-useless-call": "error", 48 | "no-void": "off", 49 | 50 | // Variables 51 | "no-catch-shadow": "error", 52 | "no-label-var": "error", 53 | "no-shadow": ["error", { "allow": ["cb", "next", "req", "res", "err", "error"] }], 54 | "no-undef-init": "error", 55 | "no-undefined": "error", 56 | "no-use-before-define": ["error", "nofunc"], 57 | "no-unused-expressions": ["error", { "allowShortCircuit": true }], 58 | "no-unused-vars": ["error", { "args": "none" }], 59 | 60 | // Node.js 61 | "callback-return": "error", 62 | "no-path-concat": "error", 63 | 64 | // Stylistic Issues 65 | "comma-dangle": ["error", "always-multiline"], 66 | "linebreak-style": ["error", "unix"], 67 | "no-plusplus": "off", 68 | 69 | // ECMAScript 6 70 | "arrow-parens": ["error", "always"], 71 | "constructor-super": "error", 72 | "generator-star-spacing": ["error", "after"], 73 | "no-this-before-super": "error", 74 | "prefer-arrow-callback": ["error", { "allowNamedFunctions": true }], 75 | "prefer-spread": "error", 76 | "prefer-template": "off", 77 | 78 | // React 79 | "react/forbid-prop-types": "off", 80 | "react/no-danger": "error", 81 | "react/no-direct-mutation-state": "error", 82 | "react/no-set-state": "error", 83 | "react/no-unused-prop-types": "off", 84 | "react/prefer-stateless-function": "off", 85 | "react/prop-types": "off", 86 | "react/self-closing-comp": "off", 87 | 88 | // JSX 89 | "react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx"] }], 90 | "react/jsx-key": "error", 91 | "react/jsx-max-props-per-line": ["error", {"maximum": 3}], 92 | 93 | // dependencies 94 | "dependencies/case-sensitive": "error", 95 | "dependencies/no-cycles": "error", 96 | "dependencies/no-unresolved": "error", 97 | 98 | // coding styles 99 | "max-len": ["error", 100] 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /.fixpackrc: -------------------------------------------------------------------------------- 1 | { 2 | "sortToTop": [ 3 | "name", 4 | "version", 5 | "description", 6 | "keywords", 7 | "author", 8 | "contributors", 9 | "license", 10 | "homepage", 11 | "bugs", 12 | "repository", 13 | "main", 14 | "bin", 15 | "man", 16 | "files", 17 | "directories", 18 | "scripts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Transpiled source 2 | lib 3 | 4 | ### Node 5 | # Logs 6 | logs 7 | *.log 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build 28 | 29 | # Dependency directory 30 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 31 | node_modules 32 | 33 | ### OSX 34 | .DS_Store 35 | .AppleDouble 36 | .LSOverride 37 | 38 | # Icon must end with two \r 39 | Icon 40 | 41 | # Thumbnails 42 | ._* 43 | 44 | # Files that might appear in the root of a volume 45 | .DocumentRevisions-V100 46 | .fseventsd 47 | .Spotlight-V100 48 | .TemporaryItems 49 | .Trashes 50 | .VolumeIcon.icns 51 | 52 | # Directories potentially created on remote AFP share 53 | .AppleDB 54 | .AppleDesktop 55 | Network Trash Folder 56 | Temporary Items 57 | .apdisk 58 | 59 | ### Windows 60 | # Windows image file caches 61 | Thumbs.db 62 | ehthumbs.db 63 | 64 | # Folder config file 65 | Desktop.ini 66 | 67 | # Recycle Bin used on file shares 68 | $RECYCLE.BIN/ 69 | 70 | # Windows Installer files 71 | *.cab 72 | *.msi 73 | *.msm 74 | *.msp 75 | 76 | # Windows shortcuts 77 | *.lnk 78 | 79 | ### Jetbrain's IDE 80 | .idea/ 81 | 82 | # File-based project format: 83 | *.ipr 84 | *.iws 85 | *.iml 86 | 87 | ## Plugin-specific files: 88 | 89 | # IntelliJ 90 | out/ 91 | 92 | # mpeltonen/sbt-idea plugin 93 | .idea_modules/ 94 | 95 | # JIRA plugin 96 | atlassian-ide-plugin.xml 97 | 98 | # Crashlytics plugin (for Android Studio and IntelliJ) 99 | com_crashlytics_export_strings.xml 100 | crashlytics.properties 101 | crashlytics-build.properties 102 | 103 | # Webpack Isomorphic Tools 104 | webpack-assets.json 105 | webpack-stats.json 106 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "airbnb", 3 | "maximumLineLength": 120, 4 | "requirePaddingNewLinesAfterBlocks": { 5 | "allExcept": ["inCallExpressions", "inNewExpressions", "inArrayExpressions", "inProperties"] 6 | }, 7 | "requireSpread": true 8 | } 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Koichi Kobayashi 2 | 渡辺貴明 3 | Yoshiya Hinosawa 4 | Yosuke Furukawa -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-async-loader 2 | 3 | Async data loader for Redux apps with React-Router. 4 | 5 | ## CAUTION 6 | 7 | `redux-async-loader` is incompatible with `react-redux@5` 8 | 9 | For `react-redux@5`, you neeed to use the 1.x release of redux-async-loader. 10 | 11 | ## Installation 12 | 13 | ``` 14 | npm install --save redux-async-loader 15 | ``` 16 | 17 | ## Usage 18 | 19 | ### 1. Register Reducer 20 | 21 | ```javascript 22 | import { combineReducers, createStore } from 'redux'; 23 | import { reduxAsyncLoader } from 'redux-async-loader'; 24 | 25 | const store = createStore(combineReducers({ 26 | reduxAsyncLoader, 27 | ... 28 | }), initialState); 29 | ``` 30 | 31 | ### 2. Server-Side Rendering (Optional) 32 | 33 | ```javascript 34 | import { applyRouterMiddleware, match, RouterContext } from 'react-router'; 35 | import { loadOnServer } from 'redux-async-loader'; 36 | 37 | match({ history, routes }, (error, redirectLocation, renderProps) => { 38 | // ... 39 | 40 | loadOnServer(renderProps, store).then(() => { 41 | const content = renderToString( 42 | 43 | 44 | 45 | ); 46 | }); 47 | }); 48 | ``` 49 | 50 | ### 3. Client-Side Rendering 51 | 52 | ```javascript 53 | import { render } from 'react-dom'; 54 | import { Router, applyRouterMiddleware, browserHistory } from 'react-router'; 55 | import { useAsyncLoader } from 'redux-async-loader'; 56 | 57 | const RenderWithMiddleware = applyRouterMiddleware( 58 | useAsyncLoader(), 59 | ); 60 | 61 | render( 62 | 63 | } /> 64 | 65 | , el); 66 | ``` 67 | 68 | If you are using 69 | [react-router-scroll](https://github.com/taion/react-router-scroll), 70 | it should be applied *after* redux-async-loader. 71 | 72 | ```javascript 73 | import useScroll from 'react-router-scroll'; 74 | 75 | const RenderWithMiddleware = applyRouterMiddleware( 76 | useAsyncLoader(), 77 | useScroll() 78 | ); 79 | ``` 80 | 81 | ### 4. Async data loading by routing (both client and server-side rendering) 82 | 83 | ```javascript 84 | import { connect } from 'react-redux'; 85 | import { asyncLoader } from 'redux-async-loader'; 86 | 87 | class UserList extends React.Component { 88 | // ... 89 | } 90 | 91 | export default asyncLoader((props, store) => store.dispatch(loadUsers(props)))( 92 | connect({ ... }, { ... })( 93 | UserList 94 | ) 95 | ); 96 | ``` 97 | 98 | or, with 99 | [decorator](https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy): 100 | 101 | ```javascript 102 | @asyncLoader((props, store) => store.dispatch(loadUsers(props))) 103 | @connect({ ... }, { ... }) 104 | export default class UserList extends React.Component { 105 | // ... 106 | } 107 | ``` 108 | 109 | or, with 110 | [recompose](https://github.com/acdlite/recompose): 111 | 112 | ```javascript 113 | import { compose } from 'recompose'; 114 | 115 | export default compose( 116 | asyncLoader((props, store) => store.dispatch(loadUsers(props))), 117 | connect({ ... }, { ... }) 118 | )(class UserList exptends React.Component { 119 | // ... 120 | }); 121 | ``` 122 | 123 | Unlike 124 | [redux-async-connect](https://www.npmjs.com/package/redux-async-connect), 125 | redux-async-loader itself doesn't connect to store. 126 | You have to call 127 | [connect()](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) 128 | explicitly if you want to use store's state. 129 | 130 | If you want to invoke `asyncLoader()` when just querystring (not path) is changed, you must specify key names of querystring to router. 131 | 132 | ``` 133 | 134 | ``` 135 | 136 | Or, you can use the wildcard for any keys of querystring: 137 | 138 | ``` 139 | 140 | ``` 141 | 142 | ### 5. Async data loading by mounting/updating (client-side rendering only) 143 | 144 | ```javascript 145 | import { connect } from 'react-redux'; 146 | import { deferLoader } from 'redux-async-loader'; 147 | 148 | class UserList extends React.Component { 149 | // ... 150 | } 151 | 152 | export default deferLoader((props, store) => store.dispatch(loadUsers(props)))( 153 | connect({ ... }, { ... })( 154 | UserList 155 | ) 156 | ); 157 | ``` 158 | 159 | or, with 160 | [decorator](https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy): 161 | 162 | ```javascript 163 | @deferLoader((props, store) => store.dispatch(loadUsers(props))) 164 | @connect({ ... }, { ... }) 165 | export default class UserList extends React.Component { 166 | // ... 167 | } 168 | ``` 169 | 170 | or, with 171 | [recompose](https://github.com/acdlite/recompose): 172 | 173 | ```javascript 174 | import { compose } from 'recompose'; 175 | 176 | export default compose( 177 | deferLoader((props, store) => store.dispatch(loadUsers(props))), 178 | connect({ ... }, { ... }) 179 | )(class UserList exptends React.Component { 180 | // ... 181 | }); 182 | ``` 183 | 184 | ## API 185 | 186 | #### `asyncLoader(loader)` 187 | 188 | Creates Higher-order Component for async data loading by routing. 189 | 190 | ##### Arguments 191 | 192 | * `loader` *(Function)*: Called when this component is routed. 193 | * Arguments 194 | * `props` *(Object)*: The props passed from React-Router. 195 | See 196 | [React-Router API docs](https://github.com/reactjs/react-router/blob/master/docs/API.md#proptypes) 197 | for more info. 198 | * `store`: *(Object)*: Redux's store object. 199 | * Returns 200 | * *(Promise)*: Fulfilled when data loading is completed. 201 | 202 | ##### Returns 203 | 204 | * *(Function)*: Creates higher-order component. 205 | * Arguments 206 | * wrappedComponent *(Component)*: Wrapped component. 207 | * Returns 208 | * *(Component)*: Wrapper component. 209 | 210 | #### `deferLoader(loader)` 211 | 212 | Creates Higher-order Component for async data loading by mounting and updating. 213 | 214 | ##### Arguments 215 | 216 | * `loader` *(Function)*: Called when this component is mounted or updated. 217 | * Arguments 218 | * `props` *(Object)*: The props passed from parent component. 219 | * `store`: *(Object)*: Redux's store object. 220 | * Returns 221 | * *(Promise)*: Fulfilled when data loading is completed. 222 | 223 | ##### Returns 224 | 225 | * *(Function)*: Creates higher-order component. 226 | * Arguments 227 | * wrappedComponent *(Component)*: Wrapped component. 228 | * Returns 229 | * *(Component)*: Wrapper component. 230 | 231 | #### `loadOnServer(renderProps, store)` 232 | 233 | Loads async data on the server side. 234 | 235 | ##### Arguments 236 | 237 | * `renderProps` *(Object)*: The props passed via `match()`'s callback. 238 | See 239 | [React-Router API docs](https://github.com/reactjs/react-router/blob/master/docs/API.md#match-routes-location-history-options--cb) 240 | for more info. 241 | * `store` *(Object)*: Redux's store object. 242 | 243 | ##### Returns 244 | 245 | * *(Promise)*: Fulfilled when data loading is completed. 246 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@recruit-tech/redux-async-loader", 3 | "version": "2.0.1", 4 | "description": "Async data loader for Redux apps.", 5 | "keywords": [ 6 | "async", 7 | "data", 8 | "loading", 9 | "react", 10 | "react-router", 11 | "redux" 12 | ], 13 | "author": "Recruit Technologies Co.,Ltd.", 14 | "license": "MIT", 15 | "homepage": "https://github.com/recruit-tech/redux-async-loader", 16 | "bugs": { 17 | "url": "https://github.com/recruit-tech/redux-async-loader/issues" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/recruit-tech/redux-async-loader.git" 22 | }, 23 | "main": "lib/index.js", 24 | "files": [ 25 | "lib", 26 | "src" 27 | ], 28 | "scripts": { 29 | "build": "babel src --out-dir lib", 30 | "clean": "rimraf lib/*", 31 | "fixpack": "fixpack", 32 | "lint": "eslint src", 33 | "preversion": "npm-run-all lint", 34 | "version": "npm-run-all clean build" 35 | }, 36 | "dependencies": { 37 | "hoist-non-react-statics": "^1.0.0", 38 | "prop-types": "^15.0.0" 39 | }, 40 | "devDependencies": { 41 | "@babel/cli": "^7.7.4", 42 | "@babel/core": "^7.7.4", 43 | "@babel/preset-env": "^7.7.4", 44 | "@babel/preset-react": "^7.7.4", 45 | "babel-eslint": "7.2.3", 46 | "babel-preset-power-assert": "^3.0.0", 47 | "eslint": "4.18.2", 48 | "eslint-config-airbnb": "14.1.0", 49 | "eslint-plugin-dependencies": "2.3.0", 50 | "eslint-plugin-import": "2.2.0", 51 | "eslint-plugin-jsx-a11y": "4.0.0", 52 | "eslint-plugin-react": "6.10.3", 53 | "fixpack": "2.3.1", 54 | "npm-run-all": "4.0.2", 55 | "react": "15.5.4", 56 | "react-redux": "^7.1.3", 57 | "react-router": "3.0.5", 58 | "rimraf": "2.6.1" 59 | }, 60 | "peerDependencies": { 61 | "react": "^16.0.0", 62 | "react-redux": "^7.0.0", 63 | "react-router": "^2.3.0 || ^3.0.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ReduxAsyncLoaderContext.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-set-state */ 2 | 3 | /* 4 | * A part of these functions are: 5 | * Copyright (c) 2015 Ryan Florence 6 | * Released under the MIT license. 7 | * https://github.com/ryanflorence/async-props/blob/master/LICENSE.md 8 | */ 9 | 10 | import { Component } from 'react'; 11 | import PropTypes from 'prop-types'; 12 | import computeChangedRoutes from './computeChangedRoutes'; 13 | import { beginAsyncLoad, endAsyncLoad, skipAsyncLoad } from './actions'; 14 | import flattenComponents from './flattenComponents'; 15 | import loadAsync from './loadAsync'; 16 | import { reducerName } from './names'; 17 | 18 | class ReduxAsyncLoaderContext extends Component { 19 | constructor(props) { 20 | super(props); 21 | 22 | this.state = { 23 | children: null, 24 | }; 25 | this.loadCount = 0; 26 | } 27 | 28 | componentDidMount() { 29 | const { loading, loaded, onServer } = this.getAsyncLoaderState(); 30 | if (loading) { 31 | return; 32 | } 33 | 34 | if (loaded && onServer) { 35 | const { dispatch } = this.props.ctx.store; 36 | dispatch(skipAsyncLoad(false)); 37 | return; 38 | } 39 | 40 | this.loadAsync(this.props); 41 | } 42 | 43 | componentWillReceiveProps(nextProps) { 44 | if (nextProps.location === this.props.location) { 45 | return; 46 | } 47 | 48 | const enterRoutes = computeChangedRoutes( 49 | { 50 | routes: this.props.routes, 51 | params: this.props.params, 52 | location: this.props.location, 53 | }, 54 | { 55 | routes: nextProps.routes, 56 | params: nextProps.params, 57 | location: nextProps.location, 58 | } 59 | ); 60 | 61 | const indexDiff = nextProps.components.length - enterRoutes.length; 62 | const components = enterRoutes.map( 63 | (_route, index) => nextProps.components[indexDiff + index] 64 | ); 65 | 66 | this.loadAsync(Object.assign({}, nextProps, { components })); 67 | } 68 | 69 | shouldComponentUpdate() { 70 | const { loading } = this.getAsyncLoaderState(); 71 | return !loading; 72 | } 73 | 74 | getAsyncLoaderState() { 75 | const { getAsyncLoaderState } = this.props; 76 | const { getState } = this.props.ctx.store; 77 | return getAsyncLoaderState(getState()); 78 | } 79 | 80 | loadAsync(props) { 81 | const { children, components } = props; 82 | 83 | const flattened = flattenComponents(components); 84 | if (!flattened.length) { 85 | return; 86 | } 87 | 88 | const { store } = this.props.ctx; 89 | const { dispatch } = store; 90 | this.beginLoad(dispatch, children) 91 | .then(() => loadAsync(flattened, props, store)) 92 | .then( 93 | () => this.endLoad(dispatch), 94 | (error) => this.endLoad(dispatch, error) 95 | ); 96 | } 97 | 98 | beginLoad(dispatch, children) { 99 | if (this.loadCount === 0) { 100 | dispatch(beginAsyncLoad()); 101 | } 102 | 103 | ++this.loadCount; 104 | return new Promise((resolve) => { 105 | this.setState({ children }, () => resolve()); 106 | }); 107 | } 108 | 109 | endLoad(dispatch, error) { 110 | if (error) { 111 | this.props.onError(error); 112 | } 113 | 114 | --this.loadCount; 115 | if (this.loadCount === 0) { 116 | dispatch(endAsyncLoad()); 117 | this.setState({ children: null }); 118 | } 119 | } 120 | 121 | render() { 122 | const { loading } = this.getAsyncLoaderState(); 123 | 124 | return loading ? this.state.children : this.props.children; 125 | } 126 | } 127 | 128 | ReduxAsyncLoaderContext.propTypes = { 129 | children: PropTypes.node.isRequired, 130 | components: PropTypes.array.isRequired, 131 | params: PropTypes.object.isRequired, 132 | location: PropTypes.object.isRequired, 133 | getAsyncLoaderState: PropTypes.func, 134 | onError: PropTypes.func, 135 | }; 136 | 137 | ReduxAsyncLoaderContext.defaultProps = { 138 | getAsyncLoaderState(state) { 139 | return state[reducerName]; 140 | }, 141 | onError(_error) { 142 | // ignore 143 | }, 144 | }; 145 | 146 | export default ReduxAsyncLoaderContext; 147 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | export const BEGIN_ASYNC_LOAD = 'redux-async-loader/load/begin'; 2 | export const END_ASYNC_LOAD = 'redux-async-loader/load/end'; 3 | export const SKIP_ASYNC_LOAD = 'redux-async-loader/load/skip'; 4 | 5 | export function beginAsyncLoad(onServer = false) { 6 | return { 7 | type: BEGIN_ASYNC_LOAD, 8 | payload: { 9 | onServer, 10 | }, 11 | }; 12 | } 13 | 14 | export function endAsyncLoad(onServer = false) { 15 | return { 16 | type: END_ASYNC_LOAD, 17 | payload: { 18 | onServer, 19 | }, 20 | }; 21 | } 22 | 23 | export function skipAsyncLoad(onServer = false) { 24 | return { 25 | type: SKIP_ASYNC_LOAD, 26 | payload: { 27 | onServer, 28 | }, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/asyncLoader.js: -------------------------------------------------------------------------------- 1 | import { loadAsyncPropertyName } from './names'; 2 | 3 | export default function asyncLoader(loader) { 4 | return (Component) => { 5 | Component[loadAsyncPropertyName] = loader; 6 | return Component; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/computeChangedRoutes.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A part of these functions are: 3 | * Copyright (c) 2015-present, Ryan Florence, Michael Jackson 4 | * Released under the MIT license. 5 | * https://github.com/reactjs/react-router/blob/master/LICENSE.md 6 | */ 7 | 8 | import { getParamNames } from 'react-router/lib/PatternUtils'; 9 | 10 | export default function computeChangedRoutes(prevState, nextState) { 11 | const prevRoutes = prevState && prevState.routes; 12 | const nextRoutes = nextState.routes; 13 | 14 | if (!prevRoutes) { 15 | return nextRoutes; 16 | } 17 | 18 | const leaveIndex = prevRoutes.findIndex((route) => ( 19 | nextRoutes.indexOf(route) === -1 || 20 | routeParamsChanged(route, prevState, nextState) || 21 | queryParamsChanged(route, prevState, nextState) || 22 | routeChanged(route, prevState, nextState) 23 | )); 24 | const leaveRoutes = leaveIndex === -1 ? [] : prevRoutes.slice(leaveIndex); 25 | 26 | return nextRoutes.filter((route) => { 27 | const isNew = prevRoutes.indexOf(route) === -1; 28 | const paramsChanged = leaveRoutes.indexOf(route) !== -1; 29 | 30 | return isNew || paramsChanged; 31 | }); 32 | } 33 | 34 | function routeParamsChanged(route, prevState, nextState) { 35 | if (!route.path) { 36 | return false; 37 | } 38 | 39 | const paramNames = getParamNames(route.path); 40 | 41 | return paramNames.some((paramName) => 42 | prevState.params[paramName] !== nextState.params[paramName] 43 | ); 44 | } 45 | 46 | function queryParamsChanged(route, prevState, nextState) { 47 | const queryKeys = (route.asyncLoaderProps && route.asyncLoaderProps.queryKeys) || route.queryKeys; 48 | if (!queryKeys) { 49 | return false; 50 | } 51 | 52 | const prevQuery = prevState.location.query; 53 | const nextQuery = nextState.location.query; 54 | 55 | if (queryKeys === '*') { 56 | const prevQueryKeys = Object.keys(prevQuery); 57 | const nextQueryKeys = Object.keys(nextQuery); 58 | 59 | return prevQueryKeys.length !== nextQueryKeys.length || 60 | prevQueryKeys.some((key) => prevQuery[key] !== nextQuery[key]); 61 | } 62 | 63 | const keys = queryKeys.split(/[, ]+/); 64 | return keys.some((key) => prevQuery[key] !== nextQuery[key]); 65 | } 66 | 67 | function routeChanged(route, prevState, nextState) { 68 | const changed = route.asyncLoaderProps && route.asyncLoaderProps.routeChanged; 69 | return changed && changed(route, prevState, nextState); 70 | } 71 | -------------------------------------------------------------------------------- /src/deferLoader.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { ReactReduxContext } from 'react-redux'; 3 | import hoistStatics from 'hoist-non-react-statics'; 4 | 5 | export default function deferLoader(loader) { 6 | return (WrappedComponent) => { 7 | class WrapperComponent extends Component { 8 | componentDidMount() { 9 | const { store } = this.props.ctx; 10 | loader(this.props, store); 11 | } 12 | 13 | componentWillReceiveProps(nextProps) { 14 | const { store } = this.props.ctx; 15 | loader(nextProps, store); 16 | } 17 | 18 | render() { 19 | return ; 20 | } 21 | } 22 | 23 | WrapperComponent.displayName = `deferLoader(${getDisplayName( 24 | WrappedComponent 25 | )})`; 26 | 27 | const WrapperComponentWithContext = (props) => ( 28 | 29 | {({ store }) => } 30 | 31 | ); 32 | 33 | return hoistStatics(WrapperComponentWithContext, WrappedComponent); 34 | }; 35 | } 36 | 37 | function getDisplayName(component) { 38 | return component.displayName || component.name; 39 | } 40 | -------------------------------------------------------------------------------- /src/flattenComponents.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A part of these functions are: 3 | * Copyright (c) 2015 Ryan Florence 4 | * Released under the MIT license. 5 | * https://github.com/ryanflorence/async-props/blob/master/LICENSE.md 6 | */ 7 | 8 | import { loadAsyncPropertyName } from './names'; 9 | 10 | // based on https://github.com/ryanflorence/async-props/blob/v0.3.2/modules/AsyncProps.js#L8-L18 11 | function eachComponents(components, cb) { 12 | for (let i = 0, l = components.length; i < l; i++) { 13 | const component = components[i]; 14 | if (typeof component === 'object') { 15 | // named components 16 | // https://github.com/reactjs/react-router/blob/master/docs/API.md#named-components 17 | Object.keys(component).forEach((key) => cb(component[key], i, key)); 18 | } else { 19 | cb(component, i); // eslint-disable-line callback-return 20 | } 21 | } 22 | } 23 | 24 | // based on https://github.com/ryanflorence/async-props/blob/v0.3.2/modules/AsyncProps.js#L20-L27 25 | export default function flattenComponents(components) { 26 | const flattened = []; 27 | eachComponents(components, (Component) => { 28 | if (Component && Component[loadAsyncPropertyName]) { 29 | flattened.push(Component); 30 | } 31 | }); 32 | return flattened; 33 | } 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | export { default as asyncLoader } from './asyncLoader'; 3 | export { default as deferLoader } from './deferLoader'; 4 | export { default as loadOnServer } from './loadOnServer'; 5 | export { default as reduxAsyncLoader } from './reducer'; 6 | export { default as useAsyncLoader } from './useAsyncLoader'; 7 | export { reducerName } from './names'; 8 | -------------------------------------------------------------------------------- /src/loadAsync.js: -------------------------------------------------------------------------------- 1 | import { loadAsyncPropertyName } from './names'; 2 | 3 | export default function loadAsync(components, props, store) { 4 | return Promise.all(components.map((component) => component[loadAsyncPropertyName](props, store))); 5 | } 6 | -------------------------------------------------------------------------------- /src/loadOnServer.js: -------------------------------------------------------------------------------- 1 | import { beginAsyncLoad, endAsyncLoad, skipAsyncLoad } from './actions'; 2 | import flattenComponents from './flattenComponents'; 3 | import loadAsync from './loadAsync'; 4 | 5 | export default function loadOnServer(renderProps, store) { 6 | const { dispatch } = store; 7 | 8 | const flattened = flattenComponents(renderProps.components); 9 | if (!flattened.length) { 10 | dispatch(skipAsyncLoad(true)); 11 | return Promise.resolve(); 12 | } 13 | 14 | dispatch(beginAsyncLoad(true)); 15 | return loadAsync(flattened, renderProps, store).then( 16 | (v) => { 17 | dispatch(endAsyncLoad(true)); 18 | return v; 19 | }, 20 | (e) => { 21 | dispatch(endAsyncLoad(true)); 22 | return Promise.reject(e); 23 | } 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/names.js: -------------------------------------------------------------------------------- 1 | export const loadAsyncPropertyName = 'redux-async-loader/loadAsync'; 2 | 3 | export const reducerName = 'reduxAsyncLoader'; 4 | -------------------------------------------------------------------------------- /src/reducer.js: -------------------------------------------------------------------------------- 1 | import { BEGIN_ASYNC_LOAD, END_ASYNC_LOAD, SKIP_ASYNC_LOAD } from './actions'; 2 | 3 | const INITIAL_STATE = { 4 | loading: false, 5 | loaded: false, 6 | onServer: false, 7 | }; 8 | 9 | export default function reducer(state = INITIAL_STATE, { type, payload } = {}) { 10 | switch (type) { 11 | case BEGIN_ASYNC_LOAD: 12 | return { 13 | loading: true, 14 | loaded: false, 15 | onServer: !!payload.onServer, 16 | }; 17 | case END_ASYNC_LOAD: 18 | case SKIP_ASYNC_LOAD: 19 | return { 20 | loading: false, 21 | loaded: true, 22 | onServer: !!payload.onServer, 23 | }; 24 | default: 25 | return state; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/useAsyncLoader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ReactReduxContext } from 'react-redux'; 3 | import ReduxAsyncLoaderContext from './ReduxAsyncLoaderContext'; 4 | 5 | const WrapedReduxAsyncLoaderContext = ({ child, renderProps }) => ( 6 | 7 | {({ store }) => ( 8 | 9 | {child} 10 | 11 | )} 12 | 13 | ); 14 | 15 | export default function useAsyncLoader() { 16 | return { 17 | renderRouterContext: (child, renderProps) => ( 18 | 19 | ), 20 | }; 21 | } 22 | --------------------------------------------------------------------------------