├── .buildkite ├── nodeTests └── pipeline.yml ├── .codecov.yml ├── .cuprc.js ├── .eslintrc.js ├── .flowconfig ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .npmrc ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── flow-typed ├── globals.js └── tape-cup_v4.x.x.js ├── package.json ├── renovate.json ├── src ├── __tests__ │ ├── __node__ │ │ ├── context.node.js │ │ ├── prepare-context.node.js │ │ ├── prepare-render.node.js │ │ └── split.node.js │ └── index.node.js ├── constants.js ├── dispatched.js ├── index.js ├── middleware.js ├── prepare-provider.js ├── prepare.js ├── prepared.js ├── split.js ├── traverse-exclude.js └── utils │ └── isReactCompositeComponent.js └── yarn.lock /.buildkite/nodeTests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ./node_modules/.bin/nyc --instrument=false --exclude-after-remap=false --reporter=text --reporter=json node dist-tests/node.js || exit 1 3 | bash <(curl -s https://codecov.io/bash) -f coverage/coverage-final.json -n node 4 | -------------------------------------------------------------------------------- /.buildkite/pipeline.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: ':docker: :package:' 3 | plugins: 4 | 'docker-compose#v1.7.0': 5 | build: fusion-react-async 6 | image-repository: 027047743804.dkr.ecr.us-east-2.amazonaws.com/uber 7 | agents: 8 | queue: builders 9 | - wait 10 | - command: yarn flow 11 | name: ':flowtype:' 12 | plugins: 13 | 'docker-compose#v1.7.0': 14 | run: fusion-react-async 15 | agents: 16 | queue: workers 17 | - name: ':eslint:' 18 | command: yarn lint 19 | plugins: 20 | 'docker-compose#v1.7.0': 21 | run: fusion-react-async 22 | agents: 23 | queue: workers 24 | - name: ':node: :white_check_mark:' 25 | command: .buildkite/nodeTests 26 | plugins: 27 | 'docker-compose#v1.7.0': 28 | run: fusion-react-async 29 | agents: 30 | queue: workers 31 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | parsers: 2 | javascript: 3 | enable_partials: 'yes' 4 | -------------------------------------------------------------------------------- /.cuprc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | babel: { 3 | presets: [ 4 | require.resolve('@babel/preset-react') 5 | ], 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('eslint-config-fusion')], 3 | }; 4 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.*[^(package)]\.json$ 3 | /dist/.* 4 | 5 | [include] 6 | ./src/ 7 | 8 | [libs] 9 | ./node_modules/fusion-core/flow-typed 10 | 11 | [lints] 12 | 13 | [options] 14 | 15 | [strict] 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | ### Type of issue 13 | 14 | 15 | 16 | ### Description 17 | 18 | 19 | 20 | ### Current behavior 21 | 22 | 23 | 24 | ### Expected behavior 25 | 26 | 27 | 28 | ### Steps to reproduce 29 | 30 | 1. 31 | 2. 32 | 3. 33 | 34 | ### Your environment 35 | 36 | * fusion-react-async version: 37 | 38 | * Node.js version (`node --version`): 39 | 40 | * npm version (`npm --version`): 41 | 42 | * Operating System: -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | dist-tests/ 4 | coverage/ 5 | .nyc_output/ 6 | 7 | .DS_Store 8 | npm-debug.log 9 | yarn-error.log 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.yarnpkg.com 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM uber/web-base-image:1.0.5@sha256:4e53fd9da9d710a9cee8e7c39c3d6edad110904ffc3cf7b1260b9adedd5ba518 2 | 3 | WORKDIR /fusion-react-async 4 | 5 | COPY . . 6 | 7 | RUN yarn 8 | 9 | RUN yarn build-test 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Uber Technologies, Inc. 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 | # Deprecation notice 2 | 3 | This package has been merged into [`fusion-react`](https://github.com/fusionjs/fusion-react). 4 | 5 | ```diff 6 | import { 7 | dispatched, 8 | prepare, 9 | prepared, 10 | split, 11 | exclude, 12 | middleware, 13 | - } from "fusion-react-async"; 14 | + } from "fusion-react"; 15 | ``` 16 | 17 | # fusion-react-async 18 | 19 | [![Build status](https://badge.buildkite.com/7037953b28b5737bad5844360b3ceef38b3aab09b6fd31587d.svg?branch=master)](https://buildkite.com/uberopensource/fusion-react-async) 20 | 21 | This package allows you to have deeply nested components with asynchronous dependencies and have everything just work with server-side rendering. 22 | 23 | The typical use-case is when a deeply-nested component needs to have a resource fetched from a remote HTTP server, such as GraphQL or REST API. Since `renderToString` is synchronous, when you call it on your app, this component won't render correctly. 24 | 25 | One solution is to have a central router at the root of your application that knows exactly what data needs to be fetched before rendering. But this solution doesn't fit the component-based architecture of a typical React app. You want to declare data dependencies at the component level, much like your declare your props. 26 | 27 | This is exactly what `fusion-react-async` does: it allows you to declare asynchronous dependencies at the component level, and make them work fine with server-side rendering as well as client-side rendering. 28 | 29 | If an application grows too much in size, one way to help reduce the size of the initial download is to split parts of the application into separate bundles and download those only when they are needed. This technique is known 30 | as bundle splitting and `fusion-react-async` provides tools to do it easily. 31 | 32 | --- 33 | 34 | ### Examples 35 | 36 | #### Data fetching 37 | 38 | ```js 39 | // src/main.js 40 | import React from 'react'; 41 | import App from 'fusion-react'; 42 | import Example from './components/example'; 43 | import UserAPI from './api' 44 | 45 | export default () => { 46 | const app = new App(); 47 | 48 | app.register(UserAPI); 49 | 50 | return app; 51 | } 52 | 53 | // src/components/example.js 54 | import {prepared} from 'fusion-react-async'; 55 | 56 | function Example({name}) { 57 | return
Hello, {name}
; 58 | } 59 | 60 | export default prepared(() => fetch('/api/user/1'))(Example); 61 | 62 | // src/api.js 63 | import {createPlugin} from 'fusion-core'; 64 | 65 | export default createPlugin({ 66 | middleware() { 67 | return (ctx, next) => { 68 | if (ctx.path === '/api/user/1') { 69 | ctx.body = {name: 'Bob'}; 70 | } 71 | return next(); 72 | }; 73 | } 74 | }); 75 | ``` 76 | 77 | #### Bundle splitting 78 | 79 | ```js 80 | // src/main.js 81 | import App from 'fusion-react'; 82 | import root from './components/root'; 83 | 84 | export default () => { 85 | return new App(root); 86 | } 87 | 88 | // src/components/root.js 89 | import React from 'react'; 90 | import {split} from 'fusion-react-async'; 91 | 92 | const LoadingComponent = () =>
Loading...
; 93 | const ErrorComponent = () =>
Error loading component
; 94 | const BundleSplit = split({ 95 | load: () => import('./components/hello'); 96 | LoadingComponent, 97 | ErrorComponent 98 | }); 99 | 100 | const root = ( 101 |
102 |
This is part of the initial bundle
103 | 104 |
105 | ) 106 | 107 | export default root; 108 | 109 | // src/components/hello.js 110 | export default () => ( 111 |
112 | This is part of a separate bundle that gets loaded asynchronously 113 | when the BundleSplit component gets mounted 114 |
115 | ) 116 | ``` 117 | 118 | --- 119 | 120 | ### API 121 | 122 | #### middleware 123 | 124 | ```js 125 | import { middleware } from 'fusion-react-async'; 126 | ``` 127 | 128 | A middleware that adds a `PrepareProvider` to the React tree. 129 | 130 | Consider using [`fusion-react`](https://github.com/fusionjs/fusion-react) instead of setting up React and registering this middleware manually, since that package does all of that for you. 131 | 132 | #### split 133 | 134 | ```js 135 | import { split } from 'fusion-react-async'; 136 | 137 | const Component = split({ load, LoadingComponent, ErrorComponent }); 138 | ``` 139 | 140 | * `load: () => Promise` - Required. Load a component asynchronously. Typically, this should make a dynamic `import()` call. 141 | The Fusion compiler takes care of bundling the appropriate code and de-duplicating dependencies. The argument to `import` should be a string literal (not a variable). See [webpack docs](https://webpack.js.org/api/module-methods/#import-) for more information. 142 | * `LoadingComponent` - Required. A component to be displayed while the asynchronous component hasn't downloaded 143 | * `ErrorComponent` - Required. A component to be displayed if the asynchronous component could not be loaded 144 | * `Component` - A placeholder component that can be used in your view which will show the asynchronous component 145 | 146 | #### prepare 147 | 148 | ```js 149 | import { prepare } from 'fusion-react-async'; 150 | 151 | const Component = prepare(element); 152 | ``` 153 | 154 | * `Element: React.Element` - Required. A React element created via `React.createElement` 155 | * `Component: React.Component` - A React component 156 | 157 | Consider using [`fusion-react`](https://github.com/fusionjs/fusion-react) instead of setting up React manually and calling `prepare` directly, since that package does all of that for you. 158 | 159 | The `prepare` function recursively traverses the element rendering tree and awaits the side effects of components decorated with `prepared` (or `dispatched`). 160 | 161 | It should be used (and `await`-ed) _before_ calling `renderToString` on the server. If any of the side effects throws, `prepare` will also throw. 162 | 163 | #### prepared 164 | 165 | ```js 166 | import { prepared } from 'fusion-react-async'; 167 | 168 | const hoc = prepared(sideEffect, opts); 169 | ``` 170 | 171 | * `sideEffect: (props: Object, context: Object) => Promise` - Required. When `prepare` is called, `sideEffect` is called (and awaited) before continuing the rendering traversal. 172 | * `opts: {defer, boundary, componentDidMount, componentWillReceiveProps, componentDidUpdate, forceUpdate, contextTypes}` - Optional 173 | * `defer: boolean` - Optional. Defaults to `false`. If the component is deferred, skip the prepare step. 174 | * `boundary: boolean` - Optional. Defaults to `false`. Stop traversing if the component is defer or boundary. 175 | * `componentDidMount: boolean` - Optional. Defaults to `true`. On the browser, `sideEffect` is called when the component is mounted. 176 | * [TO BE DEPRECATED] `componentWillReceiveProps: boolean` - Optional. Defaults to `false`. On the browser, `sideEffect` is called again whenever the component receive props. 177 | * `componentDidUpdate: boolean` - Optional. Defaults to `false`. On the browser, `sideEffect` is called again right after updating occurs. 178 | * `forceUpdate: boolean` - Optional. Defaults to `false`. 179 | * `contextTypes: Object` - Optional. Custom React context types to add to the prepared component. 180 | * `hoc: (Component: React.Component) => React.Component` - A higher-order component that returns a component that awaits for async side effects before rendering. 181 | * `Component: React.Component` - Required. 182 | 183 | #### exclude 184 | 185 | ```js 186 | import { exclude } from 'fusion-react-async'; 187 | 188 | const NewComponent = exclude(Component); 189 | ``` 190 | 191 | * `Component: React.Component` - Required. A component that should not be traversed via `prepare`. 192 | * `NewComponent: React.Component` - A component that is excluded from `prepare` traversal. 193 | 194 | Stops `prepare` traversal at `Component`. Useful for optimizing the `prepare` traversal to visit the minimum number of nodes. 195 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | fusion-react-async: 4 | build: . 5 | volumes: 6 | - '.:/fusion-react-async' 7 | - /fusion-react-async/node_modules/ 8 | - /fusion-react-async/dist/ 9 | - /fusion-react-async/dist-tests/ 10 | environment: 11 | - CODECOV_TOKEN 12 | - CI=true 13 | - BUILDKITE 14 | - BUILDKITE_BRANCH 15 | - BUILDKITE_BUILD_NUMBER 16 | - BUILDKITE_JOB_ID 17 | - BUILDKITE_BUILD_URL 18 | - BUILDKITE_PROJECT_SLUG 19 | - BUILDKITE_COMMIT 20 | -------------------------------------------------------------------------------- /flow-typed/globals.js: -------------------------------------------------------------------------------- 1 | /** Copyright (c) 2018 Uber Technologies, Inc. 2 | * 3 | * This source code is licensed under the MIT license found in the 4 | * LICENSE file in the root directory of this source tree. 5 | * 6 | * @flow 7 | */ 8 | 9 | declare var __NODE__: boolean; 10 | declare var __BROWSER__: boolean; 11 | -------------------------------------------------------------------------------- /flow-typed/tape-cup_v4.x.x.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | declare type tape$TestOpts = { 4 | skip: boolean, 5 | timeout?: number, 6 | } | { 7 | skip?: boolean, 8 | timeout: number, 9 | }; 10 | 11 | declare type tape$TestCb = (t: tape$Context) => mixed; 12 | declare type tape$TestFn = (a: string | tape$TestOpts | tape$TestCb, b?: tape$TestOpts | tape$TestCb, c?: tape$TestCb) => void; 13 | 14 | declare interface tape$Context { 15 | fail(msg?: string): void, 16 | pass(msg?: string): void, 17 | 18 | error(err: mixed, msg?: string): void, 19 | ifError(err: mixed, msg?: string): void, 20 | ifErr(err: mixed, msg?: string): void, 21 | iferror(err: mixed, msg?: string): void, 22 | 23 | ok(value: mixed, msg?: string): void, 24 | true(value: mixed, msg?: string): void, 25 | assert(value: mixed, msg?: string): void, 26 | 27 | notOk(value: mixed, msg?: string): void, 28 | false(value: mixed, msg?: string): void, 29 | notok(value: mixed, msg?: string): void, 30 | 31 | // equal + aliases 32 | equal(actual: mixed, expected: mixed, msg?: string): void, 33 | equals(actual: mixed, expected: mixed, msg?: string): void, 34 | isEqual(actual: mixed, expected: mixed, msg?: string): void, 35 | is(actual: mixed, expected: mixed, msg?: string): void, 36 | strictEqual(actual: mixed, expected: mixed, msg?: string): void, 37 | strictEquals(actual: mixed, expected: mixed, msg?: string): void, 38 | 39 | // notEqual + aliases 40 | notEqual(actual: mixed, expected: mixed, msg?: string): void, 41 | notEquals(actual: mixed, expected: mixed, msg?: string): void, 42 | notStrictEqual(actual: mixed, expected: mixed, msg?: string): void, 43 | notStrictEquals(actual: mixed, expected: mixed, msg?: string): void, 44 | isNotEqual(actual: mixed, expected: mixed, msg?: string): void, 45 | isNot(actual: mixed, expected: mixed, msg?: string): void, 46 | not(actual: mixed, expected: mixed, msg?: string): void, 47 | doesNotEqual(actual: mixed, expected: mixed, msg?: string): void, 48 | isInequal(actual: mixed, expected: mixed, msg?: string): void, 49 | 50 | // deepEqual + aliases 51 | deepEqual(actual: mixed, expected: mixed, msg?: string): void, 52 | deepEquals(actual: mixed, expected: mixed, msg?: string): void, 53 | isEquivalent(actual: mixed, expected: mixed, msg?: string): void, 54 | same(actual: mixed, expected: mixed, msg?: string): void, 55 | 56 | // notDeepEqual 57 | notDeepEqual(actual: mixed, expected: mixed, msg?: string): void, 58 | notEquivalent(actual: mixed, expected: mixed, msg?: string): void, 59 | notDeeply(actual: mixed, expected: mixed, msg?: string): void, 60 | notSame(actual: mixed, expected: mixed, msg?: string): void, 61 | isNotDeepEqual(actual: mixed, expected: mixed, msg?: string): void, 62 | isNotDeeply(actual: mixed, expected: mixed, msg?: string): void, 63 | isNotEquivalent(actual: mixed, expected: mixed, msg?: string): void, 64 | isInequivalent(actual: mixed, expected: mixed, msg?: string): void, 65 | 66 | // deepLooseEqual 67 | deepLooseEqual(actual: mixed, expected: mixed, msg?: string): void, 68 | looseEqual(actual: mixed, expected: mixed, msg?: string): void, 69 | looseEquals(actual: mixed, expected: mixed, msg?: string): void, 70 | 71 | // notDeepLooseEqual 72 | notDeepLooseEqual(actual: mixed, expected: mixed, msg?: string): void, 73 | notLooseEqual(actual: mixed, expected: mixed, msg?: string): void, 74 | notLooseEquals(actual: mixed, expected: mixed, msg?: string): void, 75 | 76 | throws(fn: Function, expected?: RegExp | Function, msg?: string): void, 77 | doesNotThrow(fn: Function, expected?: RegExp | Function, msg?: string): void, 78 | 79 | timeoutAfter(ms: number): void, 80 | 81 | skip(msg?: string): void, 82 | plan(n: number): void, 83 | onFinish(fn: Function): void, 84 | end(): void, 85 | comment(msg: string): void, 86 | test: tape$TestFn, 87 | } 88 | 89 | declare module 'tape-cup' { 90 | declare type TestHarness = Tape; 91 | declare type StreamOpts = { 92 | objectMode?: boolean, 93 | }; 94 | 95 | declare type Tape = { 96 | (a: string | tape$TestOpts | tape$TestCb, b?: tape$TestCb | tape$TestOpts, c?: tape$TestCb, ...rest: Array): void, 97 | test: tape$TestFn, 98 | skip: (name: string, cb?: tape$TestCb) => void, 99 | createHarness: () => TestHarness, 100 | createStream: (opts?: StreamOpts) => stream$Readable, 101 | only: (a: string | tape$TestOpts | tape$TestCb, b?: tape$TestCb | tape$TestOpts, c?: tape$TestCb, ...rest: Array) => void, 102 | }; 103 | 104 | declare module.exports: Tape; 105 | } 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fusion-react-async", 3 | "version": "1.2.4", 4 | "description": "Prepare you app state for async rendering", 5 | "repository": "fusionjs/fusion-react-async", 6 | "files": [ 7 | "dist", 8 | "src" 9 | ], 10 | "main": "./dist/index.js", 11 | "module": "./dist/index.es.js", 12 | "browser": { 13 | "./dist/index.js": "./dist/browser.es5.js", 14 | "./dist/index.es.js": "./dist/browser.es5.es.js" 15 | }, 16 | "es2015": { 17 | "./dist/browser.es5.es.js": "./dist/browser.es2015.es.js" 18 | }, 19 | "es2017": { 20 | "./dist/browser.es5.es.js": "./dist/browser.es2017.es.js", 21 | "./dist/browser.es2015.es.js": "./dist/browser.es2017.es.js" 22 | }, 23 | "scripts": { 24 | "lint": "eslint . --ignore-path .gitignore", 25 | "build-test": "cup build-tests", 26 | "just-test": "node dist-tests/node.js", 27 | "clean": "rm -rf dist", 28 | "transpile": "npm run clean && cup build", 29 | "prepublish": "npm run clean && npm run transpile", 30 | "test": "npm run build-test && npm run just-test" 31 | }, 32 | "dependencies": { 33 | "fusion-core": "^1.3.0", 34 | "prop-types": "^15.6.1", 35 | "react-is": "^16.3.2" 36 | }, 37 | "devDependencies": { 38 | "@babel/preset-react": "7.0.0-beta.52", 39 | "babel-eslint": "8.2.5", 40 | "create-universal-package": "3.4.4", 41 | "enzyme": "^3.3.0", 42 | "enzyme-adapter-react-16": "^1.1.1", 43 | "eslint": "4.19.1", 44 | "eslint-config-fusion": "^1.0.1", 45 | "eslint-plugin-cup": "1.0.2", 46 | "eslint-plugin-flowtype": "2.50.0", 47 | "eslint-plugin-import": "^2.11.0", 48 | "eslint-plugin-prettier": "2.6.0", 49 | "eslint-plugin-react": "7.10.0", 50 | "flow-bin": "^0.72.0", 51 | "nyc": "^11.8.0", 52 | "prettier": "1.12.1", 53 | "react": "^16.3.2", 54 | "react-dom": "^16.3.2", 55 | "tape-cup": "^4.7.1" 56 | }, 57 | "peerDependencies": { 58 | "fusion-core": "^1.0.0", 59 | "react": "^16.3", 60 | "react-dom": "^16.3" 61 | }, 62 | "engines": { 63 | "node": ">= 8.9.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "uber" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/__tests__/__node__/context.node.js: -------------------------------------------------------------------------------- 1 | /** Copyright (c) 2018 Uber Technologies, Inc. 2 | * 3 | * This source code is licensed under the MIT license found in the 4 | * LICENSE file in the root directory of this source tree. 5 | * 6 | * @flow 7 | */ 8 | 9 | import tape from 'tape-cup'; 10 | import React from 'react'; 11 | import {renderToString} from 'react-dom/server'; 12 | import Provider from '../../prepare-provider'; 13 | import {prepare} from '../../index.js'; 14 | 15 | tape('Handling context', async t => { 16 | class Child extends React.Component { 17 | static contextTypes = { 18 | field: () => {}, 19 | }; 20 | 21 | constructor(props: *) { 22 | super(props); 23 | } 24 | 25 | render() { 26 | return

{this.context.field ? 'Yes' : 'No'}

; 27 | } 28 | } 29 | 30 | class Parent extends React.Component { 31 | static childContextTypes = { 32 | field: () => {}, 33 | }; 34 | 35 | getChildContext() { 36 | return {field: true}; 37 | } 38 | 39 | render() { 40 | return ; 41 | } 42 | } 43 | 44 | const ToTest = () => { 45 | return ( 46 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | const app = ( 53 | 54 | 55 | 56 | ); 57 | t.ok(/Yes/.test(renderToString(app))); 58 | await prepare(app); 59 | t.ok(/Yes/.test(renderToString(app))); 60 | t.end(); 61 | }); 62 | -------------------------------------------------------------------------------- /src/__tests__/__node__/prepare-context.node.js: -------------------------------------------------------------------------------- 1 | /** Copyright (c) 2018 Uber Technologies, Inc. 2 | * 3 | * This source code is licensed under the MIT license found in the 4 | * LICENSE file in the root directory of this source tree. 5 | * 6 | * @flow 7 | */ 8 | 9 | /* eslint-disable react/no-multi-comp */ 10 | import tape from 'tape-cup'; 11 | import React, {Component} from 'react'; 12 | import {prepare} from '../../index.js'; 13 | 14 | tape('Preparing a sync app passing through context', t => { 15 | let numConstructors = 0; 16 | let numRenders = 0; 17 | let numChildRenders = 0; 18 | class SimpleComponent extends Component { 19 | constructor(props, context) { 20 | super(props, context); 21 | t.equal( 22 | context.__IS_PREPARE__, 23 | true, 24 | 'sets __IS_PREPARE__ to true in context' 25 | ); 26 | numConstructors++; 27 | } 28 | getChildContext() { 29 | return { 30 | test: 'data', 31 | }; 32 | } 33 | render() { 34 | numRenders++; 35 | return ; 36 | } 37 | } 38 | function SimplePresentational(props, context) { 39 | t.equal(context.test, 'data', 'handles child context correctly'); 40 | numChildRenders++; 41 | return
Hello World
; 42 | } 43 | const app = ; 44 | const p = prepare(app); 45 | t.ok(p instanceof Promise, 'prepare returns a promise'); 46 | p.then(() => { 47 | t.equal(numConstructors, 1, 'constructs SimpleComponent once'); 48 | t.equal(numRenders, 1, 'renders SimpleComponent once'); 49 | t.equal(numChildRenders, 1, 'renders SimplePresentational once'); 50 | t.end(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/__tests__/__node__/prepare-render.node.js: -------------------------------------------------------------------------------- 1 | /** Copyright (c) 2018 Uber Technologies, Inc. 2 | * 3 | * This source code is licensed under the MIT license found in the 4 | * LICENSE file in the root directory of this source tree. 5 | * 6 | * @flow 7 | */ 8 | 9 | /* eslint-disable react/no-multi-comp */ 10 | import tape from 'tape-cup'; 11 | import React, {Component} from 'react'; 12 | import Enzyme, {shallow} from 'enzyme'; 13 | import Adapter from 'enzyme-adapter-react-16'; 14 | import {prepare, prepared} from '../../index.js'; 15 | 16 | Enzyme.configure({adapter: new Adapter()}); 17 | 18 | tape('Preparing a sync app', t => { 19 | let numConstructors = 0; 20 | let numRenders = 0; 21 | let numChildRenders = 0; 22 | class SimpleComponent extends Component { 23 | constructor(props, context) { 24 | super(props, context); 25 | t.equal( 26 | context.__IS_PREPARE__, 27 | true, 28 | 'sets __IS_PREPARE__ to true in context' 29 | ); 30 | numConstructors++; 31 | } 32 | render() { 33 | numRenders++; 34 | return ; 35 | } 36 | } 37 | function SimplePresentational() { 38 | numChildRenders++; 39 | return
Hello World
; 40 | } 41 | const app = ; 42 | const p = prepare(app); 43 | t.ok(p instanceof Promise, 'prepare returns a promise'); 44 | p.then(() => { 45 | t.equal(numConstructors, 1, 'constructs SimpleComponent once'); 46 | t.equal(numRenders, 1, 'renders SimpleComponent once'); 47 | t.equal(numChildRenders, 1, 'renders SimplePresentational once'); 48 | t.end(); 49 | }); 50 | }); 51 | 52 | tape('Preparing a sync app with nested children', t => { 53 | let numConstructors = 0; 54 | let numRenders = 0; 55 | let numChildRenders = 0; 56 | class SimpleComponent extends Component { 57 | constructor(props, context) { 58 | super(props, context); 59 | t.equal( 60 | context.__IS_PREPARE__, 61 | true, 62 | 'sets __IS_PREPARE__ to true in context' 63 | ); 64 | numConstructors++; 65 | } 66 | render() { 67 | numRenders++; 68 | return
{this.props.children}
; 69 | } 70 | } 71 | function SimplePresentational() { 72 | numChildRenders++; 73 | return
Hello World
; 74 | } 75 | const app = ( 76 | 77 | ; 78 | 79 | ); 80 | const p = prepare(app); 81 | t.ok(p instanceof Promise, 'prepare returns a promise'); 82 | p.then(() => { 83 | t.equal(numConstructors, 1, 'constructs SimpleComponent once'); 84 | t.equal(numRenders, 1, 'renders SimpleComponent once'); 85 | t.equal(numChildRenders, 1, 'renders SimplePresentational once'); 86 | t.end(); 87 | }); 88 | }); 89 | 90 | tape( 91 | 'Preparing a sync app with functional components referencing children', 92 | t => { 93 | let numRenders = 0; 94 | let numChildRenders = 0; 95 | let numPrepares = 0; 96 | function SimpleComponent(props, context) { 97 | t.equal( 98 | context.__IS_PREPARE__, 99 | true, 100 | 'sets __IS_PREPARE__ to true in context' 101 | ); 102 | numRenders++; 103 | return
{props.children}
; 104 | } 105 | function SimplePresentational() { 106 | numChildRenders++; 107 | return
Hello World
; 108 | } 109 | const AsyncChild = prepared(props => { 110 | numPrepares++; 111 | t.equal( 112 | props.data, 113 | 'test', 114 | 'passes props through to prepared component correctly' 115 | ); 116 | return Promise.resolve(); 117 | })(SimplePresentational); 118 | const app = ( 119 | 120 | 121 | 122 | ); 123 | const p = prepare(app); 124 | t.ok(p instanceof Promise, 'prepare returns a promise'); 125 | p.then(() => { 126 | t.equal(numRenders, 1, 'renders SimpleComponent once'); 127 | t.equal(numPrepares, 1, 'runs prepare function once'); 128 | t.equal(numChildRenders, 1, 'renders SimplePresentational once'); 129 | t.end(); 130 | }); 131 | } 132 | ); 133 | 134 | tape('Preparing an async app', t => { 135 | let numConstructors = 0; 136 | let numRenders = 0; 137 | let numChildRenders = 0; 138 | let numPrepares = 0; 139 | class SimpleComponent extends Component { 140 | constructor(props, context) { 141 | super(props, context); 142 | t.equal( 143 | context.__IS_PREPARE__, 144 | true, 145 | 'sets __IS_PREPARE__ to true in context' 146 | ); 147 | numConstructors++; 148 | } 149 | render() { 150 | numRenders++; 151 | return ; 152 | } 153 | } 154 | function SimplePresentational() { 155 | numChildRenders++; 156 | return
Hello World
; 157 | } 158 | const AsyncParent = prepared(props => { 159 | numPrepares++; 160 | t.equal( 161 | props.data, 162 | 'test', 163 | 'passes props through to prepared component correctly' 164 | ); 165 | return Promise.resolve(); 166 | })(SimpleComponent); 167 | const app = ; 168 | const p = prepare(app); 169 | t.ok(p instanceof Promise, 'prepare returns a promise'); 170 | p.then(() => { 171 | t.equal(numPrepares, 1, 'runs the prepare function once'); 172 | t.equal(numConstructors, 1, 'constructs SimpleComponent once'); 173 | t.equal(numRenders, 1, 'renders SimpleComponent once'); 174 | t.equal(numChildRenders, 1, 'renders SimplePresentational once'); 175 | t.end(); 176 | }); 177 | }); 178 | 179 | tape('Preparing an async app with nested asyncs', t => { 180 | let numConstructors = 0; 181 | let numRenders = 0; 182 | let numChildRenders = 0; 183 | let numPrepares = 0; 184 | class SimpleComponent extends Component { 185 | constructor(props, context) { 186 | super(props, context); 187 | t.equal( 188 | context.__IS_PREPARE__, 189 | true, 190 | 'sets __IS_PREPARE__ to true in context' 191 | ); 192 | numConstructors++; 193 | } 194 | render() { 195 | numRenders++; 196 | return
{this.props.children}
; 197 | } 198 | } 199 | 200 | function SimplePresentational() { 201 | numChildRenders++; 202 | return
Hello World
; 203 | } 204 | const AsyncParent = prepared(props => { 205 | numPrepares++; 206 | t.equal( 207 | props.data, 208 | 'test', 209 | 'passes props through to prepared component correctly' 210 | ); 211 | return Promise.resolve(); 212 | })(SimpleComponent); 213 | const app = ( 214 | 215 | 216 | 217 | 218 | 219 | ); 220 | 221 | const p = prepare(app); 222 | t.ok(p instanceof Promise, 'prepare returns a promise'); 223 | p.then(() => { 224 | t.equal(numPrepares, 2, 'runs each prepare function once'); 225 | t.equal( 226 | numConstructors, 227 | 2, 228 | 'constructs SimpleComponent once for each render' 229 | ); 230 | t.equal(numRenders, 2, 'renders SimpleComponent twice'); 231 | t.equal(numChildRenders, 1, 'renders SimplePresentational once'); 232 | t.end(); 233 | }); 234 | }); 235 | 236 | tape('Preparing an app with sibling async components', t => { 237 | let numConstructors = 0; 238 | let numRenders = 0; 239 | let numChildRenders = 0; 240 | let numPrepares = 0; 241 | class SimpleComponent extends Component { 242 | constructor(props, context) { 243 | super(props, context); 244 | t.equal( 245 | context.__IS_PREPARE__, 246 | true, 247 | 'sets __IS_PREPARE__ to true in context' 248 | ); 249 | numConstructors++; 250 | } 251 | render() { 252 | numRenders++; 253 | return
{this.props.children}
; 254 | } 255 | } 256 | 257 | function SimplePresentational() { 258 | numChildRenders++; 259 | return
Hello World
; 260 | } 261 | const AsyncParent = prepared(props => { 262 | numPrepares++; 263 | t.equal( 264 | props.data, 265 | 'test', 266 | 'passes props through to prepared component correctly' 267 | ); 268 | return Promise.resolve(); 269 | })(SimpleComponent); 270 | const app = ( 271 |
272 | 273 | 274 | 275 | 276 | 277 | 278 |
279 | ); 280 | 281 | const p = prepare(app); 282 | t.ok(p instanceof Promise, 'prepare returns a promise'); 283 | p.then(() => { 284 | t.equal(numPrepares, 2, 'runs each prepare function once'); 285 | t.equal( 286 | numConstructors, 287 | 2, 288 | 'constructs SimpleComponent once for each render' 289 | ); 290 | t.equal(numRenders, 2, 'renders SimpleComponent twice'); 291 | t.equal( 292 | numChildRenders, 293 | 2, 294 | 'renders SimplePresentational once for each render' 295 | ); 296 | t.end(); 297 | }); 298 | }); 299 | 300 | tape('Rendering a component triggers componentWillMount before render', t => { 301 | const orderedMethodCalls = []; 302 | const orderedChildMethodCalls = []; 303 | 304 | // Disable eslint for deprecated componentWillMount 305 | // eslint-disable-next-line react/no-deprecated 306 | class SimpleComponent extends Component { 307 | componentWillMount() { 308 | orderedMethodCalls.push('componentWillMount'); 309 | } 310 | 311 | render() { 312 | orderedMethodCalls.push('render'); 313 | return ; 314 | } 315 | } 316 | 317 | // Disable eslint for deprecated componentWillMount 318 | // eslint-disable-next-line react/no-deprecated 319 | class SimpleChildComponent extends Component { 320 | componentWillMount() { 321 | orderedChildMethodCalls.push('componentWillMount'); 322 | } 323 | 324 | render() { 325 | orderedChildMethodCalls.push('render'); 326 | return
Hello World
; 327 | } 328 | } 329 | 330 | const app = ; 331 | const p = prepare(app); 332 | t.ok(p instanceof Promise, 'prepare returns a promise'); 333 | p.then(() => { 334 | t.deepEqual(orderedMethodCalls, ['componentWillMount', 'render']); 335 | t.deepEqual(orderedChildMethodCalls, ['componentWillMount', 'render']); 336 | t.end(); 337 | }); 338 | }); 339 | 340 | tape('Preparing an async app with componentWillReceiveProps option', t => { 341 | let numConstructors = 0; 342 | let numRenders = 0; 343 | let numChildRenders = 0; 344 | let numPrepares = 0; 345 | class SimpleComponent extends Component { 346 | constructor(props, context) { 347 | super(props, context); 348 | t.equal( 349 | context.__IS_PREPARE__, 350 | true, 351 | 'sets __IS_PREPARE__ to true in context' 352 | ); 353 | numConstructors++; 354 | } 355 | render() { 356 | numRenders++; 357 | return ; 358 | } 359 | } 360 | function SimplePresentational() { 361 | numChildRenders++; 362 | return
Hello World
; 363 | } 364 | const AsyncParent = prepared( 365 | props => { 366 | numPrepares++; 367 | t.equal( 368 | props.data, 369 | 'test', 370 | 'passes props through to prepared component correctly' 371 | ); 372 | return Promise.resolve(); 373 | }, 374 | { 375 | componentWillReceiveProps: true, 376 | } 377 | )(SimpleComponent); 378 | const app = ; 379 | const p = prepare(app); 380 | t.ok(p instanceof Promise, 'prepare returns a promise'); 381 | p.then(() => { 382 | t.equal(numPrepares, 1, 'runs the prepare function once'); 383 | t.equal(numConstructors, 1, 'constructs SimpleComponent once'); 384 | t.equal(numRenders, 1, 'renders SimpleComponent once'); 385 | t.equal(numChildRenders, 1, 'renders SimplePresentational once'); 386 | // triggers componentDidMount 387 | const wrapper = shallow(app); 388 | t.equal(numPrepares, 2, 'runs prepare on componentDidMount'); 389 | // triggers componentWillReceiveProps 390 | wrapper.setProps({test: true}); 391 | t.equal(numPrepares, 3, 'runs prepare on componentWillReceiveProps'); 392 | t.end(); 393 | }); 394 | }); 395 | 396 | tape('Preparing an async app with componentDidUpdate option', t => { 397 | let numConstructors = 0; 398 | let numRenders = 0; 399 | let numChildRenders = 0; 400 | let numPrepares = 0; 401 | class SimpleComponent extends Component { 402 | constructor(props, context) { 403 | super(props, context); 404 | t.equal( 405 | context.__IS_PREPARE__, 406 | true, 407 | 'sets __IS_PREPARE__ to true in context' 408 | ); 409 | numConstructors++; 410 | } 411 | render() { 412 | numRenders++; 413 | return ; 414 | } 415 | } 416 | function SimplePresentational() { 417 | numChildRenders++; 418 | return
Hello World
; 419 | } 420 | const AsyncParent = prepared( 421 | props => { 422 | numPrepares++; 423 | t.equal( 424 | props.data, 425 | 'test', 426 | 'passes props through to prepared component correctly' 427 | ); 428 | return Promise.resolve(); 429 | }, 430 | { 431 | componentDidUpdate: true, 432 | } 433 | )(SimpleComponent); 434 | const app = ; 435 | const p = prepare(app); 436 | t.ok(p instanceof Promise, 'prepare returns a promise'); 437 | p.then(() => { 438 | t.equal(numPrepares, 1, 'runs the prepare function once'); 439 | t.equal(numConstructors, 1, 'constructs SimpleComponent once'); 440 | t.equal(numRenders, 1, 'renders SimpleComponent once'); 441 | t.equal(numChildRenders, 1, 'renders SimplePresentational once'); 442 | // triggers componentDidMount 443 | const wrapper = shallow(app); 444 | t.equal(numPrepares, 2, 'runs prepare on componentDidMount'); 445 | // triggers componentDidUpdate 446 | wrapper.setProps({test: true}); 447 | t.equal(numPrepares, 3, 'runs prepare on componentDidUpdate'); 448 | t.end(); 449 | }); 450 | }); 451 | 452 | tape('Preparing a Fragment', t => { 453 | const app = ( 454 | // $FlowFixMe 455 | 456 | 1 457 | 2 458 | 459 | ); 460 | const p = prepare(app); 461 | t.ok(p instanceof Promise, 'prepare returns a promise'); 462 | p.then(() => { 463 | const wrapper = shallow(
{app}
); 464 | t.equal(wrapper.find('span').length, 2, 'has two children'); 465 | t.end(); 466 | }); 467 | }); 468 | 469 | tape('Preparing a fragment with async children', t => { 470 | let numChildRenders = 0; 471 | let numPrepares = 0; 472 | function SimplePresentational() { 473 | numChildRenders++; 474 | return
Hello World
; 475 | } 476 | const AsyncChild = prepared(props => { 477 | numPrepares++; 478 | t.equal( 479 | props.data, 480 | 'test', 481 | 'passes props through to prepared component correctly' 482 | ); 483 | return Promise.resolve(); 484 | })(SimplePresentational); 485 | const app = ( 486 | // $FlowFixMe 487 | 488 | 489 | 490 | 491 | ); 492 | const p = prepare(app); 493 | t.ok(p instanceof Promise, 'prepare returns a promise'); 494 | p.then(() => { 495 | t.equal(numPrepares, 2, 'runs prepare function twice'); 496 | t.equal(numChildRenders, 2, 'renders SimplePresentational twice'); 497 | t.end(); 498 | }); 499 | }); 500 | 501 | tape('Preparing React.createContext()', t => { 502 | // $FlowFixMe 503 | const {Provider, Consumer} = React.createContext('light'); 504 | 505 | const app = ( 506 | 507 | 1 508 | {() => 2} 509 | 510 | ); 511 | const p = prepare(app); 512 | t.ok(p instanceof Promise, 'prepare returns a promise'); 513 | p.then(() => { 514 | const wrapper = shallow(
{app}
); 515 | t.equal(wrapper.find('span').length, 1, 'one span is rendered'); 516 | t.end(); 517 | }); 518 | }); 519 | 520 | tape('Preparing React.createContext() with async children', t => { 521 | // $FlowFixMe 522 | const {Provider, Consumer} = React.createContext('light'); 523 | 524 | let numChildRenders = 0; 525 | let numPrepares = 0; 526 | let numRenderPropsRenders = 0; 527 | function SimplePresentational() { 528 | numChildRenders++; 529 | 530 | return ( 531 | 532 | {theme => { 533 | numRenderPropsRenders++; 534 | t.equal(theme, 'dark', 'passes the context value correctly'); 535 | return
{theme}
; 536 | }} 537 |
538 | ); 539 | } 540 | 541 | const AsyncChild = prepared(props => { 542 | numPrepares++; 543 | t.equal( 544 | props.data, 545 | 'test', 546 | 'passes props through to prepared component correctly' 547 | ); 548 | return Promise.resolve(); 549 | })(SimplePresentational); 550 | 551 | const app = ( 552 | 553 | 554 | 555 | 556 | ); 557 | const p = prepare(app); 558 | t.ok(p instanceof Promise, 'prepare returns a promise'); 559 | p.then(() => { 560 | t.equal(numPrepares, 2, 'runs prepare function twice'); 561 | t.equal(numRenderPropsRenders, 2, 'prepares consumer render props'); 562 | t.equal(numChildRenders, 2, 'renders SimplePresentational twice'); 563 | 564 | t.equal( 565 | shallow(
{app}
).html(), 566 | '
dark
dark
', 567 | 'passes values via context' 568 | ); 569 | t.end(); 570 | }); 571 | }); 572 | 573 | tape('Preparing React.createContext() with deep async children', t => { 574 | // $FlowFixMe 575 | const {Provider, Consumer} = React.createContext('light'); 576 | 577 | let numChildRenders = 0; 578 | let numPrepares = 0; 579 | let numRenderPropsRenders = 0; 580 | function SimplePresentational() { 581 | numChildRenders++; 582 | return
Hello World
; 583 | } 584 | 585 | const AsyncChild = prepared(props => { 586 | numPrepares++; 587 | t.equal( 588 | props.data, 589 | 'test', 590 | 'passes props through to prepared component correctly' 591 | ); 592 | return Promise.resolve(); 593 | })(SimplePresentational); 594 | 595 | const ConsumerComponent = () => { 596 | return ( 597 | 598 | {theme => { 599 | numRenderPropsRenders++; 600 | t.equal(theme, 'dark'); 601 | return ; 602 | }} 603 | 604 | ); 605 | }; 606 | 607 | const app = ( 608 | 609 | 610 | 611 | ); 612 | const p = prepare(app); 613 | t.ok(p instanceof Promise, 'prepare returns a promise'); 614 | p.then(() => { 615 | t.equal(numPrepares, 1, 'runs prepare function'); 616 | t.equal(numChildRenders, 1, 'prepares SimplePresentational'); 617 | t.equal(numRenderPropsRenders, 1, 'runs render prop function'); 618 | t.end(); 619 | }); 620 | }); 621 | 622 | tape('Preparing React.createContext() using the default provider value', t => { 623 | // $FlowFixMe 624 | const {Consumer} = React.createContext('light'); 625 | 626 | let numChildRenders = 0; 627 | let numPrepares = 0; 628 | let numRenderPropsRenders = 0; 629 | function SimplePresentational() { 630 | numChildRenders++; 631 | return
Hello World
; 632 | } 633 | 634 | const AsyncChild = prepared(props => { 635 | numPrepares++; 636 | t.equal( 637 | props.data, 638 | 'test', 639 | 'passes props through to prepared component correctly' 640 | ); 641 | return Promise.resolve(); 642 | })(SimplePresentational); 643 | 644 | const ConsumerComponent = () => { 645 | return ( 646 | 647 | {theme => { 648 | numRenderPropsRenders++; 649 | t.equal(theme, 'light'); 650 | return ; 651 | }} 652 | 653 | ); 654 | }; 655 | 656 | const app = ; 657 | const p = prepare(app); 658 | t.ok(p instanceof Promise, 'prepare returns a promise'); 659 | p.then(() => { 660 | t.equal(numPrepares, 1, 'runs prepare function'); 661 | t.equal(numChildRenders, 1, 'prepares SimplePresentational'); 662 | t.equal(numRenderPropsRenders, 1, 'runs render prop function'); 663 | t.end(); 664 | }); 665 | }); 666 | -------------------------------------------------------------------------------- /src/__tests__/__node__/split.node.js: -------------------------------------------------------------------------------- 1 | /** Copyright (c) 2018 Uber Technologies, Inc. 2 | * 3 | * This source code is licensed under the MIT license found in the 4 | * LICENSE file in the root directory of this source tree. 5 | * 6 | * @flow 7 | */ 8 | 9 | /* eslint-disable react/no-multi-comp */ 10 | import tape from 'tape-cup'; 11 | import React from 'react'; 12 | import {renderToString} from 'react-dom/server'; 13 | import Provider from '../../prepare-provider'; 14 | import {prepare, split} from '../../index.js'; 15 | 16 | tape('Preparing an app with an async component', async t => { 17 | function DeferredComponent() { 18 | return
Loaded
; 19 | } 20 | function LoadingComponent() { 21 | return
Loading
; 22 | } 23 | function ErrorComponent() { 24 | return
Failed
; 25 | } 26 | 27 | const ToTest = split({ 28 | defer: false, 29 | load: () => Promise.resolve({default: DeferredComponent}), 30 | LoadingComponent, 31 | ErrorComponent, 32 | }); 33 | 34 | const app = ( 35 | 36 | 37 | 38 | ); 39 | 40 | t.ok(/Loading/.test(renderToString(app)), 'starts off loading'); 41 | 42 | await prepare(app); 43 | 44 | t.ok(/Loaded/.test(renderToString(app)), 'ends loaded'); 45 | try { 46 | await prepare(app); 47 | } catch (e) { 48 | t.ifError(e, 'should not error'); 49 | } 50 | t.end(); 51 | }); 52 | 53 | tape('Preparing an app with an errored async component', async t => { 54 | function LoadingComponent() { 55 | return
Loading
; 56 | } 57 | function ErrorComponent() { 58 | return
Failed
; 59 | } 60 | 61 | const ToTest = split({ 62 | defer: false, 63 | load: () => Promise.reject(new Error('failed')), 64 | LoadingComponent, 65 | ErrorComponent, 66 | }); 67 | 68 | const app = ( 69 | 70 | 71 | 72 | ); 73 | 74 | t.ok(/Loading/.test(renderToString(app)), 'starts off loading'); 75 | await prepare(app); 76 | t.ok(/Failed/.test(renderToString(app)), 'ends failed'); 77 | t.end(); 78 | }); 79 | -------------------------------------------------------------------------------- /src/__tests__/index.node.js: -------------------------------------------------------------------------------- 1 | /** Copyright (c) 2018 Uber Technologies, Inc. 2 | * 3 | * This source code is licensed under the MIT license found in the 4 | * LICENSE file in the root directory of this source tree. 5 | * 6 | * @flow 7 | */ 8 | 9 | /* eslint-env node */ 10 | import './__node__/prepare-render.node.js'; 11 | import './__node__/prepare-context.node.js'; 12 | import './__node__/split.node.js'; 13 | import './__node__/context.node.js'; 14 | 15 | process.on('unhandledRejection', e => { 16 | throw e; 17 | }); 18 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /** Copyright (c) 2018 Uber Technologies, Inc. 2 | * 3 | * This source code is licensed under the MIT license found in the 4 | * LICENSE file in the root directory of this source tree. 5 | * 6 | * @flow 7 | */ 8 | 9 | export const REACT_PREPARE = '@__REACT_PREPARE__@'; 10 | -------------------------------------------------------------------------------- /src/dispatched.js: -------------------------------------------------------------------------------- 1 | /** Copyright (c) 2018 Uber Technologies, Inc. 2 | * 3 | * This source code is licensed under the MIT license found in the 4 | * LICENSE file in the root directory of this source tree. 5 | * 6 | * @flow 7 | */ 8 | 9 | import PropTypes from 'prop-types'; 10 | 11 | import prepared from './prepared'; 12 | 13 | const storeShape = PropTypes.shape({ 14 | dispatch: PropTypes.func.isRequired, 15 | }); 16 | 17 | // $FlowFixMe 18 | const dispatched = (prepareUsingDispatch, opts = {}) => OriginalComponent => { 19 | const prepare = (props, context) => { 20 | return prepareUsingDispatch(props, context.store.dispatch); 21 | }; 22 | const contextTypes = Object.assign( 23 | {}, 24 | opts && opts.contextTypes ? opts.contextTypes : {}, 25 | {store: storeShape} 26 | ); 27 | const preparedOpts = Object.assign({}, opts, {contextTypes}); 28 | return prepared(prepare, preparedOpts)(OriginalComponent); 29 | }; 30 | 31 | export default dispatched; 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** Copyright (c) 2018 Uber Technologies, Inc. 2 | * 3 | * This source code is licensed under the MIT license found in the 4 | * LICENSE file in the root directory of this source tree. 5 | * 6 | * @flow 7 | */ 8 | 9 | import * as React from 'react'; 10 | 11 | import dispatched from './dispatched'; 12 | import prepare from './prepare'; 13 | import prepared from './prepared'; 14 | import split from './split'; 15 | import exclude from './traverse-exclude'; 16 | import middleware from './middleware'; //typed 17 | 18 | // eslint-disable-next-line no-console 19 | console.warn(` 20 | Note: "fusion-react-async" has been deprecated and merged into "fusion-react@1.1.0". 21 | Please replace imports to "fusion-react-async" with "fusion-react". 22 | `); 23 | 24 | const prepareTyped: ( 25 | element: React.Element 26 | // $FlowFixMe 27 | ) => Promise> = prepare; 28 | 29 | const preparedTyped: ( 30 | sideEffect: (props: Object, context: Object) => Promise, 31 | opts?: { 32 | defer?: boolean, 33 | boundary?: boolean, 34 | componentDidMount?: boolean, 35 | componentWillReceiveProps?: boolean, 36 | componentDidUpdate?: boolean, 37 | forceUpdate?: boolean, 38 | contextTypes?: Object, 39 | } 40 | ) => ( 41 | Component: React.ComponentType 42 | ) => React.ComponentType = prepared; 43 | 44 | const splitTyped: (opts: { 45 | load: () => Promise, 46 | LoadingComponent: React.ComponentType, 47 | ErrorComponent: React.ComponentType, 48 | }) => React.ComponentType = split; 49 | 50 | const excludeTyped: ( 51 | Component: React.ComponentType 52 | ) => React.ComponentType = exclude; 53 | 54 | // TODO(#3): Can we get ride of some of these exports? 55 | export { 56 | dispatched, 57 | prepareTyped as prepare, 58 | preparedTyped as prepared, 59 | splitTyped as split, 60 | excludeTyped as exclude, 61 | middleware, 62 | }; 63 | -------------------------------------------------------------------------------- /src/middleware.js: -------------------------------------------------------------------------------- 1 | /** Copyright (c) 2018 Uber Technologies, Inc. 2 | * 3 | * This source code is licensed under the MIT license found in the 4 | * LICENSE file in the root directory of this source tree. 5 | * 6 | * @flow 7 | */ 8 | 9 | import React from 'react'; 10 | 11 | import type {Middleware} from 'fusion-core'; 12 | 13 | import PrepareProvider from './prepare-provider'; 14 | 15 | const middleware: Middleware = function(ctx, next) { 16 | if (__NODE__ && !ctx.element) { 17 | return next(); 18 | } 19 | ctx.element = ( 20 | 21 | {ctx.element} 22 | 23 | ); 24 | return next(); 25 | }; 26 | 27 | export default middleware; 28 | -------------------------------------------------------------------------------- /src/prepare-provider.js: -------------------------------------------------------------------------------- 1 | /** Copyright (c) 2018 Uber Technologies, Inc. 2 | * 3 | * This source code is licensed under the MIT license found in the 4 | * LICENSE file in the root directory of this source tree. 5 | * 6 | * @flow 7 | */ 8 | 9 | import React from 'react'; 10 | import PropTypes from 'prop-types'; 11 | 12 | class PrepareProvider extends React.Component<*, *> { 13 | constructor(props: any, context: any) { 14 | super(props, context); 15 | this.splitComponentLoaders = []; 16 | this.preloadChunks = props.preloadChunks; 17 | } 18 | 19 | preloadChunks: any; 20 | splitComponentLoaders: Array; 21 | 22 | getChildContext() { 23 | return { 24 | splitComponentLoaders: this.splitComponentLoaders, 25 | preloadChunks: this.preloadChunks, 26 | }; 27 | } 28 | render() { 29 | return React.Children.only(this.props.children); 30 | } 31 | } 32 | 33 | PrepareProvider.childContextTypes = { 34 | splitComponentLoaders: PropTypes.array.isRequired, 35 | preloadChunks: PropTypes.array.isRequired, 36 | }; 37 | 38 | export default PrepareProvider; 39 | -------------------------------------------------------------------------------- /src/prepare.js: -------------------------------------------------------------------------------- 1 | /** Copyright (c) 2018 Uber Technologies, Inc. 2 | * 3 | * This source code is licensed under the MIT license found in the 4 | * LICENSE file in the root directory of this source tree. 5 | * 6 | * @flow 7 | */ 8 | 9 | import React from 'react'; 10 | import {isFragment, isContextConsumer, isContextProvider} from 'react-is'; 11 | 12 | import isReactCompositeComponent from './utils/isReactCompositeComponent'; 13 | import {isPrepared, getPrepare} from './prepared'; 14 | 15 | function renderCompositeElementInstance(instance) { 16 | const childContext = Object.assign( 17 | {}, 18 | instance.context, 19 | instance.getChildContext ? instance.getChildContext() : {} 20 | ); 21 | if (instance.componentWillMount) { 22 | instance.componentWillMount(); 23 | } 24 | const children = instance.render(); 25 | return [children, childContext]; 26 | } 27 | 28 | function prepareComponentInstance(instance) { 29 | if (!isPrepared(instance)) { 30 | return Promise.resolve({}); 31 | } 32 | const prepareConfig = getPrepare(instance); 33 | // If the component is deferred, skip the prepare step 34 | if (prepareConfig.defer) { 35 | return Promise.resolve(prepareConfig); 36 | } 37 | // $FlowFixMe 38 | return prepareConfig.prepare(instance.props, instance.context).then(() => { 39 | return prepareConfig; 40 | }); 41 | } 42 | 43 | function prepareElement(element, context) { 44 | if (element === null || typeof element !== 'object') { 45 | return Promise.resolve([null, context]); 46 | } 47 | const {type, props} = element; 48 | if (isContextConsumer(element)) { 49 | return Promise.resolve([props.children(type._currentValue), context]); 50 | } 51 | if (isContextProvider(element)) { 52 | type._context._currentValue = props.value; 53 | return Promise.resolve([props.children, context]); 54 | } 55 | if (typeof type === 'string' || isFragment(element)) { 56 | return Promise.resolve([props.children, context]); 57 | } 58 | if (!isReactCompositeComponent(type)) { 59 | return Promise.resolve([type(props, context), context]); 60 | } 61 | const CompositeComponent = type; 62 | const instance = new CompositeComponent(props, context); 63 | instance.props = props; 64 | instance.context = context; 65 | return prepareComponentInstance(instance).then(prepareConfig => { 66 | // Stop traversing if the component is defer or boundary 67 | if (prepareConfig.defer || prepareConfig.boundary) { 68 | return Promise.resolve([null, context]); 69 | } 70 | return renderCompositeElementInstance(instance); 71 | }); 72 | } 73 | 74 | // TODO(#4) We can optimize this algorithm I think 75 | function _prepare(element, context) { 76 | return prepareElement(element, context).then(([children, childContext]) => { 77 | return Promise.all( 78 | React.Children.toArray(children).map(child => 79 | _prepare(child, childContext) 80 | ) 81 | ); 82 | }); 83 | } 84 | 85 | function prepare(element: any, context: any = {}) { 86 | context.__IS_PREPARE__ = true; 87 | return _prepare(element, context).then(() => { 88 | context.__IS_PREPARE__ = false; 89 | }); 90 | } 91 | 92 | export default prepare; 93 | -------------------------------------------------------------------------------- /src/prepared.js: -------------------------------------------------------------------------------- 1 | /** Copyright (c) 2018 Uber Technologies, Inc. 2 | * 3 | * This source code is licensed under the MIT license found in the 4 | * LICENSE file in the root directory of this source tree. 5 | * 6 | * @flow 7 | */ 8 | 9 | import React, {Component} from 'react'; 10 | 11 | import {REACT_PREPARE} from './constants'; 12 | 13 | // $FlowFixMe 14 | const prepared = (sideEffect, opts = {}) => OriginalComponent => { 15 | opts = Object.assign( 16 | { 17 | boundary: false, 18 | defer: false, 19 | componentDidMount: true, 20 | componentWillReceiveProps: false, 21 | componentDidUpdate: false, 22 | contextTypes: {}, 23 | forceUpdate: false, 24 | }, 25 | opts 26 | ); 27 | const prep = { 28 | prepare: (...args) => Promise.resolve(sideEffect(...args)), 29 | defer: opts.defer, 30 | }; 31 | // Disable eslint for deprecated componentWillReceiveProps 32 | // eslint-disable-next-line react/no-deprecated 33 | class PreparedComponent extends Component<*, *> { 34 | // $FlowFixMe 35 | constructor(props, context) { 36 | super(props, context); 37 | // $FlowFixMe 38 | this[REACT_PREPARE] = prep; 39 | } 40 | componentDidMount() { 41 | if (opts.componentDidMount) { 42 | Promise.resolve(sideEffect(this.props, this.context)).then(() => { 43 | if (opts.forceUpdate) { 44 | this.forceUpdate(); // TODO(#10) document 45 | } 46 | }); 47 | } 48 | } 49 | 50 | // $FlowFixMe 51 | componentWillReceiveProps(nextProps, nextContext) { 52 | if (opts.componentWillReceiveProps) { 53 | sideEffect(nextProps, nextContext); 54 | } 55 | } 56 | 57 | componentDidUpdate() { 58 | if (opts.componentDidUpdate) { 59 | sideEffect(this.props, this.context); 60 | } 61 | } 62 | 63 | render() { 64 | return ; 65 | } 66 | } 67 | 68 | const displayName = 69 | OriginalComponent.displayName || OriginalComponent.name || ''; 70 | PreparedComponent.contextTypes = opts.contextTypes; 71 | PreparedComponent.displayName = `PreparedComponent(${displayName})`; 72 | 73 | return PreparedComponent; 74 | }; 75 | 76 | // $FlowFixMe 77 | function isPrepared(CustomComponent) { 78 | return ( 79 | CustomComponent[REACT_PREPARE] && 80 | typeof CustomComponent[REACT_PREPARE].prepare === 'function' 81 | ); 82 | } 83 | 84 | // $FlowFixMe 85 | function getPrepare(CustomComponent) { 86 | return CustomComponent[REACT_PREPARE] || {}; 87 | } 88 | 89 | export {isPrepared, getPrepare}; 90 | export default prepared; 91 | -------------------------------------------------------------------------------- /src/split.js: -------------------------------------------------------------------------------- 1 | /** Copyright (c) 2018 Uber Technologies, Inc. 2 | * 3 | * This source code is licensed under the MIT license found in the 4 | * LICENSE file in the root directory of this source tree. 5 | * 6 | * @flow 7 | */ 8 | 9 | import React from 'react'; 10 | import PropTypes from 'prop-types'; 11 | import prepared from './prepared.js'; 12 | 13 | // TODO(#6): Dry this up 14 | const CHUNKS_KEY = '__CHUNK_IDS'; 15 | 16 | const contextTypes = { 17 | splitComponentLoaders: PropTypes.array.isRequired, 18 | }; 19 | 20 | // TODO(#7): Figure out what we are going to do with chunks/preloading 21 | if (__NODE__) { 22 | // $FlowFixMe 23 | contextTypes.preloadChunks = PropTypes.array.isRequired; 24 | } 25 | 26 | // $FlowFixMe 27 | export default function withAsyncComponent({ 28 | defer, 29 | load, 30 | LoadingComponent, 31 | ErrorComponent, 32 | }) { 33 | let AsyncComponent = null; 34 | let error = null; 35 | let chunkIds = []; 36 | 37 | function WithAsyncComponent(props) { 38 | if (error) { 39 | return ; 40 | } 41 | if (!AsyncComponent) { 42 | return ; 43 | } 44 | return ; 45 | } 46 | return prepared( 47 | (props, context) => { 48 | if (AsyncComponent) { 49 | if (__NODE__) { 50 | chunkIds.forEach(chunkId => { 51 | context.preloadChunks.push(chunkId); 52 | }); 53 | } 54 | return Promise.resolve(AsyncComponent); 55 | } 56 | 57 | let componentPromise; 58 | try { 59 | componentPromise = load(); 60 | } catch (e) { 61 | componentPromise = Promise.reject(e); 62 | } 63 | // $FlowFixMe 64 | chunkIds = componentPromise[CHUNKS_KEY] || []; 65 | 66 | if (__NODE__) { 67 | chunkIds.forEach(chunkId => { 68 | context.preloadChunks.push(chunkId); 69 | }); 70 | } 71 | 72 | const loadPromises = [ 73 | componentPromise, 74 | ...context.splitComponentLoaders.map(loader => loader(chunkIds)), 75 | ]; 76 | 77 | return Promise.all(loadPromises) 78 | .then(([asyncComponent]) => { 79 | // TODO(#8) .default is toolchain specific, breaks w/ CommonJS exports 80 | AsyncComponent = asyncComponent.default; 81 | }) 82 | .catch(err => { 83 | error = err; 84 | if (__BROWSER__) 85 | setTimeout(() => { 86 | throw err; 87 | }); // log error 88 | }); 89 | }, 90 | {defer, contextTypes, forceUpdate: true} 91 | )(WithAsyncComponent); 92 | } 93 | -------------------------------------------------------------------------------- /src/traverse-exclude.js: -------------------------------------------------------------------------------- 1 | /** Copyright (c) 2018 Uber Technologies, Inc. 2 | * 3 | * This source code is licensed under the MIT license found in the 4 | * LICENSE file in the root directory of this source tree. 5 | * 6 | * @flow 7 | */ 8 | 9 | import prepared from './prepared.js'; 10 | 11 | // Stops the traversal at this node. Useful for optimizing the prepare traversal 12 | // to visit the minimum number of nodes 13 | export default prepared(Promise.resolve(), { 14 | componentDidMount: false, 15 | componentWillReceiveProps: false, 16 | componentDidUpdate: false, 17 | defer: true, 18 | }); 19 | -------------------------------------------------------------------------------- /src/utils/isReactCompositeComponent.js: -------------------------------------------------------------------------------- 1 | /** Copyright (c) 2018 Uber Technologies, Inc. 2 | * 3 | * This source code is licensed under the MIT license found in the 4 | * LICENSE file in the root directory of this source tree. 5 | * 6 | * @flow 7 | */ 8 | 9 | export default function isReactCompositeComponent(type: mixed) { 10 | if (typeof type !== 'function') { 11 | return false; 12 | } 13 | if (typeof type.prototype !== 'object') { 14 | return false; 15 | } 16 | if (typeof type.prototype.render !== 'function') { 17 | return false; 18 | } 19 | return true; 20 | } 21 | --------------------------------------------------------------------------------