diff --git a/examples/playground/components/after-paint/with-ssr.tsx b/examples/playground/components/after-paint/with-ssr.tsx index fb0799b..ce4655b 100644 --- a/examples/playground/components/after-paint/with-ssr.tsx +++ b/examples/playground/components/after-paint/with-ssr.tsx @@ -1,15 +1,12 @@ import React from 'react'; -import { controlFetch, isServer } from '../../utils'; +import { createSuspendableData } from '../../utils'; import { Result } from '../common/result'; -let hasThrown = false; +const useSuspendableData = createSuspendableData(); const ComponentWithSSR = () => { - if (!hasThrown && !isServer()) { - hasThrown = true; - throw controlFetch(true); - } + useSuspendableData(); return ; }; diff --git a/examples/playground/components/after-paint/without-ssr.tsx b/examples/playground/components/after-paint/without-ssr.tsx index d7a8390..88f492a 100644 --- a/examples/playground/components/after-paint/without-ssr.tsx +++ b/examples/playground/components/after-paint/without-ssr.tsx @@ -1,15 +1,12 @@ import React from 'react'; -import { controlFetch } from '../../utils'; +import { createSuspendableData } from '../../utils'; import { Result } from '../common/result'; -let hasThrown = false; +const useSuspendableData = createSuspendableData(); const ComponentWithoutSSR = () => { - if (!hasThrown) { - hasThrown = true; - throw controlFetch(true); - } + useSuspendableData(); return ; }; diff --git a/examples/playground/components/common/result.tsx b/examples/playground/components/common/result.tsx index e27df7f..1bf368a 100644 --- a/examples/playground/components/common/result.tsx +++ b/examples/playground/components/common/result.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { isServer } from '../../utils'; @@ -8,6 +8,11 @@ type Props = { }; export const Result = ({ isFallback, hasSsr }: Props) => { + const [isDone, setDone] = useState(isFallback); + useEffect(() => { + setDone(true); + }, []); + return (
{

{hasSsr ? 'With ssr' : 'Without ssr'}

  • - reactive: {isServer() ? '🅾️' : isFallback ? '☑️' : '✅'} + reactive:{' '} + {isServer() || !isDone ? '🅾️' : isFallback || !isDone ? '☑️' : '✅'}
  • - content: {isFallback ? '🅾️' : isServer() ? '☑️' : '✅'} + content:{' '} + {isFallback || !isDone ? '🅾️' : isServer() || !isDone ? '☑️' : '✅'}
diff --git a/examples/playground/components/common/section.tsx b/examples/playground/components/common/section.tsx index 3970579..389ad59 100644 --- a/examples/playground/components/common/section.tsx +++ b/examples/playground/components/common/section.tsx @@ -3,6 +3,7 @@ import { LazySuspense } from 'react-loosely-lazy'; import { Result } from './result'; import { Progress } from './progress'; +import { isServer } from '../../utils'; type Props = { components: { @@ -36,7 +37,8 @@ export const Section = memo( )} {WithoutSSR != null && ( }> - + {/* given use renderToString, we fake SSR fallback render */} + {isServer() ? : } )} diff --git a/examples/playground/components/custom-wait/with-ssr.tsx b/examples/playground/components/custom-wait/with-ssr.tsx index fb0799b..ce4655b 100644 --- a/examples/playground/components/custom-wait/with-ssr.tsx +++ b/examples/playground/components/custom-wait/with-ssr.tsx @@ -1,15 +1,12 @@ import React from 'react'; -import { controlFetch, isServer } from '../../utils'; +import { createSuspendableData } from '../../utils'; import { Result } from '../common/result'; -let hasThrown = false; +const useSuspendableData = createSuspendableData(); const ComponentWithSSR = () => { - if (!hasThrown && !isServer()) { - hasThrown = true; - throw controlFetch(true); - } + useSuspendableData(); return ; }; diff --git a/examples/playground/components/custom-wait/without-ssr.tsx b/examples/playground/components/custom-wait/without-ssr.tsx index d7a8390..88f492a 100644 --- a/examples/playground/components/custom-wait/without-ssr.tsx +++ b/examples/playground/components/custom-wait/without-ssr.tsx @@ -1,15 +1,12 @@ import React from 'react'; -import { controlFetch } from '../../utils'; +import { createSuspendableData } from '../../utils'; import { Result } from '../common/result'; -let hasThrown = false; +const useSuspendableData = createSuspendableData(); const ComponentWithoutSSR = () => { - if (!hasThrown) { - hasThrown = true; - throw controlFetch(true); - } + useSuspendableData(); return ; }; diff --git a/examples/playground/components/for-paint/with-ssr.tsx b/examples/playground/components/for-paint/with-ssr.tsx index fb0799b..ce4655b 100644 --- a/examples/playground/components/for-paint/with-ssr.tsx +++ b/examples/playground/components/for-paint/with-ssr.tsx @@ -1,15 +1,12 @@ import React from 'react'; -import { controlFetch, isServer } from '../../utils'; +import { createSuspendableData } from '../../utils'; import { Result } from '../common/result'; -let hasThrown = false; +const useSuspendableData = createSuspendableData(); const ComponentWithSSR = () => { - if (!hasThrown && !isServer()) { - hasThrown = true; - throw controlFetch(true); - } + useSuspendableData(); return ; }; diff --git a/examples/playground/components/for-paint/without-ssr.tsx b/examples/playground/components/for-paint/without-ssr.tsx index 4961370..ec08858 100644 --- a/examples/playground/components/for-paint/without-ssr.tsx +++ b/examples/playground/components/for-paint/without-ssr.tsx @@ -1,15 +1,12 @@ import React from 'react'; -import { controlFetch } from '../../utils'; +import { createSuspendableData } from '../../utils'; import { Result } from '../common/result'; -let hasThrown = false; +const useSuspendableData = createSuspendableData(); const ComponentNoSSR = () => { - if (!hasThrown) { - hasThrown = true; - throw controlFetch(true); - } + useSuspendableData(); return ; }; diff --git a/examples/playground/components/lazy/without-ssr.tsx b/examples/playground/components/lazy/without-ssr.tsx index 2ee3127..7f971f7 100644 --- a/examples/playground/components/lazy/without-ssr.tsx +++ b/examples/playground/components/lazy/without-ssr.tsx @@ -1,14 +1,11 @@ import React from 'react'; -import { controlFetch } from '../../utils'; +import { createSuspendableData } from '../../utils'; import { Result } from '../common/result'; -let hasThrown = false; +const useSuspendableData = createSuspendableData(); const ComponentWithSSR = () => { - if (!hasThrown) { - hasThrown = true; - throw controlFetch(true); - } + useSuspendableData(); return ; }; diff --git a/examples/playground/constants.tsx b/examples/playground/constants.tsx index f865a03..9b3c230 100644 --- a/examples/playground/constants.tsx +++ b/examples/playground/constants.tsx @@ -7,7 +7,7 @@ export const steps = [ 'PAINT READY', 'AFTER LOADING', 'AFTER FETCHING', - 'AFTER READY', // also 'LAZY READY' + 'AFTER READY', 'CUSTOM LOADING', 'CUSTOM FETCHING', 'CUSTOM READY', diff --git a/examples/playground/index.tsx b/examples/playground/index.tsx index 43a08d6..8591384 100644 --- a/examples/playground/index.tsx +++ b/examples/playground/index.tsx @@ -1,11 +1,17 @@ -import React, { useState, useEffect, FunctionComponent } from 'react'; -import { hydrate, render } from 'react-dom'; +import React, { + useState, + useEffect, + FunctionComponent, + StrictMode, +} from 'react'; +import { createRoot, hydrateRoot } from 'react-dom/client'; import { renderToString } from 'react-dom/server'; import { useLazyPhase, MODE } from 'react-loosely-lazy'; -import { listeners, steps, pastSteps } from './constants'; +import { listeners, steps, pastSteps } from './constants'; import { buildServerComponents, buildClientComponents } from './components'; +import { isFailSsr, isRender } from './utils'; /** * Controls App @@ -84,7 +90,7 @@ const App = ({ initialStep, components }: AppProps) => { }, [step, startNextPhase]); return ( - <> +

 

@@ -92,29 +98,34 @@ const App = ({ initialStep, components }: AppProps) => {
- +
); }; const renderApp = (v: string) => { const appContainer = document.querySelector('#app'); - const isFailSsr = window.location.search.includes('failssr'); - const isRender = isFailSsr || window.location.search.includes('render'); - const mode = isRender ? MODE.RENDER : MODE.HYDRATE; + const mode = isRender() ? MODE.RENDER : MODE.HYDRATE; - if (v === 'SSR' && appContainer && !isFailSsr) { + if (v === 'SSR' && appContainer && !isFailSsr()) { const components = buildServerComponents(mode); const ssr = renderToString(); - appContainer.innerHTML = isRender ? `
${ssr}
` : ssr; + appContainer.innerHTML = isRender() ? `
${ssr}
` : ssr; } if (v === 'PAINT LOADING') { const components = buildClientComponents(mode); - const renderer = isRender ? render : hydrate; - - renderer(, appContainer); + if (isRender()) { + createRoot(appContainer!).render( + + ); + } else { + hydrateRoot( + appContainer!, + + ); + } } }; listeners.add(renderApp); -render(, document.querySelector('#controls')); +createRoot(document.querySelector('#controls')!).render(); diff --git a/examples/playground/utils.ts b/examples/playground/utils.ts index cf42a75..778a08e 100644 --- a/examples/playground/utils.ts +++ b/examples/playground/utils.ts @@ -1,6 +1,9 @@ import { listeners } from './constants'; export const isServer = () => window.name === 'nodejs'; +export const isFailSsr = () => window.location.search.includes('failssr'); +export const isRender = () => + isFailSsr() || window.location.search.includes('render'); export const controlLoad = (result: T): Promise => { let resolve: (v: T) => void; @@ -14,7 +17,7 @@ export const controlLoad = (result: T): Promise => { return deferred; }; -export const controlFetch = (result: T): Promise => { +const controlFetch = (result: T): Promise => { let resolve: (v: T) => void; const deferred = new Promise(r => { resolve = r; @@ -25,3 +28,20 @@ export const controlFetch = (result: T): Promise => { return deferred; }; + +export const createSuspendableData = () => { + let promise: Promise | null = null; + let resolved = false; + + return function useSuspendableData() { + if (promise == null && !isServer()) { + promise = controlFetch(true); + } + + if (promise && !resolved) { + throw promise.then(() => { + resolved = true; + }); + } + }; +}; diff --git a/package-lock.json b/package-lock.json index 3632b4a..b39a02f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,8 +24,8 @@ "@babel/runtime": "^7.14.5", "@testing-library/jest-dom": "^5.14.1", "@types/jest": "^26.0.23", - "@types/react": "^16.14.8", - "@types/react-dom": "^16.9.13", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", "@types/testing-library__jest-dom": "^5.14.0", "@types/webpack": "^4.41.29", "@typescript-eslint/eslint-plugin": "^4.27.0", @@ -47,8 +47,8 @@ "flow-bin": "^0.151.0", "jest": "^26.6.3", "prettier": "^2.3.1", - "react": "^16.14.0", - "react-dom": "^16.14.0", + "react": "^18.0.0-0", + "react-dom": "^18.0.0-0", "semver": "^7.3.5", "tsconfig-paths-webpack-plugin": "^3.5.1", "typescript": "^4.3.3", @@ -3974,24 +3974,6 @@ "integrity": "sha512-hsPGC/U/0xe/WghMeSgyFsq9nNPfA5oY1Il2AaeAJcu/vTm4Bv8o9ev4eAgxcA61i5WWp72amN20XVyxWwM5aQ==", "dev": true }, - "node_modules/@testing-library/dom": { - "version": "7.31.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^4.2.0", - "aria-query": "^4.2.2", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.6", - "lz-string": "^1.4.4", - "pretty-format": "^26.6.2" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@testing-library/jest-dom": { "version": "5.14.1", "dev": true, @@ -4025,22 +4007,6 @@ "node": ">=8" } }, - "node_modules/@testing-library/react": { - "version": "11.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^7.28.1" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, "node_modules/@tootallnate/once": { "version": "1.1.2", "dev": true, @@ -4566,7 +4532,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "16.14.8", + "version": "17.0.41", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/@types/react/-/react-17.0.41.tgz", + "integrity": "sha512-chYZ9ogWUodyC7VUTRBfblysKLjnohhFY9bGLwvnUFFy48+vB9DikmB3lW0qTFmBcKSzmdglcvkHK71IioOlDA==", "dev": true, "license": "MIT", "dependencies": { @@ -4576,11 +4544,13 @@ } }, "node_modules/@types/react-dom": { - "version": "16.9.13", + "version": "17.0.14", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/@types/react-dom/-/react-dom-17.0.14.tgz", + "integrity": "sha512-H03xwEP1oXmSfl3iobtmQ/2dHF5aBHr8aUMwyGZya6OW45G+xtdzmq6HkncefiBt5JU8DVyaWl/nWZbjZCnzAQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/react": "^16" + "@types/react": "*" } }, "node_modules/@types/scheduler": { @@ -5167,7 +5137,9 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.0", + "version": "5.0.1", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" @@ -7803,7 +7775,9 @@ } }, "node_modules/dom-accessibility-api": { - "version": "0.5.6", + "version": "0.5.13", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/dom-accessibility-api/-/dom-accessibility-api-0.5.13.tgz", + "integrity": "sha512-R305kwb5CcMDIpSHUnLyIAp7SrSPBx6F0VfQFB3M75xVMHhXJJIdePYgbPPh1o57vCHNu5QztokWUPsLjWzFqw==", "dev": true, "license": "MIT" }, @@ -13023,6 +12997,7 @@ }, "node_modules/object-assign": { "version": "4.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14628,6 +14603,7 @@ }, "node_modules/prop-types": { "version": "15.7.2", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -14637,6 +14613,7 @@ }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", + "dev": true, "license": "MIT" }, "node_modules/proxy-addr": { @@ -14832,29 +14809,29 @@ } }, "node_modules/react": { - "version": "16.14.0", + "version": "18.0.0-rc.2-next-b9de50d2f-20220308", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/react/-/react-18.0.0-rc.2-next-b9de50d2f-20220308.tgz", + "integrity": "sha512-1RpONBLI/vgKN/kdq0DtPxrgXHS5OXCHbzXWXk2RcLRjlbBiqmAPUIpCq7x+02WhYQkgA6D0iRJo4NJ9fZMCGQ==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" + "loose-envify": "^1.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "16.14.0", + "version": "18.0.0-rc.1", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/react-dom/-/react-dom-18.0.0-rc.1.tgz", + "integrity": "sha512-BZ9NEwUp56MEguEwAzuh3u4bYE9Jv3QrzjaTmu11PV4C/lJCARTELEI16vjnHLq184GoJcCHMBrDILqlCrkZFQ==", "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" + "scheduler": "^0.21.0-rc.1" }, "peerDependencies": { - "react": "^16.14.0" + "react": "^18.0.0-rc.1" } }, "node_modules/react-is": { @@ -15661,12 +15638,13 @@ } }, "node_modules/scheduler": { - "version": "0.19.1", + "version": "0.21.0-rc.2-next-b9de50d2f-20220308", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/scheduler/-/scheduler-0.21.0-rc.2-next-b9de50d2f-20220308.tgz", + "integrity": "sha512-aBVNOtcCn7k8FUi0lsIY34DZN1SUeuAjAQB6Y55Cxrhtrx9mggUepmF5gTWn7Z2RkEhQtrp7fzmRA/24bxxNJQ==", "dev": true, "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "node_modules/schema-utils": { @@ -19416,14 +19394,90 @@ "version": "1.0.0", "license": "Apache-2.0", "devDependencies": { - "@testing-library/react": "^11.2.7", - "@types/react": "^16.14.8", - "@types/react-dom": "^16.9.13", - "react-dom": "^16.14.0" + "@testing-library/react": "^13.0.0-alpha.6", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", + "react-dom": "^18.0.0-0" }, "peerDependencies": { "@react-loosely-lazy/manifest": "1.0.0", - "react": "^16.9.0 || ^17.0.0-0" + "react": "^16.9.0 || ^17.0.0-0 || ^18.0.0-0" + } + }, + "packages/core/react-loosely-lazy/node_modules/@testing-library/dom": { + "version": "8.11.3", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/@testing-library/dom/-/dom-8.11.3.tgz", + "integrity": "sha512-9LId28I+lx70wUiZjLvi1DB/WT2zGOxUh46glrSNMaWVx849kKAluezVzZrXJfTKKoQTmEOutLes/bHg4Bj3aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^4.2.0", + "aria-query": "^5.0.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.4.4", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "packages/core/react-loosely-lazy/node_modules/@testing-library/react": { + "version": "13.0.0-alpha.6", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/@testing-library/react/-/react-13.0.0-alpha.6.tgz", + "integrity": "sha512-AVJTnwLlnjvXDNe91P6Nt9pN2fMS4csAzTmIbOewja+LVKzhlr53EONhv3ck0J3GzSZ5MIN5qL3BfISX/Wf1Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^8.5.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "18.0.0-rc.1", + "react-dom": "18.0.0-rc.1" + } + }, + "packages/core/react-loosely-lazy/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "packages/core/react-loosely-lazy/node_modules/aria-query": { + "version": "5.0.0", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/aria-query/-/aria-query-5.0.0.tgz", + "integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=6.0" + } + }, + "packages/core/react-loosely-lazy/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, "packages/plugins/babel": { @@ -19487,13 +19541,13 @@ "@react-loosely-lazy/integration-app": "^1.0.0", "@react-loosely-lazy/manifest": "^1.0.0", "@types/mini-css-extract-plugin": "^1.4.3", - "@types/react": "^16.14.8", - "@types/react-dom": "^16.9.13", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", "@types/webpack": "^4.41.29", "css-loader": "^5.2.6", "mini-css-extract-plugin": "^1.6.0", - "react": "^16.14.0", - "react-dom": "^16.14.0", + "react": "^18.0.0-0", + "react-dom": "^18.0.0-0", "react-loosely-lazy": "1.0.0", "tsconfig-paths-webpack-plugin": "^3.5.1", "webpack": "^4.46.0" @@ -19508,11 +19562,11 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "react": "^16.14.0", + "react": "^18.0.0-0", "react-loosely-lazy": "^1.0.0" }, "devDependencies": { - "@types/react": "^16.14.8" + "@types/react": "^17.0.0" } } }, @@ -22052,8 +22106,8 @@ "@react-loosely-lazy/integration-app": { "version": "file:packages/testing/integration-app", "requires": { - "@types/react": "^16.14.8", - "react": "^16.14.0", + "@types/react": "^17.0.0", + "react": "^18.0.0-0", "react-loosely-lazy": "^1.0.0" } }, @@ -22087,13 +22141,13 @@ "@react-loosely-lazy/integration-app": "^1.0.0", "@react-loosely-lazy/manifest": "^1.0.0", "@types/mini-css-extract-plugin": "^1.4.3", - "@types/react": "^16.14.8", - "@types/react-dom": "^16.9.13", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", "@types/webpack": "^4.41.29", "css-loader": "^5.2.6", "mini-css-extract-plugin": "^1.6.0", - "react": "^16.14.0", - "react-dom": "^16.14.0", + "react": "^18.0.0-0", + "react-dom": "^18.0.0-0", "react-loosely-lazy": "1.0.0", "tsconfig-paths-webpack-plugin": "^3.5.1", "webpack": "^4.46.0" @@ -22119,20 +22173,6 @@ "integrity": "sha512-hsPGC/U/0xe/WghMeSgyFsq9nNPfA5oY1Il2AaeAJcu/vTm4Bv8o9ev4eAgxcA61i5WWp72amN20XVyxWwM5aQ==", "dev": true }, - "@testing-library/dom": { - "version": "7.31.2", - "dev": true, - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^4.2.0", - "aria-query": "^4.2.2", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.6", - "lz-string": "^1.4.4", - "pretty-format": "^26.6.2" - } - }, "@testing-library/jest-dom": { "version": "5.14.1", "dev": true, @@ -22158,14 +22198,6 @@ } } }, - "@testing-library/react": { - "version": "11.2.7", - "dev": true, - "requires": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^7.28.1" - } - }, "@tootallnate/once": { "version": "1.1.2", "dev": true @@ -22565,7 +22597,9 @@ "dev": true }, "@types/react": { - "version": "16.14.8", + "version": "17.0.41", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/@types/react/-/react-17.0.41.tgz", + "integrity": "sha512-chYZ9ogWUodyC7VUTRBfblysKLjnohhFY9bGLwvnUFFy48+vB9DikmB3lW0qTFmBcKSzmdglcvkHK71IioOlDA==", "dev": true, "requires": { "@types/prop-types": "*", @@ -22574,10 +22608,12 @@ } }, "@types/react-dom": { - "version": "16.9.13", + "version": "17.0.14", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/@types/react-dom/-/react-dom-17.0.14.tgz", + "integrity": "sha512-H03xwEP1oXmSfl3iobtmQ/2dHF5aBHr8aUMwyGZya6OW45G+xtdzmq6HkncefiBt5JU8DVyaWl/nWZbjZCnzAQ==", "dev": true, "requires": { - "@types/react": "^16" + "@types/react": "*" } }, "@types/scheduler": { @@ -22989,7 +23025,9 @@ "version": "0.0.7" }, "ansi-regex": { - "version": "5.0.0" + "version": "5.0.1", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "ansi-styles": { "version": "4.3.0", @@ -24898,7 +24936,9 @@ } }, "dom-accessibility-api": { - "version": "0.5.6", + "version": "0.5.13", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/dom-accessibility-api/-/dom-accessibility-api-0.5.13.tgz", + "integrity": "sha512-R305kwb5CcMDIpSHUnLyIAp7SrSPBx6F0VfQFB3M75xVMHhXJJIdePYgbPPh1o57vCHNu5QztokWUPsLjWzFqw==", "dev": true }, "dom-serializer": { @@ -28480,7 +28520,8 @@ "dev": true }, "object-assign": { - "version": "4.1.1" + "version": "4.1.1", + "dev": true }, "object-copy": { "version": "0.1.0", @@ -29541,6 +29582,7 @@ }, "prop-types": { "version": "15.7.2", + "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -29548,7 +29590,8 @@ }, "dependencies": { "react-is": { - "version": "16.13.1" + "version": "16.13.1", + "dev": true } } }, @@ -29693,21 +29736,21 @@ } }, "react": { - "version": "16.14.0", + "version": "18.0.0-rc.2-next-b9de50d2f-20220308", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/react/-/react-18.0.0-rc.2-next-b9de50d2f-20220308.tgz", + "integrity": "sha512-1RpONBLI/vgKN/kdq0DtPxrgXHS5OXCHbzXWXk2RcLRjlbBiqmAPUIpCq7x+02WhYQkgA6D0iRJo4NJ9fZMCGQ==", "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" + "loose-envify": "^1.1.0" } }, "react-dom": { - "version": "16.14.0", + "version": "18.0.0-rc.1", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/react-dom/-/react-dom-18.0.0-rc.1.tgz", + "integrity": "sha512-BZ9NEwUp56MEguEwAzuh3u4bYE9Jv3QrzjaTmu11PV4C/lJCARTELEI16vjnHLq184GoJcCHMBrDILqlCrkZFQ==", "dev": true, "requires": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" + "scheduler": "^0.21.0-rc.1" } }, "react-is": { @@ -29717,10 +29760,61 @@ "react-loosely-lazy": { "version": "file:packages/core/react-loosely-lazy", "requires": { - "@testing-library/react": "^11.2.7", - "@types/react": "^16.14.8", - "@types/react-dom": "^16.9.13", - "react-dom": "^16.14.0" + "@testing-library/react": "^13.0.0-alpha.6", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", + "react-dom": "^18.0.0-0" + }, + "dependencies": { + "@testing-library/dom": { + "version": "8.11.3", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/@testing-library/dom/-/dom-8.11.3.tgz", + "integrity": "sha512-9LId28I+lx70wUiZjLvi1DB/WT2zGOxUh46glrSNMaWVx849kKAluezVzZrXJfTKKoQTmEOutLes/bHg4Bj3aA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^4.2.0", + "aria-query": "^5.0.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.4.4", + "pretty-format": "^27.0.2" + } + }, + "@testing-library/react": { + "version": "13.0.0-alpha.6", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/@testing-library/react/-/react-13.0.0-alpha.6.tgz", + "integrity": "sha512-AVJTnwLlnjvXDNe91P6Nt9pN2fMS4csAzTmIbOewja+LVKzhlr53EONhv3ck0J3GzSZ5MIN5qL3BfISX/Wf1Jg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^8.5.0" + } + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "aria-query": { + "version": "5.0.0", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/aria-query/-/aria-query-5.0.0.tgz", + "integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==", + "dev": true + }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + } + } } }, "react-refresh": { @@ -30259,11 +30353,12 @@ } }, "scheduler": { - "version": "0.19.1", + "version": "0.21.0-rc.2-next-b9de50d2f-20220308", + "resolved": "https://packages.atlassian.com/api/npm/npm-remote/scheduler/-/scheduler-0.21.0-rc.2-next-b9de50d2f-20220308.tgz", + "integrity": "sha512-aBVNOtcCn7k8FUi0lsIY34DZN1SUeuAjAQB6Y55Cxrhtrx9mggUepmF5gTWn7Z2RkEhQtrp7fzmRA/24bxxNJQ==", "dev": true, "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "schema-utils": { diff --git a/package.json b/package.json index cc4141b..bb39c89 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ "@babel/runtime": "^7.14.5", "@testing-library/jest-dom": "^5.14.1", "@types/jest": "^26.0.23", - "@types/react": "^16.14.8", - "@types/react-dom": "^16.9.13", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", "@types/testing-library__jest-dom": "^5.14.0", "@types/webpack": "^4.41.29", "@typescript-eslint/eslint-plugin": "^4.27.0", @@ -54,8 +54,8 @@ "flow-bin": "^0.151.0", "jest": "^26.6.3", "prettier": "^2.3.1", - "react": "^16.14.0", - "react-dom": "^16.14.0", + "react": "^18.0.0-0", + "react-dom": "^18.0.0-0", "semver": "^7.3.5", "tsconfig-paths-webpack-plugin": "^3.5.1", "typescript": "^4.3.3", diff --git a/packages/core/react-loosely-lazy/package.json b/packages/core/react-loosely-lazy/package.json index 241af0a..9b16213 100644 --- a/packages/core/react-loosely-lazy/package.json +++ b/packages/core/react-loosely-lazy/package.json @@ -26,13 +26,13 @@ "dist" ], "devDependencies": { - "@testing-library/react": "^11.2.7", - "@types/react": "^16.14.8", - "@types/react-dom": "^16.9.13", - "react-dom": "^16.14.0" + "@testing-library/react": "^13.0.0-alpha.6", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", + "react-dom": "^18.0.0-0" }, "peerDependencies": { "@react-loosely-lazy/manifest": "1.0.0", - "react": "^16.9.0 || ^17.0.0-0" + "react": "^16.9.0 || ^17.0.0-0 || ^18.0.0-0" } } diff --git a/packages/core/react-loosely-lazy/src/lazy-wait/context.ts b/packages/core/react-loosely-lazy/src/lazy-wait/context.ts index bcaa2ab..2f3e5c3 100644 --- a/packages/core/react-loosely-lazy/src/lazy-wait/context.ts +++ b/packages/core/react-loosely-lazy/src/lazy-wait/context.ts @@ -1,19 +1,9 @@ import { createContext } from 'react'; -import type { MutableRefObject } from 'react'; -import type { Cleanup } from '../cleanup'; +import type { SubscriptionContextValue } from '../lazy/types'; import { noopCleanup } from '../cleanup'; -export type UntilSubscriber = (until: boolean) => void; - -export type UntilContextValue = { - subscribe: (subscriber: UntilSubscriber) => Cleanup; - value: MutableRefObject; -}; - -export const UntilContext = createContext({ +export const WaitContext = createContext({ subscribe: () => noopCleanup, - value: { - current: true, - }, + currentValue: () => 1, }); diff --git a/packages/core/react-loosely-lazy/src/lazy-wait/index.tsx b/packages/core/react-loosely-lazy/src/lazy-wait/index.tsx index 54ab05e..71fc0ae 100644 --- a/packages/core/react-loosely-lazy/src/lazy-wait/index.tsx +++ b/packages/core/react-loosely-lazy/src/lazy-wait/index.tsx @@ -1,4 +1,3 @@ export { LazyWait } from './main'; export type { LazyWaitProps } from './main'; - -export { useUntil } from './utils'; +export { WaitContext } from './context'; diff --git a/packages/core/react-loosely-lazy/src/lazy-wait/main.tsx b/packages/core/react-loosely-lazy/src/lazy-wait/main.tsx index 174e5b8..c636eb3 100644 --- a/packages/core/react-loosely-lazy/src/lazy-wait/main.tsx +++ b/packages/core/react-loosely-lazy/src/lazy-wait/main.tsx @@ -1,8 +1,9 @@ import React, { useContext, useEffect, useRef } from 'react'; import type { ReactNode } from 'react'; -import { UntilContext } from './context'; -import type { UntilSubscriber } from './context'; +import { WaitContext } from './context'; +import type { SubscriptionContextValue } from '../lazy/types'; +// import type { UntilSubscriber } from './context'; export type LazyWaitProps = { until: boolean; @@ -10,37 +11,35 @@ export type LazyWaitProps = { }; export const LazyWait = ({ until, children }: LazyWaitProps) => { - const closestUntil = useContext(UntilContext); - const value = useRef(until && closestUntil.value.current); - const subscribers = useRef>(new Set()); - const api = useRef({ - subscribe: (subscriber: UntilSubscriber) => { + const closestWait = useContext(WaitContext); + const value = useRef(until && closestWait.currentValue() ? 1 : 0); + const subscribers = useRef void>>(new Set()); + const api = useRef({ + subscribe: subscriber => { subscribers.current.add(subscriber); return () => { subscribers.current.delete(subscriber); }; }, - value, + currentValue: () => value.current, }); useEffect(() => { // Notify subscribers when until prop or closest until value changes - const notify = (nextUntil: boolean) => { - value.current = nextUntil && until; + const notify = () => { + value.current = closestWait.currentValue() && until ? 1 : 0; for (const subscriber of subscribers.current) { - subscriber(value.current); + subscriber(); } }; - notify(closestUntil.value.current); + notify(); - return closestUntil.subscribe(notify); - }, [closestUntil, until]); + return closestWait.subscribe(notify); + }, [closestWait, until]); return ( - - {children} - + {children} ); }; diff --git a/packages/core/react-loosely-lazy/src/lazy-wait/utils.tsx b/packages/core/react-loosely-lazy/src/lazy-wait/utils.tsx deleted file mode 100644 index f654ed3..0000000 --- a/packages/core/react-loosely-lazy/src/lazy-wait/utils.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useContext, useEffect, useState } from 'react'; - -import { UntilContext } from './context'; - -export const useUntil = () => { - const { subscribe, value } = useContext(UntilContext); - const [until, setUntil] = useState(value.current); - - useEffect( - () => - subscribe(nextUntil => { - setUntil(nextUntil); - }), - [subscribe] - ); - - return until; -}; diff --git a/packages/core/react-loosely-lazy/src/lazy/components/client.tsx b/packages/core/react-loosely-lazy/src/lazy/components/client.tsx index 19fb289..b1d2b96 100644 --- a/packages/core/react-loosely-lazy/src/lazy/components/client.tsx +++ b/packages/core/react-loosely-lazy/src/lazy/components/client.tsx @@ -1,26 +1,64 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import React, { ComponentProps, ComponentType, lazy, + useCallback, useContext, - useEffect, useMemo, - useRef, useState, } from 'react'; import { getConfig, MODE } from '../../config'; import { COLLECTED, PHASE } from '../../constants'; -import { useUntil } from '../../lazy-wait'; +import { WaitContext } from '../../lazy-wait'; import { LazySuspenseContext } from '../../suspense'; -import { usePhaseSubscription } from '../../phase'; +import { LazyPhaseContext } from '../../phase'; import { PRIORITY } from '../constants'; import { Deferred } from '../deferred'; import { createLoaderError } from '../errors'; import { PlaceholderFallbackRender } from '../placeholders/render'; -import { PlaceholderFallbackHydrate } from '../placeholders/hydrate'; import { preloadAsset } from '../preload'; +import { SubscriptionContextValue } from '../types'; + +type Status = { + preload: boolean; + phase: boolean; + noWait: boolean; + start: boolean; +}; + +type UseSubscriptionArgs = { + context: C; + key: 'phase' | 'noWait'; + status: Status; + load: () => void; + comparator: (v: number) => boolean; +}; + +function useSubscription>({ + context, + key, + status, + load, + comparator, +}: UseSubscriptionArgs) { + const { subscribe, currentValue } = useContext(context); + useMemo(() => { + let unsubscribe: (() => void) | null = null; + const check = () => { + const done = comparator(currentValue()); + status[key] = done; + if (done) load(); + if (done && unsubscribe) unsubscribe(); + + return done; + }; + + unsubscribe = !check() ? subscribe(check) : null; + }, []); +} export function createComponentClient>({ defer, @@ -38,76 +76,78 @@ export function createComponentClient>({ const ResolvedLazy = lazy(() => deferred.promise); return (props: ComponentProps) => { - const { setFallback } = useContext(LazySuspenseContext); - const started = useRef(false); - const [, setState] = useState(); - const until = useUntil(); + const [status, bubbleError] = useState(() => ({ + preload: defer !== PHASE.AFTER_PAINT, + phase: defer === PHASE.LAZY, + noWait: true, + start: false, + })); - const load = useRef(() => { - if (started.current) { + const load = useCallback(() => { + if (deferred.result || status.start || !status.phase || !status.noWait) { return; } - started.current = true; + status.start = true; deferred.start().catch((err: Error) => { // Throw the error within the component lifecycle // refer to https://github.com/facebook/react/issues/11409 - setState(() => { + bubbleError(() => { throw createLoaderError(err); }); }); + }, []); + + // Subscribe to LazyWait context, triggering load when until is true + useSubscription({ + context: WaitContext, + key: 'noWait', + status, + load, + comparator: v => v === 1, }); - if (defer === PHASE.LAZY) { - useEffect(() => { - if (until) { - load.current(); - } - }, [until]); - } else { - const isOwnPhase = usePhaseSubscription(defer); + // Subscribe to LazyPhase context, triggering load when own phase starts + useSubscription({ + // @ts-expect-error Context Provider shape confuses TS + context: LazyPhaseContext, + key: 'phase', + status, + load, + comparator: v => v >= defer, + }); - useMemo(() => { - if (isOwnPhase && until) { - load.current(); - } - }, [isOwnPhase, until]); - - if (defer === PHASE.AFTER_PAINT) { - // Start preloading as will be needed soon - useEffect(() => { - if (!isOwnPhase) { - return preloadAsset({ - loader: deferred.preload, - moduleId, - priority: PRIORITY.LOW, - }); - } - }, [isOwnPhase]); + useMemo(() => { + if (!status.preload) { + status.preload = true; + preloadAsset({ + loader: deferred.preload, + moduleId, + priority: PRIORITY.LOW, + }); } - } + }, []); - useMemo(() => { - // find SSR content (or fallbacks) wrapped in inputs based on lazyId - const content = (COLLECTED.get(dataLazyId) || []).shift(); - if (!content) return; - - // override Suspense fallback with magic input wrappers - const { mode } = getConfig(); - const component = - mode === MODE.RENDER ? ( + const { setFallback } = useContext(LazySuspenseContext); + + if (getConfig().mode === MODE.RENDER) { + // Allow render mode to support partial progressive hydration + useMemo(() => { + // find SSR content (or fallbacks) wrapped in inputs based on lazyId + const content = (COLLECTED.get(dataLazyId) || []).shift(); + if (!content || !ssr) return; + + // override Suspense fallback with magic input wrappers + setFallback( - ) : ( - ); - setFallback(component); - }, [setFallback]); - - if (!ssr) { - // as the fallback is SSRd too, we want to discard it as soon as this - // mounts (to avoid hydration warnings) and let Suspense render it - useEffect(() => { - setFallback(null); + }, [setFallback]); + } else { + // Allow hydration to support partials without server components + useMemo(() => { + // suspense will discard ssr during hydration if re-renders so we + // set a dummy fallback to block updates from the provider until we resolve + setFallback(deferred.result ? null : <>); }, [setFallback]); } diff --git a/packages/core/react-loosely-lazy/src/lazy/components/server.tsx b/packages/core/react-loosely-lazy/src/lazy/components/server.tsx index 17916d8..1b04b84 100644 --- a/packages/core/react-loosely-lazy/src/lazy/components/server.tsx +++ b/packages/core/react-loosely-lazy/src/lazy/components/server.tsx @@ -1,9 +1,7 @@ -import { getAssetUrlsFromId } from '@react-loosely-lazy/manifest'; import React, { useContext } from 'react'; import type { ComponentProps, ComponentType } from 'react'; -import { getConfig } from '../../config'; -import { PHASE } from '../../constants'; +import { getConfig, MODE } from '../../config'; import { LazySuspenseContext } from '../../suspense'; import { getExport } from '../../utils'; @@ -34,24 +32,18 @@ export function createComponentServer>({ return (props: ComponentProps) => { const Resolved = ssr ? load(moduleId, loader) : null; const { fallback } = useContext(LazySuspenseContext); - const { crossOrigin, manifest } = getConfig(); + const { mode } = getConfig(); - return ( - <> - - {defer !== PHASE.LAZY && - getAssetUrlsFromId(manifest, moduleId)?.map(url => ( - - ))} - {Resolved ? : fallback} - - - ); + if (mode === MODE.RENDER) { + return ( + <> + + {Resolved ? : fallback} + + + ); + } + + return <>{Resolved ? : fallback}; }; } diff --git a/packages/core/react-loosely-lazy/src/lazy/deferred.ts b/packages/core/react-loosely-lazy/src/lazy/deferred.ts index 075a0c3..f0ee642 100644 --- a/packages/core/react-loosely-lazy/src/lazy/deferred.ts +++ b/packages/core/react-loosely-lazy/src/lazy/deferred.ts @@ -31,21 +31,11 @@ export const createDeferred = >( if (deferred.result) { return; } - - loader().then((m: any) => { - deferred.result = m; - }); + // just load the code, without resolving + loader(); }, start: () => { - if (deferred.result) { - resolve(deferred.result); - - return deferred.promise.then(() => { - // Return void... - }); - } else { - return loader().then(resolve); - } + return loader().then(resolve); }, }; diff --git a/packages/core/react-loosely-lazy/src/lazy/placeholders/hydrate.tsx b/packages/core/react-loosely-lazy/src/lazy/placeholders/hydrate.tsx deleted file mode 100644 index 55eb76c..0000000 --- a/packages/core/react-loosely-lazy/src/lazy/placeholders/hydrate.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { createElement, Fragment } from 'react'; -import { attrToProp } from '../../utils'; - -export type PlaceholderFallbackHydrateProps = { - id: string; - content: HTMLElement[]; -}; - -export const PlaceholderFallbackHydrate = ({ - id, - content, -}: PlaceholderFallbackHydrateProps) => { - return ( - <> - - {content.map((el: HTMLElement, i: number) => { - const { tagName = '', childNodes = [], attributes = [] } = el; - const props = Array.from(attributes).reduce(attrToProp, { - key: String(i), - }); - // text node - if (!tagName) return createElement(Fragment, props, el.textContent); - - // childless tag - if (!childNodes.length) - return createElement(tagName.toLowerCase(), props); - - // tag with content - return createElement(tagName.toLowerCase(), { - ...props, - dangerouslySetInnerHTML: { __html: '' }, - suppressHydrationWarning: true, - }); - })} - - - ); -}; diff --git a/packages/core/react-loosely-lazy/src/lazy/placeholders/render.tsx b/packages/core/react-loosely-lazy/src/lazy/placeholders/render.tsx index 3ab1f67..bfb95bb 100644 --- a/packages/core/react-loosely-lazy/src/lazy/placeholders/render.tsx +++ b/packages/core/react-loosely-lazy/src/lazy/placeholders/render.tsx @@ -1,9 +1,5 @@ import React, { useRef, useLayoutEffect } from 'react'; -function isLinkPrefetch(el: HTMLElement): el is HTMLLinkElement { - return el.tagName === 'LINK' && (el as HTMLLinkElement).rel === 'prefetch'; -} - const usePlaceholderRender = (resolveId: string, content: HTMLElement[]) => { const hydrationRef = useRef(null); const { current: ssrDomNodes } = useRef(content || ([] as HTMLElement[])); @@ -14,10 +10,6 @@ const usePlaceholderRender = (resolveId: string, content: HTMLElement[]) => { if (parentNode && !parentNode.contains(ssrDomNodes[0])) { ssrDomNodes.reverse().forEach((node: HTMLElement) => { - // this fixes an issue with Chrome that re-triggers and cancels prefetch - // when node is appended again, making network panel quite noisy - if (isLinkPrefetch(node)) node.rel = ''; - parentNode.insertBefore(node, (element as any).nextSibling); }); } diff --git a/packages/core/react-loosely-lazy/src/lazy/types.ts b/packages/core/react-loosely-lazy/src/lazy/types.ts index a16a018..abec18b 100644 --- a/packages/core/react-loosely-lazy/src/lazy/types.ts +++ b/packages/core/react-loosely-lazy/src/lazy/types.ts @@ -28,3 +28,8 @@ export type LazyComponent> = FunctionComponent< preload: (priority?: PreloadPriority) => Cleanup; getAssetUrls: () => string[] | undefined; }; + +export interface SubscriptionContextValue { + subscribe: (callback: () => void) => Cleanup; + currentValue: () => number; +} diff --git a/packages/core/react-loosely-lazy/src/phase/context.tsx b/packages/core/react-loosely-lazy/src/phase/context.tsx index 9a5f9ad..55fe71b 100644 --- a/packages/core/react-loosely-lazy/src/phase/context.tsx +++ b/packages/core/react-loosely-lazy/src/phase/context.tsx @@ -1,6 +1,7 @@ import { createContext, useContext } from 'react'; import { PHASE } from '../constants'; +import type { SubscriptionContextValue } from '../lazy/types'; import { LISTENERS } from './listeners'; import type { Listener } from './listeners'; @@ -13,9 +14,16 @@ export const setCurrent = (phase: number) => { LISTENERS.slice(0).forEach((listener: Listener) => listener(phase)); }; -export const LazyPhaseContext = createContext({ +interface LazyPhaseContextApi extends SubscriptionContextValue { + api: { + startNextPhase: () => void; + resetPhase: () => void; + }; +} + +export const LazyPhaseContext = createContext({ subscribe: createSubscribe(LISTENERS), - currentPhase: () => CURRENT_PHASE, + currentValue: () => CURRENT_PHASE, api: { startNextPhase: () => { setCurrent(PHASE.AFTER_PAINT); diff --git a/packages/core/react-loosely-lazy/src/phase/controller.ts b/packages/core/react-loosely-lazy/src/phase/controller.ts deleted file mode 100644 index 680d804..0000000 --- a/packages/core/react-loosely-lazy/src/phase/controller.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useContext, useMemo, useState, useEffect } from 'react'; - -import { LazyPhaseContext } from './context'; - -export const usePhaseSubscription = (targetPhase = -1) => { - const { subscribe, currentPhase } = useContext(LazyPhaseContext); - const [run, setRun] = useState(() => currentPhase() >= targetPhase); - - // subscribe with memo instead of effect to retain tree order - const unsubscribe = useMemo( - () => subscribe((v: number) => setRun(v >= targetPhase)), - [subscribe, setRun, targetPhase] - ); - // subscription is done on first render, here just unsubscribe - useEffect(() => { - return unsubscribe; - }, [unsubscribe]); - - return run; -}; diff --git a/packages/core/react-loosely-lazy/src/phase/index.tsx b/packages/core/react-loosely-lazy/src/phase/index.tsx index a995851..bc97dde 100644 --- a/packages/core/react-loosely-lazy/src/phase/index.tsx +++ b/packages/core/react-loosely-lazy/src/phase/index.tsx @@ -1,7 +1,5 @@ export { LazyPhaseContext, setCurrent, useLazyPhase } from './context'; -export { usePhaseSubscription } from './controller'; - export { LISTENERS } from './listeners'; export type { Listener } from './listeners'; diff --git a/packages/core/react-loosely-lazy/src/suspense/component.tsx b/packages/core/react-loosely-lazy/src/suspense/component.tsx index 75a3dfd..ce12131 100644 --- a/packages/core/react-loosely-lazy/src/suspense/component.tsx +++ b/packages/core/react-loosely-lazy/src/suspense/component.tsx @@ -1,8 +1,7 @@ import React, { Component, Suspense, useLayoutEffect } from 'react'; import type { FunctionComponent } from 'react'; -import { isNodeEnvironment } from '../utils'; - +import { getConfig, MODE } from '../config'; import { LazySuspenseContext } from './context'; import { Fallback, LazySuspenseContextType, LazySuspenseProps } from './types'; @@ -10,7 +9,6 @@ type LazySuspenseState = LazySuspenseContextType; type DynamicFallbackProps = { children(fallback: Fallback): any; - outsideSuspense: boolean; }; /** @@ -51,27 +49,25 @@ export class LazySuspense extends Component< setFallback: (fallback: Fallback) => { if (this.hydrationFallback === fallback) return; this.hydrationFallback = fallback; - // Schedule an update so we force switch from the sibling tree - // back to the suspense boundary - if (this.mounted) this.forceUpdate(); }, }; private hydrationFallback: Fallback = null; - private mounted = false; constructor(props: LazySuspenseProps) { super(props); this.DynamicFallback.displayName = 'DynamicFallback'; } - componentDidMount() { - this.mounted = true; + shouldComponentUpdate() { + // This is a workaround to prevent Suspense during hydration + // from switching to the fallback if a re-render occurs + // https://github.com/facebook/react/issues/22692 + return this.hydrationFallback ? false : true; } private DynamicFallback: FunctionComponent = ({ children, - outsideSuspense, }) => { // eslint-disable-next-line react-hooks/rules-of-hooks useLayoutEffect(() => { @@ -80,46 +76,33 @@ export class LazySuspense extends Component< // when both Lazy AND the eventual promises thrown are done // so Suspense will re-render with actual content and we remove // the hydration fallback at the same time - if (!outsideSuspense) this.state.setFallback(null); + this.state.setFallback(this.hydrationFallback); }; - }, [outsideSuspense]); + }, []); - return outsideSuspense - ? children(this.hydrationFallback ? this.hydrationFallback : null) - : children(this.hydrationFallback ? null : this.props.fallback); + return children(this.hydrationFallback || this.props.fallback); }; - private renderFallback(outsideSuspense: boolean) { - const { DynamicFallback } = this; + private renderFallback() { + if (getConfig().mode === MODE.RENDER) { + const { DynamicFallback } = this; - // Use render prop component to allow switch to hydration fallback - return ( - - {(fallback: Fallback) => fallback} - - ); - } + // Use render prop component to allow switch to hydration fallback + return ( + {(fallback: Fallback) => fallback} + ); + } - private renderServer() { - return ( - - {this.props.children} - - ); + return this.props.fallback; } - private renderClient() { + render() { return ( - + {this.props.children} - {(!this.mounted || this.hydrationFallback) && this.renderFallback(true)} ); } - - render() { - return isNodeEnvironment() ? this.renderServer() : this.renderClient(); - } } diff --git a/packages/core/react-loosely-lazy/src/suspense/context.tsx b/packages/core/react-loosely-lazy/src/suspense/context.tsx index 8134c75..176d492 100644 --- a/packages/core/react-loosely-lazy/src/suspense/context.tsx +++ b/packages/core/react-loosely-lazy/src/suspense/context.tsx @@ -1,10 +1,9 @@ import React, { Fragment, createContext } from 'react'; -import { Fallback, LazySuspenseContextType } from './types'; +import { LazySuspenseContextType } from './types'; export const LazySuspenseContext = createContext({ fallback: , - // eslint-disable-next-line @typescript-eslint/no-unused-vars - setFallback: (fallback: Fallback) => { + setFallback: () => { console.warn('Missing boundary'); }, }); diff --git a/packages/plugins/webpack/package.json b/packages/plugins/webpack/package.json index a858687..1ee30a5 100644 --- a/packages/plugins/webpack/package.json +++ b/packages/plugins/webpack/package.json @@ -28,13 +28,13 @@ "@react-loosely-lazy/integration-app": "^1.0.0", "@react-loosely-lazy/manifest": "^1.0.0", "@types/mini-css-extract-plugin": "^1.4.3", - "@types/react": "^16.14.8", - "@types/react-dom": "^16.9.13", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", "@types/webpack": "^4.41.29", "css-loader": "^5.2.6", "mini-css-extract-plugin": "^1.6.0", - "react": "^16.14.0", - "react-dom": "^16.14.0", + "react": "^18.0.0-0", + "react-dom": "^18.0.0-0", "react-loosely-lazy": "1.0.0", "tsconfig-paths-webpack-plugin": "^3.5.1", "webpack": "^4.46.0" diff --git a/packages/testing/integration-app/package.json b/packages/testing/integration-app/package.json index 8e7ef2f..9ad5a76 100644 --- a/packages/testing/integration-app/package.json +++ b/packages/testing/integration-app/package.json @@ -20,10 +20,10 @@ "sideEffects": true, "source": "src/index.tsx", "dependencies": { - "react": "^16.14.0", + "react": "^18.0.0-0", "react-loosely-lazy": "^1.0.0" }, "devDependencies": { - "@types/react": "^16.14.8" + "@types/react": "^17.0.0" } }