├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
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 | Refresh
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 |
--------------------------------------------------------------------------------