├── .changeset └── config.json ├── .editorconfig ├── .flowconfig ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package.json ├── rollup.config.js ├── scripts ├── copy-typings.js ├── react-ssr-prepass.d.ts └── react-ssr-prepass.js.flow ├── src ├── __tests__ │ ├── concurrency.test.js │ ├── element.test.js │ ├── error-boundaries.test.js │ ├── suspense.test.js │ └── visitor.test.js ├── element.js ├── index.js ├── internals │ ├── __tests__ │ │ ├── context.test.js │ │ └── dispatcher.test.js │ ├── context.js │ ├── dispatcher.js │ ├── error.js │ ├── index.js │ ├── objectIs.js │ └── state.js ├── render │ ├── classComponent.js │ ├── functionComponent.js │ ├── index.js │ └── lazyComponent.js ├── symbols.js ├── types │ ├── element.js │ ├── frames.js │ ├── index.js │ ├── input.js │ └── state.js └── visitor.js └── yarn.lock /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "access": "public", 5 | "baseBranch": "master", 6 | "updateInternalDependencies": "patch" 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_size = 2 6 | end_of_line = lf 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/@babel/[A-Z-a-z-]*/.* 3 | .*/node_modules/babel[A-Z-a-z-]*/.* 4 | .*/node_modules/resolve/.* 5 | 6 | [untyped] 7 | .*/node_modules/react-is/.* 8 | .*/node_modules/styled-components/.* 9 | 10 | [include] 11 | .*/node_modules/react[A-Z-a-z-]*/.* 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | check_and_build: 10 | name: Check and build codebase 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | cache: 'yarn' 17 | node-version: 18 18 | 19 | - name: Installation 20 | run: yarn --frozen-lockfile 21 | 22 | - name: Build 23 | run: yarn build 24 | 25 | - name: Unit Tests 26 | run: yarn test 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | id-token: write 13 | issues: write 14 | repository-projects: write 15 | deployments: write 16 | packages: write 17 | pull-requests: write 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | token: ${{ secrets.CHANGESETS_TOKEN }} 22 | 23 | - uses: actions/setup-node@v4 24 | with: 25 | cache: 'yarn' 26 | node-version: 18 27 | 28 | - name: Install dependencies 29 | run: yarn install --frozen-lockfile 30 | 31 | - name: Build 32 | run: yarn build 33 | 34 | - name: Unit Tests 35 | run: yarn test 36 | 37 | - name: PR or Publish 38 | id: changesets 39 | uses: changesets/action@v1 40 | with: 41 | version: yarn changeset version 42 | publish: yarn changeset publish 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.CHANGESETS_TOKEN }} 45 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | yarn-error.log* 4 | package-lock.json 5 | dist 6 | .DS_Store 7 | coverage 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: node_js 3 | notifications: 4 | email: 5 | on_success: change 6 | on_failure: always 7 | 8 | cache: yarn 9 | 10 | jobs: 11 | include: 12 | - stage: Test 13 | name: Jest 14 | node_js: 10 15 | script: 16 | - NODE_ENV=production yarn run test 17 | - yarn run test --coverage 18 | - yarn run codecov 19 | - stage: Build 20 | name: Build 21 | node_js: 10 22 | script: 23 | - yarn run build 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # react-ssr-prepass 2 | 3 | ## 1.6.0 4 | 5 | ### Minor Changes 6 | 7 | - c3228c4: Add support for React 19 8 | 9 | ## 1.5.0 10 | 11 | ### Minor Changes 12 | 13 | - b7cf762: Support for React 19 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at lauren.eastridge@formidable.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Thanks for contributing! We love seeing continuous improvements 4 | and enhancements, no matter how small or big they might be. 5 | 6 | ## How to contribute? 7 | 8 | We follow fairly standard but lenient rules around pull requests and issues. 9 | Please pick a title that describes your change briefly, optionally in the imperative 10 | mood if possible. 11 | 12 | If you have an idea for a feature or want to fix a bug, consider opening an issue 13 | first. We're also happy to discuss and help you open a PR and get your changes 14 | in! 15 | 16 | ## How do I set up the project? 17 | 18 | Luckily it's not hard to get started. You can install dependencies using yarn. 19 | Please don't use `npm` to respect the lockfile. 20 | 21 | ```sh 22 | yarn 23 | ``` 24 | 25 | You can then run the build using: 26 | 27 | ```sh 28 | yarn build 29 | ``` 30 | 31 | And you can run Flow to check for any type errors: 32 | 33 | ```sh 34 | yarn flow check 35 | ``` 36 | 37 | ## How do I test my changes? 38 | 39 | It's always good practice to run the tests when making changes. 40 | It might also make sense to add more tests when you're adding features 41 | or fixing a bug, but we'll help you in the pull request, if necessary. 42 | 43 | ```sh 44 | yarn test # Single pass 45 | yarn test --watch # Watched 46 | yarn test --coverage # Single pass coverage report 47 | ``` 48 | 49 | Sometimes it's a good idea to run the Jest in `production` mode, 50 | since some data structures and behaviour changes in React in 51 | `production`: 52 | 53 | ```sh 54 | NODE_ENV=production yarn test 55 | ``` 56 | 57 | ## How do I lint my code? 58 | 59 | We ensure consistency in this codebase using `prettier`. 60 | It's run on a `precommit` hook, so if something's off it'll try 61 | to automatically fix up your code, or display an error. 62 | 63 | If you have them set up in your editor, even better! 64 | 65 | ## How do I publish a new version? 66 | 67 | If you're a core contributor or maintainer this will certainly come 68 | up once in a while. 69 | 70 | Make sure you first create a new version. The following commands 71 | bump the version in the `package.json`, create a commit, 72 | and tag the commit on git: 73 | 74 | ```sh 75 | yarn version --new-version X 76 | # or 77 | npm version patch # accepts patch|minor|major 78 | ``` 79 | 80 | Then run `npm publish` (npm is recommended here, not yarn) 81 | And maybe run `npm publish --dry-run` first to check the output. 82 | 83 | ```sh 84 | npm publish 85 | ``` 86 | 87 | There's a `prepublishOnly` hook in place that'll clean and build 88 | the package automatically. 89 | 90 | Don't forget to push afterwards: 91 | 92 | ```sh 93 | git push && git push --tags 94 | ``` 95 | 96 | [This process can be simplified and streamlined by using `np`.](https://github.com/sindresorhus/np) 97 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Formidable 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-ssr-prepass 2 | 3 |

4 | 5 | Build Status 6 | 7 | 8 | Test Coverage 9 | 10 | 11 | NPM Version 12 | 13 | 14 | Maintenance Status 15 | 16 |

17 | 18 |

19 | react-dom/server does not have support for suspense yet.
20 | react-ssr-prepass offers suspense on the server-side today, until it does. ✨ 21 |

22 | 23 | `react-ssr-prepass` is a **partial server-side React renderer** that does a prepass 24 | on a React element tree and suspends when it finds thrown promises. It also 25 | accepts a visitor function that can be used to suspend on anything. 26 | 27 | You can use it to fetch data before your SSR code calls `renderToString` or 28 | `renderToNodeStream`. 29 | 30 | > ⚠️ **Note:** Suspense is unstable and experimental. This library purely 31 | > exists since `react-dom/server` does not support data fetching or suspense 32 | > yet. This two-pass approach should just be used until server-side suspense 33 | > support lands in React. 34 | 35 | ## The Why & How 36 | 37 | It's quite common to have some data that needs to be fetched before 38 | server-side rendering and often it's inconvenient to specifically call 39 | out to random fetch calls to get some data. Instead **Suspense** 40 | offers a practical way to automatically fetch some required data, 41 | but is currently only supported in client-side React. 42 | 43 | `react-ssr-prepass` offers a solution by being a "prepass" function 44 | that walks a React element tree and executing suspense. It finds all 45 | thrown promises (a custom visitor can also be provided) and waits for 46 | those promises to resolve before continuing to walk that particular 47 | suspended subtree. Hence, it attempts to offer a practical way to 48 | use suspense and complex data fetching logic today. 49 | 50 | A two-pass React render is already quite common for in other libraries 51 | that do implement data fetching. This has however become quite impractical. 52 | While it was trivial to previously implement a primitive React renderer, 53 | these days a lot more moving parts are involved to make such a renderer 54 | correct and stable. This is why some implementations now simply rely 55 | on calling `renderToStaticMarkup` repeatedly. 56 | 57 | `react-ssr-prepass` on the other hand is a custom implementation 58 | of a React renderer. It attempts to stay true and correct to the 59 | React implementation by: 60 | 61 | - Mirroring some of the implementation of `ReactPartialRenderer` 62 | - Leaning on React elements' symbols from `react-is` 63 | - Providing only the simplest support for suspense 64 | 65 | ## Quick Start Guide 66 | 67 | First install `react-ssr-prepass` alongside `react` and `react-dom`: 68 | 69 | ```sh 70 | yarn add react-ssr-prepass 71 | # or 72 | npm install --save react-ssr-prepass 73 | ``` 74 | 75 | In your SSR code you may now add it in front of your usual `renderToString` 76 | or `renderToNodeStream` code: 77 | 78 | ```js 79 | import { createElement } from 'react' 80 | import { renderToString } from 'react-dom/server' 81 | 82 | import ssrPrepass from 'react-ssr-prepass' 83 | 84 | const renderApp = async (App) => { 85 | const element = createElement(App) 86 | await ssrPrepass(element) 87 | 88 | return renderToString(element) 89 | } 90 | ``` 91 | 92 | Additionally you can also pass a "visitor function" as your second argument. 93 | This function is called for every React class or function element that is 94 | encountered. 95 | 96 | ```js 97 | ssrPrepass(, (element, instance) => { 98 | if (element.type === SomeData) { 99 | return fetchData() 100 | } else if (instance && instance.fetchData) { 101 | return instance.fetchData() 102 | } 103 | }) 104 | ``` 105 | 106 | The first argument of the visitor is the React element. The second is 107 | the instance of a class component or undefined. When you return 108 | a promise from this function `react-ssr-prepass` will suspend before 109 | rendering this element. 110 | 111 | You should be aware that `react-ssr-prepass` does not handle any 112 | data rehydration. In most cases it's fine to collect data from your cache 113 | or store after running `ssrPrepass`, turn it into JSON, and send it 114 | down in your HTML result. 115 | 116 | ## Prior Art 117 | 118 | This library is (luckily) not a reimplementation from scratch of 119 | React's server-side rendering. Instead it's mostly based on 120 | React's own server-side rendering logic that resides in its 121 | [`ReactPartialRenderer`](https://github.com/facebook/react/blob/13645d2/packages/react-dom/src/server/ReactPartialRenderer.js). 122 | 123 | The approach of doing an initial "data fetching pass" is inspired by: 124 | 125 | - [`react-apollo`'s `getDataFromTree`](https://github.com/apollographql/react-apollo/blob/master/src/getDataFromTree.ts) 126 | - [`react-tree-walker`](https://github.com/ctrlplusb/react-tree-walker) 127 | 128 | ## Maintenance Status 129 | 130 | **Experimental:** This project is quite new. We're not sure what our ongoing maintenance plan for this project will be. Bug reports, feature requests and pull requests are welcome. If you like this project, let us know! 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-ssr-prepass", 3 | "version": "1.6.0", 4 | "description": "A custom partial React SSR renderer for prefetching and suspense", 5 | "main": "dist/react-ssr-prepass.js", 6 | "module": "dist/react-ssr-prepass.es.js", 7 | "types": "dist/react-ssr-prepass.d.ts", 8 | "author": "Phil Plückthun ", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/FormidableLabs/react-ssr-prepass.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/FormidableLabs/react-ssr-prepass/issues" 16 | }, 17 | "files": [ 18 | "dist" 19 | ], 20 | "sideEffects": false, 21 | "scripts": { 22 | "prepublishOnly": "run-s flow test build", 23 | "build": "rollup -c rollup.config.js", 24 | "postbuild": "node ./scripts/copy-typings.js", 25 | "test": "jest", 26 | "flow": "flow check" 27 | }, 28 | "prettier": { 29 | "semi": false, 30 | "singleQuote": true, 31 | "trailingComma": "none" 32 | }, 33 | "babel": { 34 | "presets": [ 35 | "@babel/preset-env", 36 | "@babel/preset-flow", 37 | "@babel/preset-react" 38 | ] 39 | }, 40 | "lint-staged": { 41 | "**/*.js": [ 42 | "flow focus-check", 43 | "prettier --write" 44 | ], 45 | "**/*.{json,md}": [ 46 | "prettier --write" 47 | ] 48 | }, 49 | "husky": { 50 | "hooks": { 51 | "pre-commit": "lint-staged" 52 | } 53 | }, 54 | "peerDependencies": { 55 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" 56 | }, 57 | "devDependencies": { 58 | "@babel/core": "^7.16.7", 59 | "@babel/plugin-transform-flow-strip-types": "^7.16.7", 60 | "@babel/plugin-transform-object-assign": "^7.16.7", 61 | "@babel/preset-env": "^7.16.7", 62 | "@babel/preset-flow": "^7.16.7", 63 | "@babel/preset-react": "^7.16.7", 64 | "@changesets/cli": "^2.27.9", 65 | "@rollup/plugin-babel": "^5.3.0", 66 | "@rollup/plugin-buble": "^0.21.3", 67 | "@rollup/plugin-commonjs": "^21.0.1", 68 | "@rollup/plugin-node-resolve": "^13.1.3", 69 | "babel-plugin-closure-elimination": "^1.3.2", 70 | "babel-plugin-transform-async-to-promises": "^0.8.18", 71 | "codecov": "^3.8.3", 72 | "flow-bin": "0.122.0", 73 | "husky-v4": "^4.3.0", 74 | "jest": "^27.4.7", 75 | "lint-staged": "^12.1.5", 76 | "npm-run-all": "^4.1.5", 77 | "prettier": "^2.5.1", 78 | "react": "^17.0.2", 79 | "react-dom": "^17.0.2", 80 | "rollup": "^2.63.0", 81 | "rollup-plugin-babel": "^4.4.0", 82 | "rollup-plugin-terser": "^7.0.2" 83 | }, 84 | "publishConfig": { 85 | "provenance": true 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs' 2 | import resolve from '@rollup/plugin-node-resolve' 3 | import buble from '@rollup/plugin-buble' 4 | import babel from '@rollup/plugin-babel' 5 | import { terser } from 'rollup-plugin-terser' 6 | 7 | const pkg = require('./package.json') 8 | 9 | const externalModules = [ 10 | 'dns', 11 | 'fs', 12 | 'path', 13 | 'url', 14 | ...Object.keys(pkg.peerDependencies || {}), 15 | ...Object.keys(pkg.dependencies || {}) 16 | ] 17 | 18 | const externalPredicate = new RegExp(`^(${externalModules.join('|')})($|/)`) 19 | const externalTest = (id) => { 20 | if (id === 'babel-plugin-transform-async-to-promises/helpers') { 21 | return false 22 | } 23 | 24 | return externalPredicate.test(id) 25 | } 26 | 27 | const plugins = [ 28 | babel({ 29 | babelrc: false, 30 | babelHelpers: 'bundled', 31 | exclude: 'node_modules/**', 32 | presets: [], 33 | plugins: ['@babel/plugin-transform-flow-strip-types'] 34 | }), 35 | resolve({ 36 | dedupe: externalModules, 37 | mainFields: ['module', 'jsnext', 'main'], 38 | browser: true 39 | }), 40 | commonjs({ 41 | ignoreGlobal: true, 42 | include: /\/node_modules\//, 43 | namedExports: { 44 | react: Object.keys(require('react')) 45 | } 46 | }), 47 | buble({ 48 | transforms: { 49 | unicodeRegExp: false, 50 | dangerousForOf: true, 51 | dangerousTaggedTemplateString: true 52 | }, 53 | objectAssign: 'Object.assign', 54 | exclude: 'node_modules/**' 55 | }), 56 | babel({ 57 | babelrc: false, 58 | babelHelpers: 'bundled', 59 | exclude: 'node_modules/**', 60 | presets: [], 61 | plugins: [ 62 | 'babel-plugin-closure-elimination', 63 | '@babel/plugin-transform-object-assign', 64 | [ 65 | 'babel-plugin-transform-async-to-promises', 66 | { 67 | inlineHelpers: true, 68 | externalHelpers: true 69 | } 70 | ] 71 | ] 72 | }), 73 | terser({ 74 | warnings: true, 75 | ecma: 5, 76 | keep_fnames: true, 77 | ie8: false, 78 | compress: { 79 | pure_getters: true, 80 | toplevel: true, 81 | booleans_as_integers: false, 82 | keep_fnames: true, 83 | keep_fargs: true, 84 | if_return: false, 85 | ie8: false, 86 | sequences: false, 87 | loops: false, 88 | conditionals: false, 89 | join_vars: false 90 | }, 91 | mangle: { 92 | module: true, 93 | keep_fnames: true 94 | }, 95 | output: { 96 | beautify: true, 97 | braces: true, 98 | indent_level: 2 99 | } 100 | }) 101 | ] 102 | 103 | export default { 104 | input: './src/index.js', 105 | onwarn: () => {}, 106 | external: externalTest, 107 | treeshake: { 108 | propertyReadSideEffects: false 109 | }, 110 | plugins, 111 | output: [ 112 | { 113 | sourcemap: true, 114 | freeze: false, 115 | // NOTE: *.mjs files will lead to issues since react is still a non-ESM package 116 | // the same goes for package.json:exports 117 | file: './dist/react-ssr-prepass.es.js', 118 | format: 'esm' 119 | }, 120 | { 121 | sourcemap: true, 122 | freeze: false, 123 | file: './dist/react-ssr-prepass.js', 124 | format: 'cjs' 125 | } 126 | ] 127 | } 128 | -------------------------------------------------------------------------------- /scripts/copy-typings.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path') 4 | const fs = require('fs') 5 | 6 | fs.copyFileSync( 7 | path.resolve(__dirname, 'react-ssr-prepass.d.ts'), 8 | path.resolve(__dirname, '../dist/react-ssr-prepass.d.ts') 9 | ) 10 | 11 | fs.copyFileSync( 12 | path.resolve(__dirname, 'react-ssr-prepass.js.flow'), 13 | path.resolve(__dirname, '../dist/react-ssr-prepass.js.flow') 14 | ) 15 | 16 | fs.copyFileSync( 17 | path.resolve(__dirname, 'react-ssr-prepass.js.flow'), 18 | path.resolve(__dirname, '../dist/react-ssr-prepass.es.js.flow') 19 | ) 20 | -------------------------------------------------------------------------------- /scripts/react-ssr-prepass.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-ssr-prepass' { 2 | type Visitor = ( 3 | element: React.ElementType, 4 | instance?: React.Component 5 | ) => void | Promise 6 | 7 | function ssrPrepass(node: React.ReactNode, visitor?: Visitor): Promise 8 | 9 | export = ssrPrepass 10 | } 11 | -------------------------------------------------------------------------------- /scripts/react-ssr-prepass.js.flow: -------------------------------------------------------------------------------- 1 | /* @flow strict */ 2 | 3 | export type Visitor = ( 4 | element: React$Element, 5 | instance?: React$Component 6 | ) => void | Promise 7 | 8 | declare module.exports: ( 9 | node: React$Node, 10 | visitor?: Visitor 11 | ) => Promise; 12 | -------------------------------------------------------------------------------- /src/__tests__/concurrency.test.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | useContext, 4 | useState, 5 | useMemo, 6 | useRef, 7 | useCallback 8 | } from 'react' 9 | import renderPrepass from '..' 10 | 11 | const CONCURRENCY = 2 12 | 13 | const Context = createContext({ test: 3, promise: null, resolved: false }) 14 | 15 | function makeApp() { 16 | return 17 | } 18 | 19 | function App() { 20 | const [state, setState] = useState(() => ({ 21 | test: Math.random(), 22 | promise: null, 23 | resolved: false 24 | })) 25 | const refresh = () => 26 | setState({ test: Math.random(), promise: null, resolved: false }) 27 | 28 | return ( 29 | 30 | 31 | 32 | ) 33 | } 34 | 35 | function Outer() { 36 | useRef({ 37 | test: 1 38 | }) 39 | 40 | const [, refresh] = useSuspenseHook() 41 | 42 | useMemo(() => { 43 | return { a: 1, b: 2 } 44 | }, []) 45 | 46 | return ( 47 |
48 | 49 | 50 |
51 | ) 52 | } 53 | 54 | function useSuspenseHook() { 55 | const context = useContext(Context) 56 | 57 | useRef({ 58 | test: 1 59 | }) 60 | 61 | if (!context.resolved && !context.promise) { 62 | context.promise = new Promise(resolve => 63 | setTimeout(resolve, Math.floor(30 + Math.random() * 50)) 64 | ).then(() => { 65 | context.resolved = true 66 | context.promise = null 67 | }) 68 | } 69 | 70 | if (context.promise) throw context.promise 71 | 72 | return [true, context.refresh] 73 | } 74 | 75 | function Inner() { 76 | const [state] = useState({ a: 3 }) 77 | 78 | useCallback(() => { 79 | return state 80 | }, [state]) 81 | 82 | return

Inner

83 | } 84 | 85 | test('concurrency', () => { 86 | return expect( 87 | Promise.all( 88 | new Array(CONCURRENCY).fill(0).map(() => renderPrepass(makeApp())) 89 | ) 90 | ).resolves.not.toThrow() 91 | }) 92 | -------------------------------------------------------------------------------- /src/__tests__/element.test.js: -------------------------------------------------------------------------------- 1 | import * as is from 'react-is' 2 | import { typeOf } from '../element' 3 | 4 | import { 5 | REACT_ELEMENT_TYPE, 6 | REACT_PORTAL_TYPE, 7 | REACT_FRAGMENT_TYPE, 8 | REACT_STRICT_MODE_TYPE, 9 | REACT_PROFILER_TYPE, 10 | REACT_PROVIDER_TYPE, 11 | REACT_CONTEXT_TYPE, 12 | REACT_CONCURRENT_MODE_TYPE, 13 | REACT_FORWARD_REF_TYPE, 14 | REACT_SUSPENSE_TYPE, 15 | REACT_MEMO_TYPE, 16 | REACT_LAZY_TYPE 17 | } from '../symbols' 18 | 19 | describe('typeOf', () => { 20 | it('correctly identifies all elements', () => { 21 | expect(typeOf({})).toBe(undefined) 22 | 23 | expect( 24 | typeOf({ 25 | $$typeof: is.Portal 26 | }) 27 | ).toBe(REACT_PORTAL_TYPE) 28 | 29 | expect( 30 | typeOf({ 31 | $$typeof: is.Element, 32 | type: is.Fragment 33 | }) 34 | ).toBe(REACT_FRAGMENT_TYPE) 35 | 36 | expect( 37 | typeOf({ 38 | $$typeof: is.Element, 39 | type: is.Profiler 40 | }) 41 | ).toBe(REACT_PROFILER_TYPE) 42 | 43 | expect( 44 | typeOf({ 45 | $$typeof: is.Element, 46 | type: is.StrictMode 47 | }) 48 | ).toBe(REACT_STRICT_MODE_TYPE) 49 | 50 | expect( 51 | typeOf({ 52 | $$typeof: is.Element, 53 | type: is.Suspense 54 | }) 55 | ).toBe(REACT_SUSPENSE_TYPE) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /src/__tests__/error-boundaries.test.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import renderPrepass from '..' 3 | 4 | it('returns to the next componentDidCatch boundary on erroring', () => { 5 | const Throw = jest.fn(() => { 6 | throw new Error() 7 | }) 8 | const Inner = jest.fn(() => null) 9 | 10 | class Outer extends Component { 11 | constructor() { 12 | super() 13 | this.state = { error: false } 14 | } 15 | 16 | componentDidCatch(error) { 17 | this.setState({ error: true }) 18 | } 19 | 20 | render() { 21 | return this.state.error ? : 22 | } 23 | } 24 | 25 | const render$ = renderPrepass() 26 | expect(Throw).toHaveBeenCalledTimes(1) 27 | expect(Inner).not.toHaveBeenCalled() 28 | 29 | return render$.then(() => { 30 | expect(Inner).toHaveBeenCalledTimes(1) 31 | }) 32 | }) 33 | 34 | it('returns to the next getDerivedStateFromError boundary on erroring', () => { 35 | const Throw = jest.fn(() => { 36 | throw new Error() 37 | }) 38 | const Inner = jest.fn(() => null) 39 | 40 | class Outer extends Component { 41 | static getDerivedStateFromProps() { 42 | return { error: false } 43 | } 44 | 45 | static getDerivedStateFromError() { 46 | return { error: true } 47 | } 48 | 49 | render() { 50 | return this.state.error ? : 51 | } 52 | } 53 | 54 | const render$ = renderPrepass() 55 | expect(Throw).toHaveBeenCalledTimes(1) 56 | expect(Inner).not.toHaveBeenCalled() 57 | 58 | return render$.then(() => { 59 | expect(Inner).toHaveBeenCalledTimes(1) 60 | }) 61 | }) 62 | 63 | it('guards against infinite render loops', () => { 64 | const Throw = jest.fn(() => { 65 | throw new Error() 66 | }) 67 | 68 | class Outer extends Component { 69 | componentDidCatch() {} // NOTE: This doesn't actually recover from errors 70 | render() { 71 | return 72 | } 73 | } 74 | 75 | return renderPrepass().then(() => { 76 | expect(Throw).toHaveBeenCalledTimes(25) 77 | }) 78 | }) 79 | 80 | it('returns to the next error boundary on a suspense error', () => { 81 | const Inner = jest.fn(() => null) 82 | 83 | const Throw = jest.fn(() => { 84 | throw Promise.reject(new Error('Suspense!')) 85 | }) 86 | 87 | class Outer extends Component { 88 | static getDerivedStateFromProps() { 89 | return { error: false } 90 | } 91 | 92 | static getDerivedStateFromError(error) { 93 | expect(error).not.toBeInstanceOf(Promise) 94 | return { error: true } 95 | } 96 | 97 | render() { 98 | return this.state.error ? : 99 | } 100 | } 101 | 102 | const render$ = renderPrepass() 103 | expect(Throw).toHaveBeenCalledTimes(1) 104 | expect(Inner).not.toHaveBeenCalled() 105 | 106 | return render$.then(() => { 107 | expect(Inner).toHaveBeenCalledTimes(1) 108 | }) 109 | }) 110 | 111 | it('returns to the next error boundary on a nested error', () => { 112 | const Throw = jest.fn(({ depth }) => { 113 | if (depth >= 4) { 114 | throw new Error('' + depth) 115 | } 116 | 117 | return 118 | }) 119 | 120 | class Outer extends Component { 121 | static getDerivedStateFromProps() { 122 | return { error: false } 123 | } 124 | 125 | static getDerivedStateFromError(error) { 126 | expect(error.message).toBe('4') 127 | return { error: true } 128 | } 129 | 130 | render() { 131 | return !this.state.error ? : null 132 | } 133 | } 134 | 135 | renderPrepass().then(() => { 136 | expect(Throw).toHaveBeenCalledTimes(4) 137 | }) 138 | }) 139 | 140 | it('always returns to the correct error boundary', () => { 141 | const values = [] 142 | 143 | const Inner = jest.fn(({ value, depth }) => { 144 | values.push({ value, depth }) 145 | return value 146 | }) 147 | 148 | const Throw = jest.fn(({ value }) => { 149 | throw new Error('' + value) 150 | }) 151 | 152 | class Outer extends Component { 153 | static getDerivedStateFromProps(props) { 154 | return { value: null } 155 | } 156 | 157 | static getDerivedStateFromError(error) { 158 | return { value: error.message } 159 | } 160 | 161 | render() { 162 | return [ 163 | this.state.value ? ( 164 | 165 | ) : ( 166 | 167 | ), 168 | this.props.depth < 4 ? : null 169 | ] 170 | } 171 | } 172 | 173 | return renderPrepass().then(() => { 174 | expect(Throw).toHaveBeenCalledTimes(4) 175 | expect(Inner).toHaveBeenCalledTimes(4) 176 | expect(values).toMatchInlineSnapshot(` 177 | Array [ 178 | Object { 179 | "depth": 1, 180 | "value": "1", 181 | }, 182 | Object { 183 | "depth": 2, 184 | "value": "2", 185 | }, 186 | Object { 187 | "depth": 3, 188 | "value": "3", 189 | }, 190 | Object { 191 | "depth": 4, 192 | "value": "4", 193 | }, 194 | ] 195 | `) 196 | }) 197 | }) 198 | -------------------------------------------------------------------------------- /src/__tests__/suspense.test.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | Fragment, 3 | Component, 4 | createElement, 5 | createContext, 6 | useContext, 7 | useState, 8 | useMemo, 9 | useRef 10 | } from 'react' 11 | 12 | import renderPrepass from '..' 13 | 14 | describe('renderPrepass', () => { 15 | describe('event loop yielding', () => { 16 | it('yields to the event loop when work is taking too long', () => { 17 | const Inner = jest.fn(() => null) 18 | 19 | const Outer = () => { 20 | const start = Date.now() 21 | while (Date.now() - start < 40) {} 22 | return 23 | } 24 | 25 | const render$ = renderPrepass() 26 | 27 | expect(Inner).toHaveBeenCalledTimes(0) 28 | 29 | setImmediate(() => { 30 | setImmediate(() => { 31 | expect(Inner).toHaveBeenCalledTimes(1) 32 | }) 33 | }) 34 | 35 | return render$.then(() => { 36 | expect(Inner).toHaveBeenCalledTimes(1) 37 | }) 38 | }) 39 | 40 | it('rejects promise after getting of exception inside', () => { 41 | const exception = new TypeError('Something went wrong') 42 | 43 | const Inner = jest.fn(() => { 44 | throw exception 45 | }) 46 | 47 | const Outer = () => { 48 | const start = Date.now() 49 | while (Date.now() - start < 40) {} 50 | return 51 | } 52 | 53 | const render$ = renderPrepass() 54 | 55 | return render$.catch((error) => { 56 | expect(Inner).toHaveBeenCalledTimes(1) 57 | expect(error).toBe(exception) 58 | }) 59 | }) 60 | 61 | it('preserves the correct legacy context values across yields', () => { 62 | let called = false 63 | const Inner = (_, context) => { 64 | expect(context.test).toBe(123) 65 | called = true 66 | return null 67 | } 68 | 69 | const Wait = (props) => { 70 | const start = Date.now() 71 | while (Date.now() - start < 21) {} 72 | return props.children 73 | } 74 | 75 | class Outer extends Component { 76 | getChildContext() { 77 | return { test: 123 } 78 | } 79 | 80 | render() { 81 | return ( 82 | 83 | 84 | 85 | 86 | 87 | ) 88 | } 89 | } 90 | 91 | Inner.contextTypes = { test: null } 92 | Outer.childContextTypes = { test: null } 93 | 94 | const render$ = renderPrepass() 95 | expect(called).toBe(false) 96 | return render$.then(() => { 97 | expect(called).toBe(true) 98 | }) 99 | }) 100 | 101 | it('preserves the correct context values across yields', () => { 102 | const Context = createContext(null) 103 | 104 | let called = false 105 | const Inner = () => { 106 | const value = useContext(Context) 107 | expect(value).toBe(123) 108 | called = true 109 | return null 110 | } 111 | 112 | const Wait = () => { 113 | const start = Date.now() 114 | while (Date.now() - start < 21) {} 115 | return 116 | } 117 | 118 | const Outer = () => { 119 | return ( 120 | 121 | 122 | 123 | ) 124 | } 125 | 126 | const render$ = renderPrepass() 127 | expect(called).toBe(false) 128 | return render$.then(() => { 129 | expect(called).toBe(true) 130 | }) 131 | }) 132 | 133 | it('does not yields when work is below the threshold', () => { 134 | const Inner = jest.fn(() => null) 135 | const Outer = () => 136 | const render$ = renderPrepass() 137 | 138 | expect(Inner).toHaveBeenCalledTimes(1) 139 | }) 140 | }) 141 | 142 | describe('function components', () => { 143 | it('supports suspending subtrees', () => { 144 | const value = {} 145 | const getValue = jest 146 | .fn() 147 | .mockImplementationOnce(() => { 148 | throw Promise.resolve() 149 | }) 150 | .mockImplementationOnce(() => value) 151 | 152 | const Inner = jest.fn((props) => { 153 | expect(props.value).toBe(value) 154 | // We expect useState to work across suspense 155 | expect(props.state).toBe('test') 156 | }) 157 | 158 | const Outer = jest.fn(() => { 159 | const [state] = useState('test') 160 | expect(state).toBe('test') 161 | return 162 | }) 163 | 164 | const Wrapper = jest.fn(() => ) 165 | const render$ = renderPrepass() 166 | 167 | // We've synchronously walked the tree and expect a suspense 168 | // queue to have now built up 169 | expect(getValue).toHaveBeenCalledTimes(1) 170 | expect(Inner).not.toHaveBeenCalled() 171 | expect(Outer).toHaveBeenCalledTimes(1) 172 | 173 | return render$.then(() => { 174 | // After suspense we expect a rerender of the suspended subtree to 175 | // have happened 176 | expect(getValue).toHaveBeenCalledTimes(2) 177 | expect(Outer).toHaveBeenCalledTimes(2) 178 | expect(Inner).toHaveBeenCalledTimes(1) 179 | 180 | // Since only the subtree rerenders, we expect the Wrapper to have 181 | // only renderer once 182 | expect(Wrapper).toHaveBeenCalledTimes(1) 183 | }) 184 | }) 185 | 186 | it('preserves state correctly across suspensions', () => { 187 | const getValue = jest 188 | .fn() 189 | .mockImplementationOnce(() => { 190 | throw Promise.resolve() 191 | }) 192 | .mockImplementation(() => 'test') 193 | 194 | const Inner = jest.fn((props) => { 195 | expect(props.value).toBe('test') 196 | expect(props.state).toBe('test') 197 | }) 198 | 199 | const Outer = jest.fn(() => { 200 | const [state, setState] = useState('default') 201 | 202 | const memoedA = useMemo(() => state, [state]) 203 | expect(memoedA).toBe(state) 204 | 205 | // This is to test changing dependency arrays 206 | const memoedB = useMemo( 207 | () => state, 208 | state === 'default' ? null : [state] 209 | ) 210 | expect(memoedB).toBe(state) 211 | 212 | const ref = useRef('initial') 213 | expect(ref.current).toBe('initial') 214 | 215 | const value = getValue() 216 | setState(() => value) 217 | 218 | return 219 | }) 220 | 221 | return renderPrepass().then(() => { 222 | expect(Outer).toHaveBeenCalledTimes(3 * 3 * 3 /* welp */) 223 | expect(Inner).toHaveBeenCalledTimes(2) 224 | }) 225 | }) 226 | 227 | it("handles suspenses thrown in useState's initialState initialiser", () => { 228 | const getValue = jest 229 | .fn() 230 | .mockImplementationOnce(() => { 231 | throw Promise.resolve() 232 | }) 233 | .mockImplementation(() => 'test') 234 | 235 | const Inner = jest.fn((props) => { 236 | expect(props.state).toBe('test') 237 | }) 238 | 239 | const Outer = jest.fn(() => { 240 | const [state] = useState(() => { 241 | // This will trigger suspense: 242 | return getValue() 243 | }) 244 | 245 | return 246 | }) 247 | 248 | return renderPrepass().then(() => { 249 | expect(Outer).toHaveBeenCalled() 250 | expect(Inner).toHaveBeenCalled() 251 | }) 252 | }) 253 | 254 | it('ignores thrown non-promises', () => { 255 | const Outer = () => { 256 | throw new Error('test') 257 | } 258 | const render$ = renderPrepass() 259 | expect(render$).rejects.toThrow('test') 260 | }) 261 | 262 | it('supports promise visitors', () => { 263 | const Inner = jest.fn(() => null) 264 | const Outer = jest.fn(() => ) 265 | 266 | const visitor = jest.fn((element) => { 267 | if (element.type === Inner) return Promise.resolve() 268 | }) 269 | 270 | const render$ = renderPrepass(, visitor) 271 | 272 | // We expect the visitor to have returned a promise 273 | // which is now queued 274 | expect(Inner).not.toHaveBeenCalled() 275 | expect(Outer).toHaveBeenCalledTimes(1) 276 | 277 | return render$.then(() => { 278 | // After suspense we expect Inner to then have renderer 279 | // and the visitor to have been called for both elements 280 | expect(Outer).toHaveBeenCalledTimes(1) 281 | expect(Inner).toHaveBeenCalledTimes(1) 282 | expect(visitor).toHaveBeenCalledTimes(2) 283 | }) 284 | }) 285 | }) 286 | 287 | describe('class components', () => { 288 | it('supports suspending subtrees', () => { 289 | const value = {} 290 | const getValue = jest 291 | .fn() 292 | .mockImplementationOnce(() => { 293 | throw Promise.resolve() 294 | }) 295 | .mockImplementationOnce(() => value) 296 | 297 | const Inner = jest.fn((props) => expect(props.value).toBe(value)) 298 | 299 | class Outer extends Component { 300 | render() { 301 | return 302 | } 303 | } 304 | 305 | const render$ = renderPrepass() 306 | 307 | // We've synchronously walked the tree and expect a suspense 308 | // queue to have now built up 309 | expect(getValue).toHaveBeenCalledTimes(1) 310 | expect(Inner).not.toHaveBeenCalled() 311 | 312 | return render$.then(() => { 313 | // After suspense we expect a rerender of the suspended subtree to 314 | // have happened 315 | expect(getValue).toHaveBeenCalledTimes(2) 316 | expect(Inner).toHaveBeenCalledTimes(1) 317 | }) 318 | }) 319 | 320 | it('ignores thrown non-promises', () => { 321 | class Outer extends Component { 322 | render() { 323 | throw new Error('test') 324 | } 325 | } 326 | 327 | const render$ = renderPrepass() 328 | expect(render$).rejects.toThrow('test') 329 | }) 330 | 331 | it('supports promise visitors', () => { 332 | const Inner = jest.fn(() => null) 333 | 334 | class Outer extends Component { 335 | render() { 336 | return 337 | } 338 | } 339 | 340 | const visitor = jest.fn((element, instance) => { 341 | if (element.type === Outer) { 342 | expect(instance).toEqual(expect.any(Outer)) 343 | return Promise.resolve() 344 | } 345 | }) 346 | 347 | const render$ = renderPrepass(, visitor) 348 | 349 | // We expect the visitor to have returned a promise 350 | // which is now queued 351 | expect(Inner).not.toHaveBeenCalled() 352 | 353 | return render$.then(() => { 354 | // After suspense we expect Inner to then have renderer 355 | // and the visitor to have been called for both elements 356 | expect(Inner).toHaveBeenCalledTimes(1) 357 | expect(visitor).toHaveBeenCalledTimes(2) 358 | }) 359 | }) 360 | 361 | it('supports unconventional updates via the visitor', () => { 362 | const newState = { value: 'test' } 363 | 364 | class Outer extends Component { 365 | constructor() { 366 | super() 367 | this.state = { value: 'initial' } 368 | } 369 | 370 | render() { 371 | expect(this.state).toEqual(newState) 372 | return null 373 | } 374 | } 375 | 376 | const visitor = jest.fn((element, instance) => { 377 | if (element.type === Outer) { 378 | expect(instance.updater.isMounted(instance)).toBe(false) 379 | instance.updater.enqueueForceUpdate(instance) 380 | instance.updater.enqueueReplaceState(instance, newState) 381 | } 382 | }) 383 | 384 | renderPrepass(, visitor) 385 | expect(visitor).toHaveBeenCalledTimes(1) 386 | }) 387 | }) 388 | 389 | describe('lazy components', () => { 390 | it('supports resolving lazy components', () => { 391 | const value = {} 392 | const Inner = jest.fn((props) => expect(props.value).toBe(value)) 393 | const loadInner = jest.fn().mockResolvedValueOnce(Inner) 394 | 395 | const Outer = React.lazy(loadInner) 396 | // Initially React sets the lazy component's status to -1 397 | expect(Outer._payload._status).toBe(-1 /* INITIAL */) 398 | 399 | const render$ = renderPrepass() 400 | 401 | // We've synchronously walked the tree and expect a suspense 402 | // queue to have now built up 403 | expect(Inner).not.toHaveBeenCalled() 404 | expect(loadInner).toHaveBeenCalledTimes(1) 405 | 406 | // The lazy component's state should be updated with some initial 407 | // progress 408 | expect(Outer._payload._status).toBe(0 /* PENDING */) 409 | 410 | return render$.then(() => { 411 | // Afterwards we can expect Inner to have loaded and rendered 412 | expect(Inner).toHaveBeenCalledTimes(1) 413 | 414 | // The lazy component's state should reflect this 415 | expect(Outer._payload._status).toBe(1 /* SUCCESSFUL */) 416 | }) 417 | }) 418 | 419 | it('supports resolving ES exported components', () => { 420 | const Inner = jest.fn(() => null) 421 | const loadInner = jest.fn().mockResolvedValueOnce({ default: Inner }) 422 | const Outer = React.lazy(loadInner) 423 | const render$ = renderPrepass() 424 | 425 | expect(Inner).not.toHaveBeenCalled() 426 | expect(loadInner).toHaveBeenCalledTimes(1) 427 | expect(Outer._payload._status).toBe(0 /* PENDING */) 428 | 429 | return render$.then(() => { 430 | expect(Inner).toHaveBeenCalledTimes(1) 431 | expect(Outer._payload._status).toBe(1 /* SUCCESSFUL */) 432 | }) 433 | }) 434 | 435 | it('supports skipping invalid components', () => { 436 | const loadInner = jest.fn().mockResolvedValueOnce({}) 437 | const Outer = React.lazy(loadInner) 438 | const render$ = renderPrepass() 439 | 440 | expect(loadInner).toHaveBeenCalledTimes(1) 441 | expect(Outer._payload._status).toBe(0 /* PENDING */) 442 | 443 | return render$.then(() => { 444 | expect(Outer._payload._status).toBe(2 /* FAILED */) 445 | }) 446 | }) 447 | 448 | it('supports rendering previously resolved lazy components', () => { 449 | const Inner = jest.fn(() => null) 450 | const loadInner = jest.fn().mockResolvedValueOnce(Inner) 451 | const Outer = React.lazy(loadInner) 452 | 453 | Outer._payload._status = 1 /* SUCCESSFUL */ 454 | Outer._payload._result = Inner /* SUCCESSFUL */ 455 | 456 | renderPrepass() 457 | 458 | expect(loadInner).toHaveBeenCalledTimes(0) 459 | expect(Inner).toHaveBeenCalled() 460 | }) 461 | }) 462 | 463 | it('correctly tracks context values across subtress and suspenses', () => { 464 | const Context = createContext('default') 465 | let hasSuspended = false 466 | 467 | const TestA = jest.fn(() => { 468 | expect(useContext(Context)).toBe('a') 469 | return null 470 | }) 471 | 472 | const TestB = jest.fn(() => { 473 | expect(useContext(Context)).toBe('b') 474 | return null 475 | }) 476 | 477 | const TestC = jest.fn(() => { 478 | expect(useContext(Context)).toBe('c') 479 | if (!hasSuspended) { 480 | throw Promise.resolve().then(() => (hasSuspended = true)) 481 | } 482 | 483 | return null 484 | }) 485 | 486 | const Wrapper = () => { 487 | expect(useContext(Context)).toBe('default') 488 | 489 | return ( 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | ) 502 | } 503 | 504 | const render$ = renderPrepass() 505 | expect(TestC).toHaveBeenCalledTimes(1) 506 | 507 | return render$.then(() => { 508 | expect(TestA).toHaveBeenCalledTimes(1) 509 | expect(TestB).toHaveBeenCalledTimes(1) 510 | expect(TestC).toHaveBeenCalledTimes(2) 511 | }) 512 | }) 513 | 514 | it('correctly tracks legacy context values across subtress and suspenses', () => { 515 | let hasSuspended = false 516 | 517 | class Provider extends Component { 518 | getChildContext() { 519 | return { value: this.props.value } 520 | } 521 | 522 | render() { 523 | return this.props.children 524 | } 525 | } 526 | 527 | Provider.childContextTypes = { value: null } 528 | 529 | class TestA extends Component { 530 | render() { 531 | expect(this.context.value).toBe('a') 532 | return null 533 | } 534 | } 535 | 536 | class TestB extends Component { 537 | render() { 538 | expect(this.context.value).toBe('b') 539 | if (!hasSuspended) { 540 | throw Promise.resolve().then(() => (hasSuspended = true)) 541 | } 542 | 543 | return null 544 | } 545 | } 546 | 547 | TestA.contextTypes = { value: null } 548 | TestB.contextTypes = { value: null } 549 | 550 | const Wrapper = () => ( 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | ) 560 | 561 | return renderPrepass().then(() => { 562 | expect(hasSuspended).toBe(true) 563 | }) 564 | }) 565 | }) 566 | -------------------------------------------------------------------------------- /src/__tests__/visitor.test.js: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom' 2 | 3 | import React, { 4 | Component, 5 | Fragment, 6 | Suspense, 7 | StrictMode, 8 | Profiler, 9 | createContext, 10 | useReducer, 11 | useContext, 12 | useState 13 | } from 'react' 14 | 15 | import { createPortal } from 'react-dom' 16 | 17 | import { 18 | Dispatcher, 19 | setCurrentContextStore, 20 | setCurrentContextMap, 21 | setCurrentErrorFrame, 22 | getCurrentContextMap, 23 | getCurrentContextStore, 24 | getCurrentErrorFrame, 25 | flushPrevContextMap, 26 | flushPrevContextStore, 27 | readContextValue 28 | } from '../internals' 29 | 30 | import { visitElement } from '../visitor' 31 | 32 | const { ReactCurrentDispatcher } = (React: any) 33 | .__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED 34 | 35 | let prevDispatcher = null 36 | 37 | beforeEach(() => { 38 | prevDispatcher = ReactCurrentDispatcher.current 39 | ReactCurrentDispatcher.current = Dispatcher 40 | 41 | setCurrentContextMap({}) 42 | setCurrentContextStore(new Map()) 43 | setCurrentErrorFrame(null) 44 | }) 45 | 46 | afterEach(() => { 47 | ReactCurrentDispatcher.current = prevDispatcher 48 | }) 49 | 50 | const Noop = () => null 51 | 52 | describe('visitElement', () => { 53 | it('walks Fragments', () => { 54 | const element = ( 55 | 56 | 57 | {null} 58 | 59 | 60 | ) 61 | const children = visitElement(element, [], () => {}) 62 | expect(children.length).toBe(2) 63 | expect(children[0].type).toBe(Noop) 64 | expect(children[1].type).toBe(Noop) 65 | }) 66 | 67 | it('walks misc. mode-like components', () => { 68 | const assert = (element) => { 69 | const children = visitElement(element, [], () => {}) 70 | expect(children.length).toBe(1) 71 | expect(children[0].type).toBe(Noop) 72 | } 73 | 74 | assert( 75 | 76 | 77 | 78 | ) 79 | assert( 80 | 81 | 82 | 83 | ) 84 | assert( 85 | 86 | 87 | 88 | ) 89 | }) 90 | 91 | it('walks DOM elements', () => { 92 | const element = ( 93 |
94 | 95 | {null} 96 | 97 |
98 | ) 99 | const children = visitElement(element, [], () => {}) 100 | expect(children.length).toBe(2) 101 | expect(children[0].type).toBe(Noop) 102 | expect(children[1].type).toBe(Noop) 103 | }) 104 | 105 | it('walks Providers and Consumers', () => { 106 | const Context = createContext('default') 107 | const leaf = jest.fn().mockReturnValue(null) 108 | 109 | const makeChild = (value) => ( 110 | 111 | {leaf} 112 | 113 | ) 114 | 115 | for (let i = 0, child = makeChild('testA'); i <= 3 && child; i++) { 116 | child = visitElement(child, [], () => {})[0] 117 | } 118 | 119 | expect(readContextValue(Context)).toBe('testA') 120 | expect(flushPrevContextStore()).toEqual([Context, undefined]) 121 | expect(leaf).toHaveBeenCalledWith('testA') 122 | 123 | for (let i = 0, child = makeChild('testB'); i <= 3 && child; i++) { 124 | child = visitElement(child, [], () => {})[0] 125 | } 126 | 127 | expect(readContextValue(Context)).toBe('testB') 128 | expect(flushPrevContextStore()).toEqual([Context, 'testA']) 129 | expect(leaf).toHaveBeenCalledWith('testB') 130 | }) 131 | 132 | it('skips over invalid Consumer components', () => { 133 | const Context = createContext('default') 134 | const children = visitElement(, [], () => {}) 135 | expect(children.length).toBe(0) 136 | }) 137 | 138 | it('resolves lazy components', () => { 139 | const defer = jest.fn().mockImplementation(() => { 140 | return Promise.resolve().then(() => Noop) 141 | }) 142 | 143 | const Test = React.lazy(defer) 144 | const queue = [] 145 | const children = visitElement(, queue, () => {}) 146 | 147 | expect(children.length).toBe(0) 148 | expect(queue.length).toBe(1) 149 | expect(defer).toHaveBeenCalled() 150 | 151 | expect(queue[0]).toMatchObject({ 152 | contextMap: getCurrentContextMap(), 153 | contextStore: getCurrentContextStore(), 154 | errorFrame: getCurrentErrorFrame(), 155 | thenable: expect.any(Promise), 156 | kind: 'frame.lazy', 157 | type: Test, 158 | props: {} 159 | }) 160 | }) 161 | 162 | it('walks over forwardRef components', () => { 163 | const Test = React.forwardRef(Noop) 164 | const children = visitElement(, [], () => {}) 165 | expect(children.length).toBe(1) 166 | expect(children[0].type).toBe(Noop) 167 | }) 168 | 169 | it('walks over memo components', () => { 170 | const Test = React.memo(Noop) 171 | const children = visitElement(, [], () => {}) 172 | expect(children.length).toBe(1) 173 | expect(children[0].type).toBe(Noop) 174 | }) 175 | 176 | it('returns nothing for portal components', () => { 177 | const document = new JSDOM().window.document 178 | const portal = createPortal(, document.createElement('div')) 179 | const children = visitElement(portal, [], () => {}) 180 | expect(children.length).toBe(0) 181 | }) 182 | 183 | it('renders class components with getDerivedStateFromProps', () => { 184 | const onUnmount = jest.fn() 185 | 186 | class Test extends Component { 187 | static getDerivedStateFromProps() { 188 | return { value: 'b' } 189 | } 190 | 191 | constructor() { 192 | super() 193 | this.state = { value: 'a' } 194 | } 195 | 196 | componentWillUnmount() { 197 | onUnmount() 198 | } 199 | 200 | render() { 201 | return {this.state.value} 202 | } 203 | } 204 | 205 | const visitor = jest.fn() 206 | const children = visitElement(, [], visitor) 207 | 208 | expect(children.length).toBe(1) 209 | expect(children[0].type).toBe(Noop) 210 | expect(children[0].props.children).toBe('b') 211 | expect(onUnmount).not.toHaveBeenCalled() 212 | expect(visitor).toHaveBeenCalledWith(, expect.any(Test)) 213 | }) 214 | 215 | it('renders class components with componentWillMount', () => { 216 | ;['componentWillMount', 'UNSAFE_componentWillMount'].forEach( 217 | (methodName) => { 218 | const onUnmount = jest.fn() 219 | 220 | class Test extends Component { 221 | constructor() { 222 | super() 223 | 224 | this.state = { value: 'a' } 225 | this[methodName] = function () { 226 | this.setState({ value: 'b' }) 227 | } 228 | } 229 | 230 | componentWillUnmount() { 231 | onUnmount() 232 | } 233 | 234 | render() { 235 | return {this.state.value} 236 | } 237 | } 238 | 239 | const children = visitElement(, [], () => {}) 240 | expect(children.length).toBe(1) 241 | expect(children[0].type).toBe(Noop) 242 | expect(children[0].props.children).toBe('b') 243 | expect(onUnmount).toHaveBeenCalled() 244 | } 245 | ) 246 | }) 247 | 248 | it('renders class components with legacy context', () => { 249 | class Inner extends Component { 250 | render() { 251 | return {this.context.value} 252 | } 253 | } 254 | 255 | Inner.contextTypes = { value: Noop } 256 | 257 | class Outer extends Component { 258 | getChildContext() { 259 | return { value: 'test' } 260 | } 261 | 262 | render() { 263 | return 264 | } 265 | } 266 | 267 | Outer.childContextTypes = { value: Noop } 268 | 269 | // We first populate the context 270 | visitElement(, [], () => {}) 271 | 272 | // Then manually mount Inner afterwards 273 | const children = visitElement(, [], () => {}) 274 | 275 | expect(flushPrevContextMap()).toEqual({ value: undefined }) 276 | expect(children.length).toBe(1) 277 | expect(children[0].type).toBe(Noop) 278 | expect(children[0].props.children).toBe('test') 279 | }) 280 | 281 | it('renders function components', () => { 282 | const Test = () => { 283 | const [value, setValue] = useState('a') 284 | if (value === 'a') { 285 | setValue('b') 286 | setValue('c') 287 | setValue('d') 288 | } 289 | 290 | return {value} 291 | } 292 | 293 | const visitor = jest.fn() 294 | const children = visitElement(, [], visitor) 295 | expect(children.length).toBe(1) 296 | expect(children[0].type).toBe(Noop) 297 | expect(children[0].props.children).toBe('d') 298 | expect(visitor).toHaveBeenCalledWith() 299 | }) 300 | 301 | it('renders function components with reducers', () => { 302 | const reducer = (prev, action) => (action === 'increment' ? prev + 1 : prev) 303 | 304 | const Test = () => { 305 | const [value, dispatch] = useReducer(reducer, 0) 306 | if (value === 0) dispatch('increment') 307 | return {value} 308 | } 309 | 310 | const visitor = jest.fn() 311 | const children = visitElement(, [], visitor) 312 | expect(children.length).toBe(1) 313 | expect(children[0].type).toBe(Noop) 314 | expect(children[0].props.children).toBe(1) 315 | expect(visitor).toHaveBeenCalledWith() 316 | }) 317 | 318 | it('renders function components with context', () => { 319 | const Context = createContext('default') 320 | const Test = () => { 321 | const value = useContext(Context) 322 | return {value} 323 | } 324 | 325 | // We first populate the context 326 | visitElement(, [], () => {}) 327 | // Then manually mount Test afterwards 328 | const children = visitElement(, [], () => {}) 329 | expect(children.length).toBe(1) 330 | expect(children[0].type).toBe(Noop) 331 | expect(children[0].props.children).toBe('test') 332 | }) 333 | 334 | it('renders function components with default props', () => { 335 | const Test = (props) => {props.value} 336 | 337 | Test.defaultProps = { value: 'default' } 338 | 339 | const childA = visitElement(, [], () => {})[0] 340 | expect(childA.props.children).toBe('default') 341 | 342 | const childB = visitElement(, [], () => {})[0] 343 | expect(childB.props.children).toBe('test') 344 | }) 345 | }) 346 | -------------------------------------------------------------------------------- /src/element.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Children, type Node, type Element, type ComponentType } from 'react' 4 | import type { AbstractContext, AbstractElement } from './types' 5 | 6 | import { 7 | type ReactSymbol, 8 | REACT_ELEMENT_TYPE, 9 | REACT_PORTAL_TYPE, 10 | REACT_FRAGMENT_TYPE, 11 | REACT_STRICT_MODE_TYPE, 12 | REACT_PROFILER_TYPE, 13 | REACT_PROVIDER_TYPE, 14 | REACT_CONTEXT_TYPE, 15 | REACT_CONCURRENT_MODE_TYPE, 16 | REACT_FORWARD_REF_TYPE, 17 | REACT_SUSPENSE_TYPE, 18 | REACT_MEMO_TYPE, 19 | REACT_LAZY_TYPE 20 | } from './symbols' 21 | 22 | /** Is a given Component a class component */ 23 | export const shouldConstruct = (Comp: ComponentType<*>): boolean %checks => 24 | (Comp: any).prototype && (Comp: any).prototype.isReactComponent 25 | 26 | /** Determine the type of element using react-is with applied fixes */ 27 | export const typeOf = (x: AbstractElement): ReactSymbol | void => { 28 | switch (x.$$typeof) { 29 | case REACT_PORTAL_TYPE: 30 | return REACT_PORTAL_TYPE 31 | case REACT_ELEMENT_TYPE: 32 | switch (x.type) { 33 | case REACT_CONCURRENT_MODE_TYPE: 34 | return REACT_CONCURRENT_MODE_TYPE 35 | case REACT_FRAGMENT_TYPE: 36 | return REACT_FRAGMENT_TYPE 37 | case REACT_PROFILER_TYPE: 38 | return REACT_PROFILER_TYPE 39 | case REACT_STRICT_MODE_TYPE: 40 | return REACT_STRICT_MODE_TYPE 41 | case REACT_SUSPENSE_TYPE: 42 | return REACT_SUSPENSE_TYPE 43 | 44 | default: { 45 | switch (x.type && ((x.type: any).$$typeof: ReactSymbol)) { 46 | case REACT_LAZY_TYPE: 47 | return REACT_LAZY_TYPE 48 | case REACT_MEMO_TYPE: 49 | return REACT_MEMO_TYPE 50 | case REACT_CONTEXT_TYPE: 51 | return REACT_CONTEXT_TYPE 52 | case REACT_PROVIDER_TYPE: 53 | return REACT_PROVIDER_TYPE 54 | case REACT_FORWARD_REF_TYPE: 55 | return REACT_FORWARD_REF_TYPE 56 | default: 57 | return REACT_ELEMENT_TYPE 58 | } 59 | } 60 | } 61 | 62 | default: 63 | return undefined 64 | } 65 | } 66 | 67 | type ScalarNode = null | boolean | string | number 68 | 69 | /** Rebound Children.toArray with modified AbstractElement types */ 70 | const toArray: (node?: Node) => Array = 71 | Children.toArray 72 | 73 | /** Checks whether the `node` is an AbstractElement */ 74 | const isAbstractElement = ( 75 | node: ScalarNode | AbstractElement 76 | ): boolean %checks => node !== null && typeof node === 'object' 77 | 78 | /** Returns a flat AbstractElement array for a given AbstractElement node */ 79 | export const getChildrenArray = (node?: Node): AbstractElement[] => { 80 | // $FlowFixMe 81 | return toArray(node).filter(isAbstractElement) 82 | } 83 | 84 | /** Returns merged props given a props and defaultProps object */ 85 | export const computeProps = (props: Object, defaultProps: void | Object) => { 86 | return typeof defaultProps === 'object' 87 | ? Object.assign({}, defaultProps, props) 88 | : props 89 | } 90 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { type Node, type Element } from 'react' 4 | import type { 5 | Visitor, 6 | YieldFrame, 7 | Frame, 8 | AbstractElement, 9 | RendererState 10 | } from './types' 11 | import { visit, update, SHOULD_YIELD } from './visitor' 12 | import { getChildrenArray } from './element' 13 | 14 | import { 15 | setCurrentContextStore, 16 | setCurrentContextMap, 17 | setCurrentErrorFrame, 18 | getCurrentErrorFrame, 19 | setCurrentRendererState, 20 | initRendererState, 21 | Dispatcher 22 | } from './internals' 23 | 24 | /** visit() walks all elements (depth-first) and while it walks the 25 | element tree some components will suspend and put a `Frame` onto 26 | the queue. Hence we recursively look at suspended components in 27 | this queue, wait for their promises to resolve, and continue 28 | calling visit() on their children. */ 29 | const flushFrames = ( 30 | queue: Frame[], 31 | visitor: Visitor, 32 | state: RendererState 33 | ): Promise => { 34 | const frame = queue.shift() 35 | if (!frame) { 36 | return Promise.resolve() 37 | } 38 | 39 | if (SHOULD_YIELD && frame.kind === 'frame.yield') { 40 | frame.thenable = new Promise((resolve, reject) => { 41 | setImmediate(resolve) 42 | }) 43 | } 44 | 45 | return Promise.resolve(frame.thenable).then( 46 | () => { 47 | setCurrentRendererState(state) 48 | update(frame, queue, visitor) 49 | return flushFrames(queue, visitor, state) 50 | }, 51 | (error: Error) => { 52 | if (!frame.errorFrame) throw error 53 | frame.errorFrame.error = error 54 | update(frame.errorFrame, queue, visitor) 55 | } 56 | ) 57 | } 58 | 59 | const defaultVisitor = () => undefined 60 | 61 | const renderPrepass = (element: Node, visitor?: Visitor): Promise => { 62 | if (!visitor) visitor = defaultVisitor 63 | 64 | const queue: Frame[] = [] 65 | // Renderer state is kept globally but restored and 66 | // passed around manually since it isn't dependent on the 67 | // render tree 68 | const state = initRendererState() 69 | // Context state is kept globally and is modified in-place. 70 | // Before we start walking the element tree we need to reset 71 | // its current state 72 | setCurrentContextMap({}) 73 | setCurrentContextStore(new Map()) 74 | setCurrentErrorFrame(null) 75 | 76 | try { 77 | visit(getChildrenArray(element), queue, visitor) 78 | } catch (error) { 79 | return Promise.reject(error) 80 | } 81 | 82 | return flushFrames(queue, visitor, state) 83 | } 84 | 85 | export default renderPrepass 86 | -------------------------------------------------------------------------------- /src/internals/__tests__/context.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | setCurrentContextStore, 3 | setCurrentContextMap, 4 | readContextValue, 5 | maskContext 6 | } from '../context' 7 | 8 | describe('readContextValue', () => { 9 | it('returns values in a Map by key', () => { 10 | const map = new Map() 11 | const ctx = {} 12 | setCurrentContextStore(map) 13 | map.set(ctx, 'value') 14 | expect(readContextValue(ctx)).toBe('value') 15 | }) 16 | 17 | it('returns default values when keys are unknown', () => { 18 | setCurrentContextMap(new Map()) 19 | const ctx = { _currentValue: 'default' } 20 | expect(readContextValue(ctx)).toBe('default') 21 | }) 22 | }) 23 | 24 | describe('maskContext', () => { 25 | it('supports contextType', () => { 26 | const map = new Map() 27 | const ctx = {} 28 | setCurrentContextStore(map) 29 | map.set(ctx, 'value') 30 | expect(maskContext({ contextType: ctx })).toBe('value') 31 | }) 32 | 33 | it('supports no context', () => { 34 | const map = new Map() 35 | setCurrentContextStore(map) 36 | expect(maskContext({})).toEqual({}) 37 | }) 38 | 39 | it('supports contextTypes', () => { 40 | const map = { a: 'a', b: 'b', c: 'c' } 41 | setCurrentContextMap(map) 42 | expect(maskContext({ contextTypes: { a: null, b: null } })).toEqual({ 43 | a: 'a', 44 | b: 'b' 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /src/internals/__tests__/dispatcher.test.js: -------------------------------------------------------------------------------- 1 | import { setCurrentContextStore } from '../context' 2 | import { getCurrentIdentity, Dispatcher } from '../dispatcher' 3 | 4 | describe('getCurrentIdentity', () => { 5 | it('throws when called outside of function components', () => { 6 | expect(getCurrentIdentity).toThrow() 7 | }) 8 | }) 9 | 10 | describe('readContext', () => { 11 | it('calls readContextValue', () => { 12 | const map = new Map() 13 | const ctx = {} 14 | setCurrentContextStore(map) 15 | map.set(ctx, 'value') 16 | expect(Dispatcher.readContext(ctx)).toBe('value') 17 | }) 18 | }) 19 | 20 | describe('useEffect', () => { 21 | it('is a noop', () => { 22 | expect(Dispatcher.useEffect).not.toThrow() 23 | }) 24 | }) 25 | 26 | describe('useTransition', () => { 27 | it('returns noop and false', () => { 28 | const result = Dispatcher.useTransition() 29 | expect(typeof result[0]).toBe('function') 30 | expect(result[1]).toBe(false) 31 | }) 32 | }) 33 | 34 | describe('useDeferredValue', () => { 35 | it('returns itself', () => { 36 | const value = {} 37 | expect(Dispatcher.useDeferredValue(value)).toBe(value) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/internals/context.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { 4 | AbstractContext, 5 | UserElement, 6 | ContextMap, 7 | ContextStore, 8 | ContextEntry 9 | } from '../types' 10 | 11 | /** The context is kept as a Map from a Context value to the current 12 | value on the React element tree. 13 | The legacy context is kept as a simple object. 14 | When the tree is being walked modifications are made by assigning 15 | new legacy context maps or new context values. 16 | These changes are kept in the `prev` variables and must be flushed 17 | before continuing to walk the tree. 18 | After walking the children they can be restored. 19 | This way the context recursively restores itself on the way up. */ 20 | 21 | let currentContextStore: ContextStore = new Map() 22 | let currentContextMap: ContextMap = {} 23 | 24 | let prevContextMap: void | ContextMap = undefined 25 | let prevContextEntry: void | ContextEntry = undefined 26 | 27 | export const getCurrentContextMap = (): ContextMap => 28 | Object.assign({}, currentContextMap) 29 | export const getCurrentContextStore = (): ContextStore => 30 | new Map(currentContextStore) 31 | 32 | export const flushPrevContextMap = (): void | ContextMap => { 33 | const prev = prevContextMap 34 | prevContextMap = undefined 35 | return prev 36 | } 37 | 38 | export const flushPrevContextStore = (): void | ContextEntry => { 39 | const prev = prevContextEntry 40 | prevContextEntry = undefined 41 | return prev 42 | } 43 | 44 | export const restoreContextMap = (prev: void | ContextMap) => { 45 | if (prev !== undefined) { 46 | Object.assign(currentContextMap, prev) 47 | } 48 | } 49 | 50 | export const restoreContextStore = (prev: void | ContextEntry) => { 51 | if (prev !== undefined) { 52 | currentContextStore.set(prev[0], prev[1]) 53 | } 54 | } 55 | 56 | export const setCurrentContextMap = (map: ContextMap) => { 57 | prevContextMap = undefined 58 | currentContextMap = map 59 | } 60 | 61 | export const setCurrentContextStore = (store: ContextStore) => { 62 | prevContextEntry = undefined 63 | currentContextStore = store 64 | } 65 | 66 | export const assignContextMap = (map: ContextMap) => { 67 | prevContextMap = {} 68 | for (const name in map) { 69 | prevContextMap[name] = currentContextMap[name] 70 | currentContextMap[name] = map[name] 71 | } 72 | } 73 | 74 | export const setContextValue = (context: AbstractContext, value: mixed) => { 75 | prevContextEntry = [context, currentContextStore.get(context)] 76 | currentContextStore.set(context, value) 77 | } 78 | 79 | export const readContextValue = (context: AbstractContext) => { 80 | const value = currentContextStore.get(context) 81 | if (value !== undefined) { 82 | return value 83 | } 84 | 85 | // Return default if context has no value yet 86 | return context._currentValue 87 | } 88 | 89 | const emptyContext = {} 90 | 91 | export const maskContext = (type: $PropertyType) => { 92 | const { contextType, contextTypes } = type 93 | 94 | if (contextType) { 95 | return readContextValue(contextType) 96 | } else if (!contextTypes) { 97 | return emptyContext 98 | } 99 | 100 | const maskedContext = {} 101 | for (const name in contextTypes) { 102 | maskedContext[name] = currentContextMap[name] 103 | } 104 | 105 | return maskedContext 106 | } 107 | -------------------------------------------------------------------------------- /src/internals/dispatcher.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | // Source: https://github.com/facebook/react/blob/c21c41e/packages/react-dom/src/server/ReactPartialRendererHooks.js 3 | 4 | import { readContextValue } from './context' 5 | import { rendererStateRef } from './state' 6 | import is from './objectIs' 7 | 8 | import type { 9 | MutableSource, 10 | MutableSourceGetSnapshotFn, 11 | MutableSourceSubscribeFn, 12 | AbstractContext, 13 | BasicStateAction, 14 | Dispatch, 15 | Update, 16 | UpdateQueue, 17 | Hook 18 | } from '../types' 19 | 20 | export opaque type Identity = {} 21 | export opaque type OpaqueIDType = string 22 | 23 | let currentIdentity: Identity | null = null 24 | 25 | export const makeIdentity = (): Identity => ({}) 26 | 27 | export const setCurrentIdentity = (id: Identity | null) => { 28 | currentIdentity = id 29 | } 30 | 31 | export const getCurrentIdentity = (): Identity => { 32 | if (currentIdentity === null) { 33 | throw new Error( 34 | '[react-ssr-prepass] Hooks can only be called inside the body of a function component. ' + 35 | '(https://fb.me/react-invalid-hook-call)' 36 | ) 37 | } 38 | 39 | // NOTE: The warning that is used in ReactPartialRendererHooks is obsolete 40 | // in a prepass, since it'll be caught by a subsequent renderer anyway 41 | // https://github.com/facebook/react/blob/c21c41e/packages/react-dom/src/server/ReactPartialRendererHooks.js#L63-L71 42 | 43 | return (currentIdentity: Identity) 44 | } 45 | 46 | let firstWorkInProgressHook: Hook | null = null 47 | let workInProgressHook: Hook | null = null 48 | // Whether an update was scheduled during the currently executing render pass. 49 | let didScheduleRenderPhaseUpdate: boolean = false 50 | // Lazily created map of render-phase updates 51 | let renderPhaseUpdates: Map, Update> | null = null 52 | // Counter to prevent infinite loops. 53 | let numberOfReRenders: number = 0 54 | const RE_RENDER_LIMIT = 25 55 | 56 | export const getFirstHook = (): Hook | null => firstWorkInProgressHook 57 | 58 | export const setFirstHook = (hook: Hook | null) => { 59 | firstWorkInProgressHook = hook 60 | } 61 | 62 | function areHookInputsEqual( 63 | nextDeps: Array, 64 | prevDeps: Array | null 65 | ) { 66 | // NOTE: The warnings that are used in ReactPartialRendererHooks are obsolete 67 | // in a prepass, since these issues will be caught by a subsequent renderer anyway 68 | if (prevDeps === null) return false 69 | 70 | for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) { 71 | if (!is(nextDeps[i], prevDeps[i])) return false 72 | } 73 | 74 | return true 75 | } 76 | 77 | function createHook(): Hook { 78 | return { 79 | memoizedState: null, 80 | queue: null, 81 | next: null 82 | } 83 | } 84 | 85 | function createWorkInProgressHook(): Hook { 86 | if (workInProgressHook === null) { 87 | // This is the first hook in the list 88 | if (firstWorkInProgressHook === null) { 89 | return (firstWorkInProgressHook = workInProgressHook = createHook()) 90 | } else { 91 | // There's already a work-in-progress. Reuse it. 92 | return (workInProgressHook = firstWorkInProgressHook) 93 | } 94 | } else { 95 | if (workInProgressHook.next === null) { 96 | // Append to the end of the list 97 | return (workInProgressHook = workInProgressHook.next = createHook()) 98 | } else { 99 | // There's already a work-in-progress. Reuse it. 100 | return (workInProgressHook = workInProgressHook.next) 101 | } 102 | } 103 | } 104 | 105 | export function renderWithHooks( 106 | Component: any, 107 | props: any, 108 | refOrContext: any 109 | ): any { 110 | workInProgressHook = null 111 | let children = Component(props, refOrContext) 112 | 113 | // NOTE: Excessive rerenders won't throw but will instead abort rendering 114 | // since a subsequent renderer can throw when this issue occurs instead 115 | while (numberOfReRenders < RE_RENDER_LIMIT && didScheduleRenderPhaseUpdate) { 116 | // Updates were scheduled during the render phase. They are stored in 117 | // the `renderPhaseUpdates` map. Call the component again, reusing the 118 | // work-in-progress hooks and applying the additional updates on top. Keep 119 | // restarting until no more updates are scheduled. 120 | didScheduleRenderPhaseUpdate = false 121 | numberOfReRenders += 1 122 | // Start over from the beginning of the list 123 | workInProgressHook = null 124 | children = Component(props, refOrContext) 125 | } 126 | 127 | // This will be reset by renderer 128 | // firstWorkInProgressHook = null 129 | 130 | numberOfReRenders = 0 131 | renderPhaseUpdates = null 132 | workInProgressHook = null 133 | 134 | return children 135 | } 136 | 137 | function readContext(context: AbstractContext, _: void | number | boolean) { 138 | // NOTE: The warning that is used in ReactPartialRendererHooks is obsolete 139 | // in a prepass, since it'll be caught by a subsequent renderer anyway 140 | // https://github.com/facebook/react/blob/c21c41e/packages/react-dom/src/server/ReactPartialRendererHooks.js#L215-L223 141 | return readContextValue(context) 142 | } 143 | 144 | function useContext(context: AbstractContext, _: void | number | boolean) { 145 | getCurrentIdentity() 146 | return readContextValue(context) 147 | } 148 | 149 | function basicStateReducer(state: S, action: BasicStateAction): S { 150 | // $FlowFixMe 151 | return typeof action === 'function' ? action(state) : action 152 | } 153 | 154 | function useState( 155 | initialState: (() => S) | S 156 | ): [S, Dispatch>] { 157 | return useReducer( 158 | basicStateReducer, 159 | // useReducer has a special case to support lazy useState initializers 160 | (initialState: any) 161 | ) 162 | } 163 | 164 | function useReducer( 165 | reducer: (S, A) => S, 166 | initialArg: I, 167 | init?: (I) => S 168 | ): [S, Dispatch] { 169 | const id = getCurrentIdentity() 170 | workInProgressHook = createWorkInProgressHook() 171 | 172 | // In the case of a re-render after a suspense, the initial state 173 | // may not be set, so instead of initialising if `!isRerender`, we 174 | // check whether `queue` is set 175 | if (workInProgressHook.queue === null) { 176 | let initialState 177 | if (reducer === basicStateReducer) { 178 | // Special case for `useState`. 179 | initialState = 180 | typeof initialArg === 'function' 181 | ? ((initialArg: any): () => S)() 182 | : ((initialArg: any): S) 183 | } else { 184 | initialState = 185 | init !== undefined ? init(initialArg) : ((initialArg: any): S) 186 | } 187 | 188 | workInProgressHook.memoizedState = initialState 189 | } 190 | 191 | const queue: UpdateQueue = 192 | workInProgressHook.queue || 193 | (workInProgressHook.queue = { last: null, dispatch: null }) 194 | const dispatch: Dispatch = 195 | queue.dispatch || (queue.dispatch = dispatchAction.bind(null, id, queue)) 196 | 197 | if (renderPhaseUpdates !== null) { 198 | // This is a re-render. Apply the new render phase updates to the previous 199 | // current hook. 200 | // Render phase updates are stored in a map of queue -> linked list 201 | const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue) 202 | if (firstRenderPhaseUpdate !== undefined) { 203 | renderPhaseUpdates.delete(queue) 204 | let newState = workInProgressHook.memoizedState 205 | let update = firstRenderPhaseUpdate 206 | do { 207 | // Process this render phase update. We don't have to check the 208 | // priority because it will always be the same as the current 209 | // render's. 210 | const action = update.action 211 | newState = reducer(newState, action) 212 | update = update.next 213 | } while (update !== null) 214 | 215 | workInProgressHook.memoizedState = newState 216 | } 217 | } 218 | 219 | return [workInProgressHook.memoizedState, dispatch] 220 | } 221 | 222 | function useMemo(nextCreate: () => T, deps: Array | void | null): T { 223 | getCurrentIdentity() 224 | workInProgressHook = createWorkInProgressHook() 225 | 226 | const nextDeps = deps === undefined ? null : deps 227 | const prevState = workInProgressHook.memoizedState 228 | if (prevState !== null && nextDeps !== null) { 229 | const prevDeps = prevState[1] 230 | if (areHookInputsEqual(nextDeps, prevDeps)) { 231 | return prevState[0] 232 | } 233 | } 234 | 235 | const nextValue = nextCreate() 236 | workInProgressHook.memoizedState = [nextValue, nextDeps] 237 | return nextValue 238 | } 239 | 240 | function useRef(initialValue: T): { current: T } { 241 | getCurrentIdentity() 242 | workInProgressHook = createWorkInProgressHook() 243 | const previousRef = workInProgressHook.memoizedState 244 | if (previousRef === null) { 245 | const ref = { current: initialValue } 246 | workInProgressHook.memoizedState = ref 247 | return ref 248 | } else { 249 | return previousRef 250 | } 251 | } 252 | 253 | function useOpaqueIdentifier(): OpaqueIDType { 254 | getCurrentIdentity() 255 | workInProgressHook = createWorkInProgressHook() 256 | if (!workInProgressHook.memoizedState) 257 | workInProgressHook.memoizedState = 258 | 'R:' + (rendererStateRef.current.uniqueID++).toString(36) 259 | return workInProgressHook.memoizedState 260 | } 261 | 262 | function dispatchAction( 263 | componentIdentity: Identity, 264 | queue: UpdateQueue, 265 | action: A 266 | ) { 267 | if (componentIdentity === currentIdentity) { 268 | // This is a render phase update. Stash it in a lazily-created map of 269 | // queue -> linked list of updates. After this render pass, we'll restart 270 | // and apply the stashed updates on top of the work-in-progress hook. 271 | didScheduleRenderPhaseUpdate = true 272 | const update: Update = { 273 | action, 274 | next: null 275 | } 276 | if (renderPhaseUpdates === null) { 277 | renderPhaseUpdates = new Map() 278 | } 279 | const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue) 280 | if (firstRenderPhaseUpdate === undefined) { 281 | renderPhaseUpdates.set(queue, update) 282 | } else { 283 | // Append the update to the end of the list. 284 | let lastRenderPhaseUpdate = firstRenderPhaseUpdate 285 | while (lastRenderPhaseUpdate.next !== null) { 286 | lastRenderPhaseUpdate = lastRenderPhaseUpdate.next 287 | } 288 | lastRenderPhaseUpdate.next = update 289 | } 290 | } else { 291 | // This means an update has happened after the function component has 292 | // returned. On the server this is a no-op. In React Fiber, the update 293 | // would be scheduled for a future render. 294 | } 295 | } 296 | 297 | function useCallback(callback: T, deps: Array | void | null): T { 298 | return useMemo(() => callback, deps) 299 | } 300 | 301 | function useMutableSource( 302 | source: MutableSource, 303 | getSnapshot: MutableSourceGetSnapshotFn, 304 | _subscribe: MutableSourceSubscribeFn 305 | ): Snapshot { 306 | getCurrentIdentity() 307 | return getSnapshot(source._source) 308 | } 309 | 310 | function noop(): void {} 311 | 312 | function useTransition(): [(callback: () => void) => void, boolean] { 313 | const startTransition = (callback) => { 314 | callback() 315 | } 316 | return [startTransition, false] 317 | } 318 | 319 | function useDeferredValue(input: T): T { 320 | return input 321 | } 322 | 323 | // See: https://github.com/facebook/react/blob/fe41934/packages/use-sync-external-store/src/useSyncExternalStoreShimServer.js#L10-L20 324 | function useSyncExternalStore( 325 | subscribe: (() => void) => () => void, 326 | getSnapshot: () => T, 327 | getServerSnapshot?: () => T 328 | ): T { 329 | // Note: The shim does not use getServerSnapshot, because pre-18 versions of 330 | // React do not expose a way to check if we're hydrating. So users of the shim 331 | // will need to track that themselves and return the correct value 332 | // from `getSnapshot`. 333 | return getSnapshot() 334 | } 335 | 336 | export const Dispatcher = { 337 | readContext, 338 | useSyncExternalStore, 339 | useContext, 340 | useMemo, 341 | useReducer, 342 | useRef, 343 | useState, 344 | useCallback, 345 | useMutableSource, 346 | useTransition, 347 | useDeferredValue, 348 | useOpaqueIdentifier, 349 | // aliased for now 350 | // see: https://github.com/FormidableLabs/react-ssr-prepass/pull/75 351 | useId: useOpaqueIdentifier, 352 | unstable_useId: useOpaqueIdentifier, 353 | unstable_useOpaqueIdentifier: useOpaqueIdentifier, 354 | // ignore useLayout effect completely as usage of it will be caught 355 | // in a subsequent render pass 356 | useLayoutEffect: noop, 357 | // useImperativeHandle is not run in the server environment 358 | useImperativeHandle: noop, 359 | // Effects are not run in the server environment. 360 | useEffect: noop, 361 | // Debugging effect 362 | useDebugValue: noop 363 | } 364 | -------------------------------------------------------------------------------- /src/internals/error.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { ClassFrame } from '../types' 4 | 5 | /** The current error boundary frame determines where to continue rendering when an error is raised */ 6 | let currentErrorFrame: null | ClassFrame = null 7 | 8 | export const getCurrentErrorFrame = (): ClassFrame | null => currentErrorFrame 9 | 10 | export const setCurrentErrorFrame = (frame?: ClassFrame | null) => { 11 | currentErrorFrame = frame || null 12 | } 13 | -------------------------------------------------------------------------------- /src/internals/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export * from './context' 4 | export * from './error' 5 | export * from './state' 6 | export * from './dispatcher' 7 | -------------------------------------------------------------------------------- /src/internals/objectIs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | /** 11 | * inlined Object.is polyfill to avoid requiring consumers ship their own 12 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is 13 | */ 14 | function is(x: any, y: any) { 15 | return ( 16 | (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare 17 | ) 18 | } 19 | 20 | const objectIs: (x: any, y: any) => boolean = 21 | typeof Object.is === 'function' ? Object.is : is 22 | 23 | export default objectIs 24 | -------------------------------------------------------------------------------- /src/internals/state.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { RendererState } from '../types' 4 | 5 | /** The current global renderer state per render cycle */ 6 | export const rendererStateRef: {| current: RendererState |} = { 7 | current: { uniqueID: 0 } 8 | } 9 | export const initRendererState = (): RendererState => 10 | (rendererStateRef.current = { uniqueID: 0 }) 11 | export const setCurrentRendererState = (state: RendererState) => 12 | (rendererStateRef.current = state) 13 | -------------------------------------------------------------------------------- /src/render/classComponent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Node, ComponentType } from 'react' 4 | import { computeProps } from '../element' 5 | 6 | import type { 7 | Visitor, 8 | Frame, 9 | ClassFrame, 10 | DefaultProps, 11 | ComponentStatics, 12 | UserElement 13 | } from '../types' 14 | 15 | import { 16 | maskContext, 17 | assignContextMap, 18 | setCurrentIdentity, 19 | setCurrentContextMap, 20 | getCurrentContextMap, 21 | setCurrentContextStore, 22 | getCurrentContextStore, 23 | setCurrentErrorFrame, 24 | getCurrentErrorFrame 25 | } from '../internals' 26 | 27 | const RE_RENDER_LIMIT = 25 28 | 29 | const createUpdater = () => { 30 | const queue = [] 31 | 32 | return { 33 | _thrown: 0, 34 | queue, 35 | isMounted: () => false, 36 | enqueueForceUpdate: () => null, 37 | enqueueReplaceState: (instance, completeState) => { 38 | if (instance._isMounted) { 39 | queue.length = 0 40 | queue.push(completeState) 41 | } 42 | }, 43 | enqueueSetState: (instance, currentPartialState) => { 44 | if (instance._isMounted) { 45 | queue.push(currentPartialState) 46 | } 47 | } 48 | } 49 | } 50 | 51 | const flushEnqueuedState = (instance: any) => { 52 | const queue = (instance.updater.queue: any[]) 53 | 54 | if (queue.length > 0) { 55 | let nextState = Object.assign({}, instance.state) 56 | 57 | for (let i = 0, l = queue.length; i < l; i++) { 58 | const partial = queue[i] 59 | const partialState = 60 | typeof partial === 'function' 61 | ? partial.call(instance, nextState, instance.props, instance.context) 62 | : partial 63 | if (partialState !== null) { 64 | Object.assign(nextState, partialState) 65 | } 66 | } 67 | 68 | instance.state = nextState 69 | queue.length = 0 70 | } 71 | } 72 | 73 | const createInstance = (type: any, props: DefaultProps) => { 74 | const updater = createUpdater() 75 | const computedProps = computeProps(props, type.defaultProps) 76 | const context = maskContext(type) 77 | const instance = new type(computedProps, context, updater) 78 | 79 | instance.props = computedProps 80 | instance.context = context 81 | instance.updater = updater 82 | instance._isMounted = true 83 | 84 | if (instance.state === undefined) { 85 | instance.state = null 86 | } 87 | 88 | if ( 89 | typeof instance.componentDidCatch === 'function' || 90 | typeof type.getDerivedStateFromError === 'function' 91 | ) { 92 | const frame = makeFrame(type, instance, null) 93 | frame.errorFrame = frame 94 | setCurrentErrorFrame(frame) 95 | } 96 | 97 | if (typeof type.getDerivedStateFromProps === 'function') { 98 | const { getDerivedStateFromProps } = type 99 | const state = getDerivedStateFromProps(instance.props, instance.state) 100 | if (state !== null && state !== undefined) { 101 | instance.state = Object.assign({}, instance.state, state) 102 | } 103 | } else if (typeof instance.componentWillMount === 'function') { 104 | instance.componentWillMount() 105 | } else if (typeof instance.UNSAFE_componentWillMount === 'function') { 106 | instance.UNSAFE_componentWillMount() 107 | } 108 | 109 | return instance 110 | } 111 | 112 | const makeFrame = ( 113 | type: any, 114 | instance: any, 115 | thenable: Promise | null 116 | ) => ({ 117 | contextMap: getCurrentContextMap(), 118 | contextStore: getCurrentContextStore(), 119 | errorFrame: getCurrentErrorFrame(), 120 | thenable, 121 | kind: 'frame.class', 122 | error: null, 123 | instance, 124 | type 125 | }) 126 | 127 | const render = (type: any, instance: any, queue: Frame[]) => { 128 | // Flush all queued up state changes 129 | flushEnqueuedState(instance) 130 | let child: Node = null 131 | 132 | try { 133 | child = instance.render() 134 | } catch (error) { 135 | if (typeof error.then !== 'function') { 136 | throw error 137 | } 138 | 139 | queue.push(makeFrame(type, instance, error)) 140 | return null 141 | } 142 | 143 | if ( 144 | type.childContextTypes !== undefined && 145 | typeof instance.getChildContext === 'function' 146 | ) { 147 | const childContext = instance.getChildContext() 148 | if (childContext !== null && typeof childContext === 'object') { 149 | assignContextMap(childContext) 150 | } 151 | } 152 | 153 | if ( 154 | typeof instance.getDerivedStateFromProps !== 'function' && 155 | (typeof instance.componentWillMount === 'function' || 156 | typeof instance.UNSAFE_componentWillMount === 'function') && 157 | typeof instance.componentWillUnmount === 'function' 158 | ) { 159 | try { 160 | instance.componentWillUnmount() 161 | } catch (_err) {} 162 | } 163 | 164 | instance._isMounted = false 165 | return child 166 | } 167 | 168 | /** Mount a class component */ 169 | export const mount = ( 170 | type: ComponentType & ComponentStatics, 171 | props: DefaultProps, 172 | queue: Frame[], 173 | visitor: Visitor, 174 | element: UserElement 175 | ) => { 176 | setCurrentIdentity(null) 177 | 178 | const instance = createInstance(type, props) 179 | const promise = visitor(element, instance) 180 | if (promise) { 181 | queue.push(makeFrame(type, instance, promise)) 182 | return null 183 | } 184 | 185 | return render(type, instance, queue) 186 | } 187 | 188 | /** Update a previously suspended class component */ 189 | export const update = (queue: Frame[], frame: ClassFrame) => { 190 | setCurrentIdentity(null) 191 | setCurrentContextMap(frame.contextMap) 192 | setCurrentContextStore(frame.contextStore) 193 | setCurrentErrorFrame(frame.errorFrame) 194 | 195 | if (frame.error) { 196 | // We simply have to bail when a loop occurs 197 | if (++frame.instance.updater._thrown >= RE_RENDER_LIMIT) return null 198 | 199 | frame.instance._isMounted = true 200 | 201 | if (typeof frame.instance.componentDidCatch === 'function') { 202 | frame.instance.componentDidCatch(frame.error) 203 | } 204 | 205 | if (typeof frame.type.getDerivedStateFromError === 'function') { 206 | frame.instance.updater.enqueueSetState( 207 | frame.instance, 208 | frame.type.getDerivedStateFromError(frame.error) 209 | ) 210 | } 211 | } 212 | 213 | return render(frame.type, frame.instance, queue) 214 | } 215 | -------------------------------------------------------------------------------- /src/render/functionComponent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Node, ComponentType } from 'react' 4 | import { computeProps } from '../element' 5 | 6 | import type { 7 | Visitor, 8 | Hook, 9 | Frame, 10 | HooksFrame, 11 | DefaultProps, 12 | ComponentStatics, 13 | UserElement 14 | } from '../types' 15 | 16 | import { 17 | type Identity, 18 | maskContext, 19 | makeIdentity, 20 | setCurrentIdentity, 21 | getCurrentIdentity, 22 | setCurrentContextStore, 23 | getCurrentContextStore, 24 | setCurrentContextMap, 25 | getCurrentContextMap, 26 | setCurrentErrorFrame, 27 | getCurrentErrorFrame, 28 | renderWithHooks, 29 | setFirstHook, 30 | getFirstHook 31 | } from '../internals' 32 | 33 | const makeFrame = ( 34 | type: ComponentType & ComponentStatics, 35 | props: DefaultProps, 36 | thenable: Promise 37 | ) => ({ 38 | contextMap: getCurrentContextMap(), 39 | contextStore: getCurrentContextStore(), 40 | id: getCurrentIdentity(), 41 | hook: getFirstHook(), 42 | kind: 'frame.hooks', 43 | errorFrame: getCurrentErrorFrame(), 44 | thenable, 45 | props, 46 | type 47 | }) 48 | 49 | const render = ( 50 | type: ComponentType & ComponentStatics, 51 | props: DefaultProps, 52 | queue: Frame[] 53 | ): Node => { 54 | try { 55 | return renderWithHooks( 56 | type, 57 | computeProps(props, type.defaultProps), 58 | maskContext(type) 59 | ) 60 | } catch (error) { 61 | if (typeof error.then !== 'function') { 62 | throw error 63 | } 64 | 65 | queue.push(makeFrame(type, props, error)) 66 | return null 67 | } 68 | } 69 | 70 | /** Mount a function component */ 71 | export const mount = ( 72 | type: ComponentType & ComponentStatics, 73 | props: DefaultProps, 74 | queue: Frame[], 75 | visitor: Visitor, 76 | element: UserElement 77 | ): Node => { 78 | setFirstHook(null) 79 | setCurrentIdentity(makeIdentity()) 80 | 81 | const promise = visitor(element) 82 | if (promise) { 83 | queue.push(makeFrame(type, props, promise)) 84 | return null 85 | } 86 | 87 | return render(type, props, queue) 88 | } 89 | 90 | /** Update a previously suspended function component */ 91 | export const update = (queue: Frame[], frame: HooksFrame) => { 92 | setFirstHook(frame.hook) 93 | setCurrentIdentity(frame.id) 94 | setCurrentContextMap(frame.contextMap) 95 | setCurrentContextStore(frame.contextStore) 96 | setCurrentErrorFrame(frame.errorFrame) 97 | return render(frame.type, frame.props, queue) 98 | } 99 | -------------------------------------------------------------------------------- /src/render/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /** Every type of component here can suspend itself. 4 | This means it pushes a `Frame` to the queue. 5 | Components are first mounted in visitor.js, 6 | and if they have suspended after their promise 7 | resolves `update` is called instead for them, 8 | which preserves their previous mounted state 9 | and rerenders the component. */ 10 | 11 | export { 12 | mount as mountLazyComponent, 13 | update as updateLazyComponent 14 | } from './lazyComponent' 15 | 16 | export { 17 | mount as mountFunctionComponent, 18 | update as updateFunctionComponent 19 | } from './functionComponent' 20 | 21 | export { 22 | mount as mountClassComponent, 23 | update as updateClassComponent 24 | } from './classComponent' 25 | -------------------------------------------------------------------------------- /src/render/lazyComponent.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { createElement, type Node } from 'react' 4 | import type { 5 | LazyComponent, 6 | LazyComponentPayload, 7 | DefaultProps, 8 | LazyFrame, 9 | Frame 10 | } from '../types' 11 | import { getChildrenArray } from '../element' 12 | 13 | import { 14 | setCurrentIdentity, 15 | setCurrentContextStore, 16 | getCurrentContextStore, 17 | setCurrentContextMap, 18 | getCurrentContextMap, 19 | setCurrentErrorFrame, 20 | getCurrentErrorFrame 21 | } from '../internals' 22 | 23 | const resolve = (type: LazyComponent): Promise => { 24 | const payload = (type._payload || type: any) 25 | if (payload._status === 0) { 26 | return payload._result 27 | } else if (payload._status === 1) { 28 | return Promise.resolve(payload._result) 29 | } else if (payload._status === 2) { 30 | return Promise.reject(payload._result) 31 | } 32 | 33 | payload._status = 0 /* PENDING */ 34 | 35 | return (payload._result = (payload._ctor || payload._result)() 36 | .then((Component) => { 37 | payload._result = Component 38 | if (typeof Component === 'function') { 39 | payload._status = 1 /* SUCCESSFUL */ 40 | } else if ( 41 | Component !== null && 42 | typeof Component === 'object' && 43 | typeof Component.default === 'function' 44 | ) { 45 | payload._result = Component.default 46 | payload._status = 1 /* SUCCESSFUL */ 47 | } else { 48 | payload._status = 2 /* FAILED */ 49 | } 50 | }) 51 | .catch((error) => { 52 | payload._status = 2 /* FAILED */ 53 | payload._result = error 54 | return Promise.reject(error) 55 | })) 56 | } 57 | 58 | const render = ( 59 | type: LazyComponent, 60 | props: DefaultProps, 61 | queue: Frame[] 62 | ): Node => { 63 | // Component has previously been fetched successfully, 64 | // so create the element with passed props and return it 65 | const payload = ((type._payload || type: any): LazyComponentPayload) 66 | if (payload._status === 1) { 67 | return createElement(payload._result, props) 68 | } 69 | 70 | return null 71 | } 72 | 73 | export const mount = ( 74 | type: LazyComponent, 75 | props: DefaultProps, 76 | queue: Frame[] 77 | ): Node => { 78 | // If the component has not been fetched yet, suspend this component 79 | const payload = ((type._payload || type: any): LazyComponentPayload) 80 | if (payload._status <= 0) { 81 | queue.push({ 82 | kind: 'frame.lazy', 83 | contextMap: getCurrentContextMap(), 84 | contextStore: getCurrentContextStore(), 85 | errorFrame: getCurrentErrorFrame(), 86 | thenable: resolve(type), 87 | props, 88 | type 89 | }) 90 | 91 | return null 92 | } 93 | 94 | return render(type, props, queue) 95 | } 96 | 97 | export const update = (queue: Frame[], frame: LazyFrame): Node => { 98 | setCurrentIdentity(null) 99 | setCurrentContextMap(frame.contextMap) 100 | setCurrentContextStore(frame.contextStore) 101 | setCurrentErrorFrame(frame.errorFrame) 102 | return render(frame.type, frame.props, queue) 103 | } 104 | -------------------------------------------------------------------------------- /src/symbols.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Node } from 'react' 4 | 5 | let Element = 0xeac7 6 | let Portal = 0xeaca 7 | let Fragment = 0xeacb 8 | let StrictMode = 0xeacc 9 | let Profiler = 0xead2 10 | let ContextProvider = 0xeacd 11 | let ContextConsumer = 0xeace 12 | let ConcurrentMode = 0xeacf 13 | let ForwardRef = 0xead0 14 | let Suspense = 0xead1 15 | let Memo = 0xead3 16 | let Lazy = 0xead4 17 | 18 | if (typeof Symbol === 'function' && Symbol.for) { 19 | const symbolFor = Symbol.for 20 | Element = symbolFor('react.element') 21 | Portal = symbolFor('react.portal') 22 | Fragment = symbolFor('react.fragment') 23 | StrictMode = symbolFor('react.strict_mode') 24 | Profiler = symbolFor('react.profiler') 25 | ContextProvider = symbolFor('react.provider') 26 | ContextConsumer = symbolFor('react.context') 27 | ConcurrentMode = Symbol.for('react.concurrent_mode') 28 | ForwardRef = symbolFor('react.forward_ref') 29 | Suspense = symbolFor('react.suspense') 30 | Memo = symbolFor('react.memo') 31 | Lazy = symbolFor('react.lazy') 32 | } 33 | 34 | /** Literal types representing the ReactSymbol values. These values do not actually match the values from react-is! */ 35 | export type ReactSymbol = 36 | | 'react.element' /* 0xeac7 | Symbol(react.element) */ 37 | | 'react.portal' /* 0xeaca | Symbol(react.portal) */ 38 | | 'react.fragment' /* 0xeacb | Symbol(react.fragment) */ 39 | | 'react.strict_mode' /* 0xeacc | Symbol(react.strict_mode) */ 40 | | 'react.profiler' /* 0xead2 | Symbol(react.profiler) */ 41 | | 'react.provider' /* 0xeacd | Symbol(react.provider) */ 42 | | 'react.context' /* 0xeace | Symbol(react.context) */ 43 | | 'react.concurrent_mode' /* 0xeacf | Symbol(react.concurrent_mode) */ 44 | | 'react.forward_ref' /* 0xead0 | Symbol(react.forward_ref) */ 45 | | 'react.suspense' /* 0xead1 | Symbol(react.suspense) */ 46 | | 'react.memo' /* 0xead3 | Symbol(react.memo) */ 47 | | 'react.lazy' /* 0xead4 | Symbol(react.lazy) */ 48 | 49 | export const REACT_ELEMENT_TYPE: 'react.element' = (Element: any) 50 | export const REACT_PORTAL_TYPE: 'react.portal' = (Portal: any) 51 | export const REACT_FRAGMENT_TYPE: 'react.fragment' = (Fragment: any) 52 | export const REACT_STRICT_MODE_TYPE: 'react.strict_mode' = (StrictMode: any) 53 | export const REACT_PROFILER_TYPE: 'react.profiler' = (Profiler: any) 54 | export const REACT_PROVIDER_TYPE: 'react.provider' = (ContextProvider: any) 55 | export const REACT_CONTEXT_TYPE: 'react.context' = (ContextConsumer: any) 56 | export const REACT_CONCURRENT_MODE_TYPE: 'react.concurrent_mode' = (ConcurrentMode: any) 57 | export const REACT_FORWARD_REF_TYPE: 'react.forward_ref' = (ForwardRef: any) 58 | export const REACT_SUSPENSE_TYPE: 'react.suspense' = (Suspense: any) 59 | export const REACT_MEMO_TYPE: 'react.memo' = (Memo: any) 60 | export const REACT_LAZY_TYPE: 'react.lazy' = (Lazy: any) 61 | -------------------------------------------------------------------------------- /src/types/element.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Node, Context, ComponentType } from 'react' 4 | 5 | import { 6 | REACT_ELEMENT_TYPE, 7 | REACT_PORTAL_TYPE, 8 | REACT_FRAGMENT_TYPE, 9 | REACT_STRICT_MODE_TYPE, 10 | REACT_PROFILER_TYPE, 11 | REACT_PROVIDER_TYPE, 12 | REACT_CONTEXT_TYPE, 13 | REACT_CONCURRENT_MODE_TYPE, 14 | REACT_FORWARD_REF_TYPE, 15 | REACT_SUSPENSE_TYPE, 16 | REACT_MEMO_TYPE, 17 | REACT_LAZY_TYPE 18 | } from '../symbols' 19 | 20 | export type AbstractContext = Context & { 21 | $$typeof: typeof REACT_CONTEXT_TYPE, 22 | _currentValue: mixed, 23 | _threadCount: number 24 | } 25 | 26 | export type DefaultProps = { 27 | children?: Node 28 | } 29 | 30 | export type ComponentStatics = { 31 | getDerivedStateFromProps?: (props: Object, state: mixed) => mixed, 32 | getDerivedStateFromError?: (error: Error) => mixed, 33 | contextType?: AbstractContext, 34 | contextTypes?: Object, 35 | childContextTypes?: Object, 36 | defaultProps?: Object 37 | } 38 | 39 | /** */ 40 | export type ConsumerElement = { 41 | type: 42 | | AbstractContext 43 | | { 44 | $$typeof: typeof REACT_CONTEXT_TYPE, 45 | _context: AbstractContext 46 | }, 47 | props: { children?: (value: mixed) => Node }, 48 | $$typeof: typeof REACT_ELEMENT_TYPE 49 | } 50 | 51 | /** */ 52 | export type ProviderElement = { 53 | type: { 54 | $$typeof: typeof REACT_PROVIDER_TYPE, 55 | _context: AbstractContext 56 | }, 57 | props: DefaultProps & { value: mixed }, 58 | $$typeof: typeof REACT_ELEMENT_TYPE 59 | } 60 | 61 | /** */ 62 | export type SuspenseElement = { 63 | type: typeof REACT_SUSPENSE_TYPE, 64 | props: DefaultProps & { fallback?: Node }, 65 | $$typeof: typeof REACT_ELEMENT_TYPE 66 | } 67 | 68 | /** , , , */ 69 | export type FragmentElement = { 70 | type: 71 | | typeof REACT_CONCURRENT_MODE_TYPE 72 | | typeof REACT_FRAGMENT_TYPE 73 | | typeof REACT_PROFILER_TYPE 74 | | typeof REACT_STRICT_MODE_TYPE, 75 | props: DefaultProps, 76 | $$typeof: typeof REACT_ELEMENT_TYPE 77 | } 78 | 79 | type LazyComponentUninitialized = { 80 | _status: -1, 81 | _result: () => Promise 82 | } 83 | 84 | type LazyComponentPending = { 85 | _status: 0, 86 | _result: Promise 87 | } 88 | 89 | type LazyComponentResolved = { 90 | _status: 1, 91 | _result: ComponentType & ComponentStatics 92 | } 93 | 94 | type LazyComponentRejected = { 95 | _status: 2, 96 | _result: mixed 97 | } 98 | 99 | export type LazyComponentPayload = 100 | | LazyComponentUninitialized 101 | | LazyComponentPending 102 | | LazyComponentResolved 103 | | LazyComponentRejected 104 | 105 | type LazyComponentLegacy = { 106 | $$typeof: typeof REACT_LAZY_TYPE, 107 | _ctor: () => Promise, 108 | _status: -1 | 0 | 1 | 2, 109 | _result: mixed 110 | } 111 | 112 | type LazyComponentModern = { 113 | $$typeof: typeof REACT_LAZY_TYPE, 114 | _payload: LazyComponentPayload 115 | } 116 | 117 | export type LazyComponent = LazyComponentLegacy | LazyComponentModern 118 | 119 | /** */ 120 | export type LazyElement = { 121 | $$typeof: typeof REACT_LAZY_TYPE, 122 | props: DefaultProps, 123 | type: LazyComponent 124 | } 125 | 126 | /** , */ 127 | export type MemoElement = { 128 | type: { 129 | type: ComponentType & ComponentStatics, 130 | $$typeof: typeof REACT_MEMO_TYPE 131 | }, 132 | props: DefaultProps, 133 | $$typeof: typeof REACT_ELEMENT_TYPE 134 | } 135 | 136 | /** */ 137 | export type ForwardRefElement = { 138 | type: { 139 | render: ComponentType & ComponentStatics, 140 | $$typeof: typeof REACT_FORWARD_REF_TYPE, 141 | defaultProps?: Object, 142 | // styled-components specific properties 143 | styledComponentId?: string, 144 | target?: ComponentType | string 145 | }, 146 | props: DefaultProps, 147 | $$typeof: typeof REACT_ELEMENT_TYPE 148 | } 149 | 150 | /** Portal */ 151 | export type PortalElement = { 152 | $$typeof: typeof REACT_PORTAL_TYPE, 153 | containerInfo: any, 154 | children: Node 155 | } 156 | 157 | /** */ 158 | export type UserElement = { 159 | type: ComponentType & ComponentStatics, 160 | props: DefaultProps, 161 | $$typeof: typeof REACT_ELEMENT_TYPE 162 | } 163 | 164 | /**
*/ 165 | export type DOMElement = { 166 | type: string, 167 | props: DefaultProps, 168 | $$typeof: typeof REACT_ELEMENT_TYPE 169 | } 170 | 171 | /** This is like React.Element but with specific symbol fields */ 172 | export type AbstractElement = 173 | | ConsumerElement 174 | | ProviderElement 175 | | FragmentElement 176 | | LazyElement 177 | | ForwardRefElement 178 | | MemoElement 179 | | UserElement 180 | | DOMElement 181 | | PortalElement 182 | | SuspenseElement 183 | 184 | export type MutableSourceGetSnapshotFn< 185 | Source: $NonMaybeType, 186 | Snapshot 187 | > = (source: Source) => Snapshot 188 | 189 | export type MutableSourceSubscribeFn, Snapshot> = ( 190 | source: Source, 191 | callback: (snapshot: Snapshot) => void 192 | ) => () => void 193 | 194 | export type MutableSource> = { 195 | _source: Source 196 | } 197 | -------------------------------------------------------------------------------- /src/types/frames.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { ComponentType } from 'react' 4 | import type { Identity } from '../internals' 5 | import type { LazyComponent } from '../types' 6 | import type { ContextMap, ContextStore, ContextEntry, Hook } from './state' 7 | import type { AbstractElement, DefaultProps, ComponentStatics } from './element' 8 | 9 | export type BaseFrame = { 10 | contextMap: ContextMap, 11 | contextStore: ContextStore, 12 | errorFrame: ClassFrame | null, 13 | thenable: Promise | null 14 | } 15 | 16 | /** Description of suspended React.lazy components */ 17 | export type LazyFrame = BaseFrame & { 18 | kind: 'frame.lazy', 19 | type: LazyComponent, 20 | props: Object 21 | } 22 | 23 | /** Description of suspended React.Components */ 24 | export type ClassFrame = BaseFrame & { 25 | kind: 'frame.class', 26 | type: ComponentType & ComponentStatics, 27 | error: Error | null, 28 | instance: any 29 | } 30 | 31 | /** Description of suspended function components with hooks state */ 32 | export type HooksFrame = BaseFrame & { 33 | kind: 'frame.hooks', 34 | type: ComponentType & ComponentStatics, 35 | props: Object, 36 | id: Identity, 37 | hook: Hook | null 38 | } 39 | 40 | /** Description of a pause to yield to the event loop */ 41 | export type YieldFrame = BaseFrame & { 42 | kind: 'frame.yield', 43 | traversalChildren: AbstractElement[][], 44 | traversalMap: Array, 45 | traversalStore: Array, 46 | traversalErrorFrame: Array 47 | } 48 | 49 | export type Frame = ClassFrame | HooksFrame | LazyFrame | YieldFrame 50 | 51 | export type RendererState = {| 52 | uniqueID: number 53 | |} 54 | -------------------------------------------------------------------------------- /src/types/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export * from './element' 4 | export * from './state' 5 | export * from './frames' 6 | export * from './input' 7 | -------------------------------------------------------------------------------- /src/types/input.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { UserElement } from './element' 4 | 5 | /** When encountering a class component this function can trigger an suspense */ 6 | export type Visitor = ( 7 | element: UserElement, 8 | instance?: any 9 | ) => void | Promise 10 | -------------------------------------------------------------------------------- /src/types/state.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { AbstractContext } from './element' 4 | 5 | export type ContextStore = Map 6 | export type ContextMap = { [name: string]: mixed } 7 | export type ContextEntry = [AbstractContext, mixed] 8 | 9 | export type Dispatch = A => void 10 | 11 | export type BasicStateAction = (S => S) | S 12 | 13 | export type Update = { 14 | action: A, 15 | next: Update | null 16 | } 17 | 18 | export type UpdateQueue = { 19 | last: Update | null, 20 | dispatch: any 21 | } 22 | 23 | export type Hook = { 24 | memoizedState: any, 25 | queue: UpdateQueue | null, 26 | next: Hook | null 27 | } 28 | -------------------------------------------------------------------------------- /src/visitor.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { type Node, type ComponentType, createElement } from 'react' 4 | 5 | import { 6 | typeOf, 7 | shouldConstruct, 8 | getChildrenArray, 9 | computeProps 10 | } from './element' 11 | 12 | import { 13 | mountFunctionComponent, 14 | updateFunctionComponent, 15 | mountClassComponent, 16 | updateClassComponent, 17 | mountLazyComponent, 18 | updateLazyComponent 19 | } from './render' 20 | 21 | import type { 22 | Visitor, 23 | YieldFrame, 24 | ClassFrame, 25 | Frame, 26 | ContextMap, 27 | ContextEntry, 28 | DefaultProps, 29 | ComponentStatics, 30 | LazyElement, 31 | AbstractElement, 32 | ConsumerElement, 33 | ProviderElement, 34 | FragmentElement, 35 | SuspenseElement, 36 | ForwardRefElement, 37 | MemoElement, 38 | UserElement, 39 | DOMElement 40 | } from './types' 41 | 42 | import { 43 | getCurrentContextMap, 44 | getCurrentContextStore, 45 | setCurrentContextMap, 46 | setCurrentContextStore, 47 | flushPrevContextMap, 48 | flushPrevContextStore, 49 | restoreContextMap, 50 | restoreContextStore, 51 | readContextValue, 52 | setContextValue, 53 | setCurrentIdentity, 54 | setCurrentErrorFrame, 55 | getCurrentErrorFrame, 56 | Dispatcher 57 | } from './internals' 58 | 59 | import { 60 | REACT_ELEMENT_TYPE, 61 | REACT_PORTAL_TYPE, 62 | REACT_FRAGMENT_TYPE, 63 | REACT_STRICT_MODE_TYPE, 64 | REACT_PROFILER_TYPE, 65 | REACT_PROVIDER_TYPE, 66 | REACT_CONTEXT_TYPE, 67 | REACT_CONCURRENT_MODE_TYPE, 68 | REACT_FORWARD_REF_TYPE, 69 | REACT_SUSPENSE_TYPE, 70 | REACT_MEMO_TYPE, 71 | REACT_LAZY_TYPE 72 | } from './symbols' 73 | 74 | const { ReactCurrentDispatcher } = 75 | (React: any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED || 76 | (React: any).__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE 77 | 78 | // In the presence of setImmediate, i.e. on Node, we'll enable the 79 | // yielding behavior that gives the event loop a chance to continue 80 | // running when the prepasses would otherwise take too long 81 | export const SHOULD_YIELD = typeof setImmediate === 'function' 82 | // Time in ms after which the otherwise synchronous visitor yields so that 83 | // the event loop is not interrupted for too long 84 | const YIELD_AFTER_MS = 5 85 | 86 | const render = ( 87 | type: ComponentType & ComponentStatics, 88 | props: DefaultProps, 89 | queue: Frame[], 90 | visitor: Visitor, 91 | element: UserElement 92 | ) => { 93 | return shouldConstruct(type) 94 | ? mountClassComponent(type, props, queue, visitor, element) 95 | : mountFunctionComponent(type, props, queue, visitor, element) 96 | } 97 | 98 | export const visitElement = ( 99 | element: AbstractElement, 100 | queue: Frame[], 101 | visitor: Visitor 102 | ): AbstractElement[] => { 103 | switch (typeOf(element)) { 104 | case REACT_SUSPENSE_TYPE: 105 | case REACT_STRICT_MODE_TYPE: 106 | case REACT_CONCURRENT_MODE_TYPE: 107 | case REACT_PROFILER_TYPE: 108 | case REACT_FRAGMENT_TYPE: { 109 | // These element types are simply traversed over but otherwise ignored 110 | const fragmentElement = ((element: any): 111 | | FragmentElement 112 | | SuspenseElement) 113 | return getChildrenArray(fragmentElement.props.children) 114 | } 115 | 116 | case REACT_PROVIDER_TYPE: { 117 | const providerElement = ((element: any): ProviderElement) 118 | // Add provider's value prop to context 119 | const { value, children } = providerElement.props 120 | setContextValue(providerElement.type._context, value) 121 | 122 | return getChildrenArray(children) 123 | } 124 | 125 | case REACT_CONTEXT_TYPE: { 126 | const consumerElement = ((element: any): ConsumerElement) 127 | const { children } = consumerElement.props 128 | 129 | // Read from context and call children, if it's been passed 130 | if (typeof children === 'function') { 131 | const type = (consumerElement.type: any) 132 | const context = typeof type._context === 'object' ? type._context : type 133 | const value = readContextValue(context) 134 | return getChildrenArray(children(value)) 135 | } else { 136 | return [] 137 | } 138 | } 139 | 140 | case REACT_LAZY_TYPE: { 141 | const lazyElement = ((element: any): LazyElement) 142 | const type = lazyElement.type 143 | const child = mountLazyComponent(type, lazyElement.props, queue) 144 | return getChildrenArray(child) 145 | } 146 | 147 | case REACT_MEMO_TYPE: { 148 | const memoElement = ((element: any): MemoElement) 149 | const { type } = memoElement.type 150 | const child = createElement((type: any), memoElement.props) 151 | return getChildrenArray(child) 152 | } 153 | 154 | case REACT_FORWARD_REF_TYPE: { 155 | const refElement = ((element: any): ForwardRefElement) 156 | const { render: type, defaultProps } = refElement.type 157 | const props = computeProps(refElement.props, defaultProps) 158 | const child = createElement((type: any), props) 159 | return getChildrenArray(child) 160 | } 161 | 162 | case REACT_ELEMENT_TYPE: { 163 | const el = ((element: any): UserElement | DOMElement) 164 | if (typeof el.type === 'string') { 165 | // String elements can be skipped, so we just return children 166 | return getChildrenArray(el.props.children) 167 | } else { 168 | const userElement = ((element: any): UserElement) 169 | const { type, props } = userElement 170 | const child = render(type, props, queue, visitor, userElement) 171 | return getChildrenArray(child) 172 | } 173 | } 174 | 175 | case REACT_PORTAL_TYPE: 176 | // Portals are unsupported during SSR since they're DOM-only 177 | default: 178 | return [] 179 | } 180 | } 181 | 182 | const visitLoop = ( 183 | traversalChildren: AbstractElement[][], 184 | traversalMap: Array, 185 | traversalStore: Array, 186 | traversalErrorFrame: Array, 187 | queue: Frame[], 188 | visitor: Visitor 189 | ): boolean => { 190 | const prevDispatcher = ReactCurrentDispatcher.current 191 | const start = Date.now() 192 | 193 | try { 194 | ReactCurrentDispatcher.current = Dispatcher 195 | while (traversalChildren.length > 0) { 196 | const element = traversalChildren[traversalChildren.length - 1].shift() 197 | if (element !== undefined) { 198 | const children = visitElement(element, queue, visitor) 199 | traversalChildren.push(children) 200 | traversalMap.push(flushPrevContextMap()) 201 | traversalStore.push(flushPrevContextStore()) 202 | traversalErrorFrame.push(getCurrentErrorFrame()) 203 | } else { 204 | traversalChildren.pop() 205 | restoreContextMap(traversalMap.pop()) 206 | restoreContextStore(traversalStore.pop()) 207 | setCurrentErrorFrame(traversalErrorFrame.pop()) 208 | } 209 | 210 | if (SHOULD_YIELD && Date.now() - start > YIELD_AFTER_MS) { 211 | return true 212 | } 213 | } 214 | 215 | return false 216 | } catch (error) { 217 | const errorFrame = getCurrentErrorFrame() 218 | if (!errorFrame) throw error 219 | errorFrame.error = error 220 | queue.unshift(errorFrame) 221 | return false 222 | } finally { 223 | ReactCurrentDispatcher.current = prevDispatcher 224 | } 225 | } 226 | 227 | const makeYieldFrame = ( 228 | traversalChildren: AbstractElement[][], 229 | traversalMap: Array, 230 | traversalStore: Array, 231 | traversalErrorFrame: Array 232 | ): Frame => ({ 233 | contextMap: getCurrentContextMap(), 234 | contextStore: getCurrentContextStore(), 235 | errorFrame: getCurrentErrorFrame(), 236 | thenable: null, 237 | kind: 'frame.yield', 238 | traversalChildren, 239 | traversalMap, 240 | traversalStore, 241 | traversalErrorFrame 242 | }) 243 | 244 | export const visit = ( 245 | init: AbstractElement[], 246 | queue: Frame[], 247 | visitor: Visitor 248 | ) => { 249 | const traversalChildren: AbstractElement[][] = [init] 250 | const traversalMap: Array = [flushPrevContextMap()] 251 | const traversalStore: Array = [flushPrevContextStore()] 252 | const traversalErrorFrame: Array = [getCurrentErrorFrame()] 253 | 254 | const hasYielded = visitLoop( 255 | traversalChildren, 256 | traversalMap, 257 | traversalStore, 258 | traversalErrorFrame, 259 | queue, 260 | visitor 261 | ) 262 | 263 | if (hasYielded) { 264 | queue.unshift( 265 | makeYieldFrame( 266 | traversalChildren, 267 | traversalMap, 268 | traversalStore, 269 | traversalErrorFrame 270 | ) 271 | ) 272 | } 273 | } 274 | 275 | export const update = (frame: Frame, queue: Frame[], visitor: Visitor) => { 276 | if (frame.kind === 'frame.yield') { 277 | setCurrentIdentity(null) 278 | setCurrentContextMap(frame.contextMap) 279 | setCurrentContextStore(frame.contextStore) 280 | setCurrentErrorFrame(frame.errorFrame) 281 | 282 | const hasYielded = visitLoop( 283 | frame.traversalChildren, 284 | frame.traversalMap, 285 | frame.traversalStore, 286 | frame.traversalErrorFrame, 287 | queue, 288 | visitor 289 | ) 290 | 291 | if (hasYielded) { 292 | queue.unshift( 293 | makeYieldFrame( 294 | frame.traversalChildren, 295 | frame.traversalMap, 296 | frame.traversalStore, 297 | frame.traversalErrorFrame 298 | ) 299 | ) 300 | } 301 | } else { 302 | const prevDispatcher = ReactCurrentDispatcher.current 303 | let children = null 304 | 305 | ReactCurrentDispatcher.current = Dispatcher 306 | 307 | try { 308 | if (frame.kind === 'frame.class') { 309 | children = updateClassComponent(queue, frame) 310 | } else if (frame.kind === 'frame.hooks') { 311 | children = updateFunctionComponent(queue, frame) 312 | } else if (frame.kind === 'frame.lazy') { 313 | children = updateLazyComponent(queue, frame) 314 | } 315 | } catch (error) { 316 | const errorFrame = getCurrentErrorFrame() 317 | if (!errorFrame) throw error 318 | errorFrame.error = error 319 | queue.unshift(errorFrame) 320 | children = null 321 | } finally { 322 | ReactCurrentDispatcher.current = prevDispatcher 323 | } 324 | 325 | visit(getChildrenArray(children), queue, visitor) 326 | } 327 | } 328 | --------------------------------------------------------------------------------