├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── circle.yml
├── package-lock.json
├── package.json
└── src
├── PropTypes.js
├── RouterHookContainer.js
├── RouterHookContext.js
├── constants.js
├── getAllComponents.js
├── getInitStatus.js
├── index.js
├── routerHooks.js
├── triggerHooksOnServer.js
└── useRouterHook.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "env",
4 | "react",
5 | "stage-0"
6 | ],
7 | "plugins": [
8 | "transform-runtime"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | indent_style = space
3 | end_of_line = lf
4 | indent_size = 2
5 | charset = utf-8
6 | trim_trailing_whitespace = true
7 |
8 | [Makefile]
9 | indent_style = tab
10 | indent_size = 8
11 |
12 | [*.md]
13 | max_line_length = 0
14 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | lib
2 | dist
3 | node_modules
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "babel-eslint",
4 | "extends": "eslint-config-airbnb",
5 | "env": {
6 | "browser": true,
7 | "node": true,
8 | "mocha": true
9 | },
10 | "plugins": [
11 | "react",
12 | "import",
13 | "babel"
14 | ],
15 | "rules": {
16 | "operator-linebreak": [
17 | 2,
18 | "after",
19 | {
20 | "overrides": {
21 | "?": "before",
22 | ":": "before"
23 | }
24 | }
25 | ],
26 | "react/jsx-boolean-value": [
27 | 2,
28 | "always"
29 | ],
30 | "react/prefer-stateless-function": 0,
31 | "react/forbid-prop-types": 0,
32 | "react/jsx-handler-names": [
33 | 2,
34 | {
35 | "eventHandlerPrefix": "handle",
36 | "eventHandlerPropPrefix": "on"
37 | }
38 | ],
39 | "react/jsx-no-duplicate-props": [
40 | 2,
41 | {
42 | "ignoreCase": false
43 | }
44 | ],
45 | "react/jsx-sort-props": [
46 | 2,
47 | {
48 | "callbacksLast": true,
49 | "shorthandFirst": false,
50 | "ignoreCase": true
51 | }
52 | ],
53 | "react/sort-prop-types": [
54 | 2,
55 | {
56 | "ignoreCase": true,
57 | "callbacksLast": true
58 | }
59 | ],
60 | "babel/object-curly-spacing": [
61 | 2,
62 | "always"
63 | ],
64 | "import/no-extraneous-dependencies": 0,
65 | "react/jsx-filename-extension": 0
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | .DS_Store
4 | dist
5 | lib
6 | .tern-port
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 - 2017 HOU Bin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | react-router-hook
2 | =========================
3 |
4 | Universal data fetching and lifecycle management for react-router@3 with multiple components. Inspired by [redial](https://github.com/markdalgleish/redial), [react-router-redial](https://github.com/dlmr/react-router-redial) and [async props](https://github.com/ryanflorence/async-props).
5 |
6 | [](https://circleci.com/gh/kouhin/react-router-hook)
7 | [](https://david-dm.org/kouhin/react-router-hook)
8 |
9 | ## Installation
10 |
11 | ```
12 | npm install --save react-router-hook
13 | ```
14 |
15 | ## Usage
16 |
17 | ```javascript
18 | import { browserHistory, Router, applyRouterMiddleware } from 'react-router';
19 | import { useRouterHook, routerHooks } from 'react-router-hook';
20 |
21 | const locals = {
22 | dispatch: store.dispatch, // redux store and dispatch, you can use any locals
23 | getState: store.getState,
24 | };
25 |
26 | const onAborted = () => {
27 | console.info('aborted');
28 | };
29 | const onCompleted = () => {
30 | console.info('completed');
31 | };
32 | const onError = (error) => {
33 | console.error(error);
34 | };
35 |
36 | const routerHookMiddleware = useRouterHook({
37 | locals,
38 | routerWillEnterHooks: ['fetch'],
39 | routerDidEnterHooks: ['defer', 'done'],
40 | onAborted,
41 | onStarted,
42 | onCompleted,
43 | onError,
44 | });
45 |
46 | ReactDOM.render((
47 |
51 |
52 |
53 |
54 |
55 | ), node)
56 | class App extends React.Component {
57 | render() {
58 | // the matched child route components become props in the parent
59 | return (
60 |
61 |
62 | {/* this will either be or */}
63 | {this.props.main}
64 |
65 |
66 | {/* this will either be or */}
67 | {this.props.footer}
68 |
69 |
70 | )
71 | }
72 | }
73 |
74 | @routerHooks({
75 | fetch: async () => {
76 | await fetchData();
77 | },
78 | defer: async () => {
79 | await fetchDeferredData();
80 | },
81 | })
82 | class Users extends React.Component {
83 | render() {
84 | return (
85 |
86 | {/* if at "/users/123" this will be
*/}
87 | {/* UsersSidebar will also get as this.props.children.
88 | You can pick where it renders */}
89 | {this.props.children}
90 |
91 | )
92 | }
93 | }
94 |
95 | @routerHook({
96 | fetch: async () => {
97 | await fetchData();
98 | },
99 | defer: async () => {
100 | await fetchDeferredData();
101 | },
102 | })
103 | class UserFooter extends React.Component {
104 | render() {
105 | return (
106 |
107 | UserFooter
108 |
109 | )
110 | }
111 | }
112 | ```
113 |
114 | ## On server side
115 |
116 | ``` javascript
117 |
118 | import { match } from 'react-router';
119 | import { triggerHooksOnServer } from 'react-router-hook';
120 | // Other imports
121 |
122 | import routes from './routes';
123 |
124 | app.get('*', (req, res) => {
125 | // create redux store (Optional);
126 | const store = createStore();
127 |
128 | match({
129 | history,
130 | routes,
131 | location: req.url,
132 | }, (err, redirectLocation, renderProps) => {
133 | if (err) {
134 | // Error Handler
135 | }
136 | const locals = {
137 | dispatch: store.dispatch,
138 | getState: store.getState,
139 | };
140 | triggerHooksOnServer(
141 | renderProps,
142 | ['fetch', 'defer'],
143 | {
144 | dispatch,
145 | getState,
146 | },
147 | // If onComponentError is null, callback will be immediately called with the error
148 | onComponentError: (err) => {
149 | console.error(err.Component, err.error);
150 | },
151 | // triggerHooksOnServer() will return a Promise if there is no callback
152 | (err) => {
153 | if (err) {
154 | res.status(500).end();
155 | return;
156 | }
157 | const body = ReactDOMServer.renderToString(
158 |
159 |
160 | ,
161 | );
162 | res.send(`${body}`);
163 | },
164 | });
165 | });
166 | triggerHooksOnServer
167 | ```
168 |
169 | ## Monitoring router status
170 |
171 | ``` javascript
172 |
173 | @routerHook({
174 | fetch: async () => {
175 | await fetchData();
176 | },
177 | defer: async () => {
178 | await fetchDeferredData();
179 | },
180 | })
181 | class SomeComponent extends React.Component{
182 |
183 | static contextTypes = {
184 | routerHookContext: routerHookContextShape,
185 | };
186 |
187 | constructor(props, context) {
188 | super(props, context);
189 | this.state = {
190 | routerLoading: false,
191 | };
192 | }
193 |
194 | componentWillMount() {
195 | if (this.context.routerHookContext) {
196 | this.removeListener = this.context.routerHookContext.addLoadingListener((loading, info) => {
197 | const { total, init, defer, done } = info;
198 | console.info(loading, total, init, defer, done);
199 | this.setState({
200 | routerLoading: loading,
201 | });
202 | });
203 | }
204 | }
205 |
206 | componentWillUnmount() {
207 | if (this.removeListener) {
208 | this.removeListener();
209 | }
210 | }
211 |
212 | render() {
213 | return (
214 |
215 | is loading: {this.state.routerLoading}
216 |
217 | );
218 | }
219 | }
220 | ```
221 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | node:
3 | version: 8
4 | dependencies:
5 | override:
6 | - sudo apt-get install jq
7 | - npm install
8 | test:
9 | override:
10 | - npm test
11 | deployment:
12 | production:
13 | branch: master
14 | commands:
15 | - git tag v`jq -r '.version' package.json`
16 | - git push origin --tags
17 | - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
18 | - npm publish
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-router-hook",
3 | "version": "0.7.1",
4 | "description": "Universal data fetching and lifecycle management for react router with multiple components",
5 | "main": "./lib/index.js",
6 | "scripts": {
7 | "build": "babel src --out-dir lib",
8 | "clean": "rimraf lib",
9 | "lint": "$(npm bin)/eslint src",
10 | "prepare": "npm run clean && npm run test && npm run build",
11 | "test": "$(npm bin)/eslint src"
12 | },
13 | "files": [
14 | "lib",
15 | "src"
16 | ],
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/kouhin/react-router-hook.git"
20 | },
21 | "keywords": [
22 | "react",
23 | "react-router",
24 | "hook",
25 | "async",
26 | "fetch",
27 | "react-component",
28 | "reactjs"
29 | ],
30 | "author": [
31 | "Bin Hou (https://twitter.com/houbin217jz)"
32 | ],
33 | "license": "MIT",
34 | "devDependencies": {
35 | "babel-cli": "^6.26.0",
36 | "babel-core": "^6.26.3",
37 | "babel-eslint": "^10.0.1",
38 | "babel-plugin-transform-runtime": "^6.23.0",
39 | "babel-preset-env": "^1.7.0",
40 | "babel-preset-react": "^6.24.1",
41 | "babel-preset-stage-0": "^6.24.1",
42 | "eslint": "^5.9.0",
43 | "eslint-config-airbnb-deps": "^16.1.0",
44 | "eslint-plugin-babel": "^5.3.0",
45 | "react": "^16.6.3",
46 | "react-dom": "^16.6.3",
47 | "rimraf": "^2.6.2"
48 | },
49 | "dependencies": {
50 | "babel-runtime": "^6.26.0",
51 | "eventemitter3": "^3.1.0",
52 | "lodash": "^4.17.11",
53 | "prop-types": "^15.6.2",
54 | "uuid": "^3.3.2"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/PropTypes.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | const {
4 | func,
5 | object,
6 | arrayOf,
7 | oneOfType,
8 | element,
9 | shape,
10 | string,
11 | } = PropTypes;
12 |
13 | export const componentShape = oneOfType([func, string]);
14 | export const componentsShape = oneOfType([componentShape, object]);
15 |
16 | export const routeShape = oneOfType([object, element]);
17 | export const routesShape = oneOfType([routeShape, arrayOf(routeShape)]);
18 |
19 | export const routerShape = shape({
20 | push: func.isRequired,
21 | replace: func.isRequired,
22 | go: func.isRequired,
23 | goBack: func.isRequired,
24 | goForward: func.isRequired,
25 | setRouteLeaveHook: func.isRequired,
26 | isActive: func.isRequired,
27 | });
28 |
29 | export const locationShape = shape({
30 | pathname: string.isRequired,
31 | search: string.isRequired,
32 | state: object,
33 | action: string.isRequired,
34 | key: string,
35 | });
36 |
37 |
38 | export const renderPropsShape = shape({
39 | location: locationShape,
40 | routes: routesShape,
41 | params: object,
42 | components: componentsShape,
43 | router: routerShape,
44 | });
45 |
46 | export const routerHookContextShape = shape({
47 | getComponentStatus: func,
48 | setComponentStatus: func,
49 | addLoadingListener: func,
50 | });
51 |
--------------------------------------------------------------------------------
/src/RouterHookContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { ComponentStatus, routerHookPropName } from './constants';
5 | import getInitStatus from './getInitStatus';
6 | import { renderPropsShape, routerHookContextShape } from './PropTypes';
7 |
8 | const ABORT = 'abort';
9 |
10 | export default class RouterHookContainer extends React.Component {
11 | static propTypes = {
12 | children: PropTypes.node.isRequired,
13 | locals: PropTypes.object.isRequired,
14 | renderProps: renderPropsShape.isRequired,
15 | routerDidEnterHooks: PropTypes.arrayOf(PropTypes.string).isRequired,
16 | routerWillEnterHooks: PropTypes.arrayOf(PropTypes.string).isRequired,
17 | }
18 |
19 | static contextTypes = {
20 | routerHookContext: routerHookContextShape,
21 | };
22 |
23 | constructor(props, context) {
24 | super(props, context);
25 | this.setStatus = this.setStatus.bind(this);
26 | this.reloadComponent = this.reloadComponent.bind(this);
27 |
28 | this.mounted = false;
29 | this.state = {
30 | status: getInitStatus(
31 | props.children.type,
32 | this.props.routerWillEnterHooks,
33 | ),
34 | };
35 | }
36 |
37 | componentWillMount() {
38 | this.context.routerHookContext.setComponentStatus(this.Component, getInitStatus(
39 | this.Component,
40 | this.props.routerWillEnterHooks,
41 | ));
42 | }
43 |
44 | componentDidMount() {
45 | this.mounted = true;
46 | this.reloadComponent(true);
47 | }
48 |
49 | componentWillReceiveProps(nextProps) {
50 | if (this.props.renderProps.location !== nextProps.renderProps.location) {
51 | this.context.routerHookContext.setComponentStatus(this.Component, getInitStatus(
52 | this.Component,
53 | this.props.routerWillEnterHooks,
54 | ));
55 | this.setState({
56 | status: getInitStatus(
57 | this.Component,
58 | this.props.routerWillEnterHooks,
59 | ),
60 | });
61 | }
62 | }
63 |
64 | componentDidUpdate(prevProps) {
65 | if (this.props.renderProps.location !== prevProps.renderProps.location) {
66 | this.reloadComponent(true);
67 | }
68 | }
69 |
70 | componentWillUnmount() {
71 | this.mounted = false;
72 | }
73 |
74 | get Component() {
75 | return this.props.children.type;
76 | }
77 |
78 | setStatus(status, shouldReport, err) {
79 | if (this.state.status === status || !this.mounted) {
80 | return;
81 | }
82 | this.setState({ status });
83 | if (shouldReport) {
84 | this.context.routerHookContext.setComponentStatus(this.Component, status, err);
85 | }
86 | }
87 |
88 | reloadComponent(shouldReportStatus = false) {
89 | if (!this.mounted) {
90 | return Promise.resolve();
91 | }
92 | const routerHooks = this.Component[routerHookPropName];
93 | if (!routerHooks) {
94 | return Promise.resolve();
95 | }
96 | const {
97 | locals,
98 | renderProps,
99 | routerDidEnterHooks,
100 | routerWillEnterHooks,
101 | } = this.props;
102 |
103 | const initStatus = getInitStatus(
104 | this.Component,
105 | routerWillEnterHooks,
106 | );
107 |
108 | const { location } = renderProps;
109 | const args = {
110 | ...renderProps,
111 | ...locals,
112 | };
113 |
114 | return Promise.resolve()
115 | .then(() => {
116 | const willEnterHooks = routerWillEnterHooks
117 | .map(key => routerHooks[key])
118 | .filter(f => f);
119 | if (willEnterHooks.length < 1) {
120 | return null;
121 | }
122 | this.setStatus(initStatus, shouldReportStatus);
123 | return willEnterHooks
124 | .reduce((total, hook) => total.then(() => {
125 | if (location !== renderProps.location || !this.mounted) {
126 | return Promise.reject(ABORT);
127 | }
128 | return hook(args);
129 | }), Promise.resolve());
130 | })
131 | .then(() => {
132 | if (location !== renderProps.location || !this.mounted) {
133 | return Promise.reject(ABORT);
134 | }
135 | const didEnterHooks = routerDidEnterHooks
136 | .map(key => routerHooks[key])
137 | .filter(f => f);
138 | if (didEnterHooks.length < 1) {
139 | return null;
140 | }
141 | if (this.state.status !== ComponentStatus.DEFER) {
142 | this.setStatus(ComponentStatus.DEFER, shouldReportStatus);
143 | }
144 | return didEnterHooks
145 | .reduce((total, hook) => total.then(() => {
146 | if (location !== renderProps.location || !this.mounted) {
147 | return Promise.reject(ABORT);
148 | }
149 | return hook(args);
150 | }), Promise.resolve());
151 | })
152 | .then(() => {
153 | if (location !== renderProps.location || !this.mounted) {
154 | return Promise.reject(ABORT);
155 | }
156 | if (this.state.status !== ComponentStatus.DONE) {
157 | this.setStatus(ComponentStatus.DONE, shouldReportStatus);
158 | }
159 | return null;
160 | })
161 | .catch((err) => {
162 | if (err === ABORT) {
163 | return;
164 | }
165 | this.setStatus(ComponentStatus.DONE, shouldReportStatus, err);
166 | });
167 | }
168 |
169 | render() {
170 | const {
171 | children,
172 | locals,
173 | renderProps,
174 | routerDidEnterHooks,
175 | routerWillEnterHooks,
176 | ...restProps
177 | } = this.props;
178 | const passProps = {
179 | ...restProps,
180 | componentStatus: this.state.status,
181 | reloadComponent: this.reloadComponent,
182 | };
183 |
184 | if (this.state.status === ComponentStatus.INIT) {
185 | if (!this.prevChildren) {
186 | return null;
187 | }
188 | return React.cloneElement(this.prevChildren, passProps);
189 | }
190 |
191 | this.prevChildren = React.cloneElement(children, passProps);
192 | return this.prevChildren;
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/src/RouterHookContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import EventEmitter from 'eventemitter3';
4 | import { ComponentStatus, routerHookPropName } from './constants';
5 | import getAllComponents from './getAllComponents';
6 | import { componentsShape, locationShape, routerHookContextShape } from './PropTypes';
7 |
8 | const CHANGE_LOADING_STATE = 'changeLoadingState';
9 | const canUseDOM = !!(
10 | typeof window !== 'undefined' &&
11 | window.document &&
12 | window.document.createElement
13 | );
14 | const noop = () => null;
15 |
16 | export default class RouterHookContext extends React.Component {
17 | static propTypes = {
18 | children: PropTypes.node.isRequired,
19 | components: PropTypes.arrayOf(componentsShape).isRequired,
20 | location: locationShape.isRequired,
21 | onAborted: PropTypes.func.isRequired,
22 | onCompleted: PropTypes.func.isRequired,
23 | onError: PropTypes.func.isRequired,
24 | onStarted: PropTypes.func.isRequired,
25 | };
26 |
27 | static childContextTypes = {
28 | routerHookContext: routerHookContextShape,
29 | };
30 |
31 | constructor(props) {
32 | super(props);
33 | this.componentStatuses = {};
34 | this.setComponentStatus = this.setComponentStatus.bind(this);
35 | this.getComponentStatus = this.getComponentStatus.bind(this);
36 | this.addLoadingListener = this.addLoadingListener.bind(this);
37 | this.updateRouterLoading = this.updateRouterLoading.bind(this);
38 | this.loading = false;
39 | }
40 |
41 | getChildContext() {
42 | return {
43 | routerHookContext: {
44 | getComponentStatus: this.getComponentStatus,
45 | setComponentStatus: this.setComponentStatus,
46 | addLoadingListener: this.addLoadingListener,
47 | },
48 | };
49 | }
50 |
51 | componentDidMount() {
52 | this.props.onStarted();
53 | }
54 |
55 | componentWillReceiveProps(nextProps) {
56 | if (nextProps.location === this.props.location) {
57 | return;
58 | }
59 | this.componentStatuses = {};
60 | if (this.loading) {
61 | this.props.onAborted();
62 | }
63 | }
64 |
65 | componentWillUnmount() {
66 | if (this.routerEventEmitter) {
67 | this.routerEventEmitter.removeAllListeners(CHANGE_LOADING_STATE);
68 | this.routerEventEmitter = null;
69 | }
70 | }
71 |
72 | setComponentStatus(Component, status, err) {
73 | const routerHooks = Component[routerHookPropName];
74 | if (!routerHooks) {
75 | return;
76 | }
77 | this.componentStatuses[routerHooks.id] = status;
78 | if (err) {
79 | this.props.onError({ Component, error: err });
80 | this.componentStatuses[routerHooks.id] = ComponentStatus.DONE;
81 | }
82 | this.updateRouterLoading();
83 | }
84 |
85 | getComponentStatus(Component) {
86 | const routerHooks = Component[routerHookPropName];
87 | if (!routerHooks) {
88 | return null;
89 | }
90 | return this.componentStatuses[routerHooks.id];
91 | }
92 |
93 | addLoadingListener(listener) {
94 | if (!canUseDOM) {
95 | return noop;
96 | }
97 | if (!this.routerEventEmitter) {
98 | this.routerEventEmitter = new EventEmitter();
99 | }
100 | this.routerEventEmitter.on(CHANGE_LOADING_STATE, listener);
101 | return () => {
102 | this.routerEventEmitter.removeListener(CHANGE_LOADING_STATE, listener);
103 | };
104 | }
105 |
106 | updateRouterLoading() {
107 | if (!canUseDOM) {
108 | return;
109 | }
110 | const components = getAllComponents(this.props.components);
111 | let total = 0;
112 | let init = 0;
113 | let defer = 0;
114 | let done = 0;
115 | for (let i = 0, len = components.length; i < len; i += 1) {
116 | const hookId = components[i][routerHookPropName].id;
117 | const status = this.componentStatuses[hookId];
118 | if (status) {
119 | total += 1;
120 | switch (status) {
121 | case ComponentStatus.INIT:
122 | init += 1;
123 | break;
124 | case ComponentStatus.DEFER:
125 | defer += 1;
126 | break;
127 | case ComponentStatus.DONE:
128 | done += 1;
129 | break;
130 | default:
131 | throw new Error(`Unknown status ${status}`);
132 | }
133 | }
134 | }
135 |
136 | const loading = done < total;
137 | if (this.routerEventEmitter) {
138 | this.routerEventEmitter.emit(CHANGE_LOADING_STATE, loading, {
139 | total,
140 | init,
141 | defer,
142 | done,
143 | });
144 | }
145 | if (this.loading !== loading) {
146 | this.loading = loading;
147 | if (!loading) {
148 | this.props.onCompleted();
149 | } else {
150 | this.props.onStarted();
151 | }
152 | }
153 | }
154 |
155 | render() {
156 | return this.props.children;
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const routerHookPropName = '@@react-router-hook';
2 |
3 | export const ComponentStatus = {
4 | INIT: 'init',
5 | DEFER: 'defer',
6 | DONE: 'done',
7 | };
8 |
--------------------------------------------------------------------------------
/src/getAllComponents.js:
--------------------------------------------------------------------------------
1 | import { routerHookPropName } from './constants';
2 |
3 | function pushComponent(acc, component) {
4 | if (!component) {
5 | return;
6 | }
7 | if (typeof component === 'object') {
8 | Object.keys(component).forEach(key => pushComponent(acc, component[key]));
9 | return;
10 | }
11 | if (component[routerHookPropName]) {
12 | acc.push(component);
13 | }
14 | }
15 |
16 | export default function getAllComponents(components) {
17 | const arr = Array.isArray(components) ? components : [components];
18 | const result = [];
19 | for (let i = 0, total = arr.length; i < total; i += 1) {
20 | pushComponent(result, arr[i]);
21 | }
22 | return result;
23 | }
24 |
--------------------------------------------------------------------------------
/src/getInitStatus.js:
--------------------------------------------------------------------------------
1 | import { routerHookPropName } from './constants';
2 |
3 | export default function getInitStatus(component, willEnterhooks) {
4 | if (!component || !component[routerHookPropName]) {
5 | return 'done';
6 | }
7 | const hooks = component[routerHookPropName];
8 | for (let i = 0; i < willEnterhooks.length; i += 1) {
9 | if (hooks[willEnterhooks[i]]) {
10 | return 'init';
11 | }
12 | }
13 | return 'defer';
14 | }
15 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { routerHookPropName, ComponentStatus } from './constants';
2 | export routerHooks from './routerHooks';
3 | export RouterHookContainer from './RouterHookContainer';
4 | export RouterHookContext from './RouterHookContext';
5 | export useRouterHook from './useRouterHook';
6 | export triggerHooksOnServer from './triggerHooksOnServer';
7 | export { routerHookContextShape } from './PropTypes';
8 |
--------------------------------------------------------------------------------
/src/routerHooks.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | import uuid from 'uuid';
3 |
4 | import { routerHookPropName } from './constants';
5 |
6 | const routerHooks = (hooks) => {
7 | hooks.id = hooks.id || uuid();
8 | return (Component) => {
9 | Component[routerHookPropName] = hooks;
10 | return Component;
11 | };
12 | };
13 |
14 | export default routerHooks;
15 |
--------------------------------------------------------------------------------
/src/triggerHooksOnServer.js:
--------------------------------------------------------------------------------
1 | import getAllComponents from './getAllComponents';
2 | import { routerHookPropName } from './constants';
3 |
4 | export default function triggerHooksOnServer(
5 | renderProps,
6 | hooks = [],
7 | locals,
8 | {
9 | onComponentError = null,
10 | },
11 | callback,
12 | ) {
13 | const args = {
14 | ...renderProps,
15 | ...locals,
16 | };
17 |
18 | const promises = getAllComponents(renderProps.components)
19 | .map((component) => {
20 | const routerHooks = component[routerHookPropName];
21 | if (!routerHooks) return null;
22 | const runHooks = hooks.map(key => routerHooks[key]).filter(f => f);
23 | if (runHooks.length < 1) return null;
24 | return runHooks.reduce((total, current) => total.then(() => current(args)), Promise.resolve())
25 | .catch(err => onComponentError({ Component: component, error: err }));
26 | })
27 | .filter(p => p);
28 |
29 | if (callback) {
30 | Promise.all(promises)
31 | .then(() => {
32 | callback();
33 | })
34 | .catch(callback);
35 | return null;
36 | }
37 | return Promise.all(promises);
38 | }
39 |
--------------------------------------------------------------------------------
/src/useRouterHook.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import RouterHookContext from './RouterHookContext';
3 | import RouterHookContainer from './RouterHookContainer';
4 |
5 | function noop() {}
6 |
7 | export default function useRouterHook(options) {
8 | let container = null;
9 | const {
10 | locals = {},
11 | onAborted = noop,
12 | onCompleted = noop,
13 | onError = noop,
14 | onStarted = noop,
15 | routerDidEnterHooks = [],
16 | routerWillEnterHooks = [],
17 | } = options;
18 | return {
19 | renderRouterContext: (child, renderProps) => {
20 | const {
21 | components,
22 | location,
23 | } = renderProps;
24 | return (
25 |
33 | {child}
34 |
35 | );
36 | },
37 | renderRouteComponent: (child, renderProps) => {
38 | if (!child) {
39 | return null;
40 | }
41 | if (!container) {
42 | container = (
43 |
49 | {child}
50 |
51 | );
52 | return container;
53 | }
54 | return React.cloneElement(container, {
55 | locals,
56 | renderProps,
57 | routerDidEnterHooks,
58 | routerWillEnterHooks,
59 | }, child);
60 | },
61 | };
62 | }
63 |
--------------------------------------------------------------------------------