diff --git a/docusaurus/docs/examples.md b/docusaurus/docs/examples.md index 91cd74c..118b274 100755 --- a/docusaurus/docs/examples.md +++ b/docusaurus/docs/examples.md @@ -55,7 +55,9 @@ import App from './App'; // react-isomorphic-data needs fetch to be available in the global scope global.fetch = fetch; -express.get('/*', async (req: express.Request, res: express.Response) => { +const server = express(); + +server.get('/*', async (req: express.Request, res: express.Response) => { const dataClient = createDataClient({ initialCache: {}, ssr: true, @@ -71,7 +73,7 @@ express.get('/*', async (req: express.Request, res: express.Response) => { ); try { - await getDataFromTree(reactApp, dataClient); + await getDataFromTree(reactApp); } catch (err) { console.error('Error while trying to getDataFromTree', err); } @@ -95,4 +97,5 @@ express.get('/*', async (req: express.Request, res: express.Response) => { ` ); -} \ No newline at end of file +}); +``` diff --git a/docusaurus/docs/intro.md b/docusaurus/docs/intro.md index 0fc229e..878814c 100755 --- a/docusaurus/docs/intro.md +++ b/docusaurus/docs/intro.md @@ -11,7 +11,7 @@ NOTE: This project is still very much work in progress, use at your own risk ⚠ ### Features - React hooks -- SSR support +- SSR support with Suspense using [react-ssr-prepass](https://github.com/FormidableLabs/react-ssr-prepass) (No multi-rendering on the server!) - Simple built-in cache - TypeScript support - [Testing utilities](./testing/writing-tests) diff --git a/docusaurus/docs/ssr/client-side-hydration.md b/docusaurus/docs/ssr/client-side-hydration.md index 7e6ca95..a51d8d7 100644 --- a/docusaurus/docs/ssr/client-side-hydration.md +++ b/docusaurus/docs/ssr/client-side-hydration.md @@ -23,7 +23,7 @@ const tree = ( let markup; try { - await getDataFromTree(tree, dataClient); + await getDataFromTree(tree); markup = renderToString(tree); } catch (err) { console.error('An error happened during server side rendering!'); diff --git a/docusaurus/docs/ssr/getDataFromTree.md b/docusaurus/docs/ssr/getDataFromTree.md index 492f01f..d5ea74d 100644 --- a/docusaurus/docs/ssr/getDataFromTree.md +++ b/docusaurus/docs/ssr/getDataFromTree.md @@ -8,7 +8,6 @@ description: 'Explanation for getDataFromTree() in react-isomorphic-data' ## `getDataFromTree()` Params * `tree: React.ReactElement` -* `dataClient: DataClient` This function return a `Promise` that will resolve to a `string`, containing the static markup for the React app. Most of the time, you are not going to use the static markup generated by this function for your response. The main purpose @@ -35,7 +34,9 @@ import App from './App'; // react-isomorphic-data needs fetch to be available in the global scope global.fetch = fetch; -express.get('/*', async (req, res) => { +const server = express(); + +server.get('/*', async (req, res) => { const dataClient = createDataClient({ initialCache: {}, ssr: true, // set this to true on server side @@ -63,5 +64,5 @@ express.get('/*', async (req, res) => { `); -} +}); ``` diff --git a/docusaurus/docs/ssr/intro.md b/docusaurus/docs/ssr/intro.md index 586b365..a0c83ed 100644 --- a/docusaurus/docs/ssr/intro.md +++ b/docusaurus/docs/ssr/intro.md @@ -27,7 +27,9 @@ import App from './App'; // react-isomorphic-data needs fetch to be available in the global scope global.fetch = fetch; -express.get('/*', async (req, res) => { +const server = express(); + +server.get('/*', async (req, res) => { const dataClient = createDataClient({ initialCache: {}, ssr: true, // set this to true on server side @@ -42,7 +44,7 @@ express.get('/*', async (req, res) => { let markup; try { - markup = await renderToStringWithData(tree, dataClient); + markup = await renderToStringWithData(tree); } catch (err) { console.error('An error happened during server side rendering!'); } @@ -54,5 +56,5 @@ express.get('/*', async (req, res) => { `); -} +}); ``` \ No newline at end of file diff --git a/docusaurus/docs/ssr/prefetching.md b/docusaurus/docs/ssr/prefetching.md index 374b80b..4dcd27a 100644 --- a/docusaurus/docs/ssr/prefetching.md +++ b/docusaurus/docs/ssr/prefetching.md @@ -7,7 +7,7 @@ description: 'Adding prefetch hints to optimise loading with react-isomorphic-da `react-isomorphic-data` provide helper function to inject `` tags in to the server-side rendered HTML. This is done to give hints to the browser that the particular resource should be prefetched. Prefetched resources have a very low priority and will only be run when the browser is idle. Prefetching resources could improve performance because when the resource is requested, it might already be ready in the prefetch cache. More about prefetch [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Link_prefetching_FAQ). -In general, you should prefetch data that you think will have a high possibility to be requested by the page. +In general, you should prefetch data that you think will be needed later by the page, but not on the initial load. ## Example First, make sure you have are setting `dataOptions.prefetch` to true for some of your data. @@ -30,7 +30,7 @@ Then, in your server side rendering code, you can `createPrefetchTags` from the ```javascript import { renderToStringWithData, createPrefetchTags } from 'react-isomorphic-data/ssr'; -express.get('/*', async (req, res) => { +server.get('/*', async (req, res) => { const dataClient = createDataClient({ initialCache: {}, ssr: true, // set this to true on server side @@ -45,7 +45,7 @@ express.get('/*', async (req, res) => { let markup; try { - markup = await renderToStringWithData(tree, dataClient); + markup = await renderToStringWithData(tree); } catch (err) { console.error('An error happened during server side rendering!'); } @@ -60,7 +60,7 @@ express.get('/*', async (req, res) => { `); -} +}); ``` ## Note diff --git a/docusaurus/docs/ssr/renderToStringWithData.md b/docusaurus/docs/ssr/renderToStringWithData.md index ec0f79f..5db2bf5 100644 --- a/docusaurus/docs/ssr/renderToStringWithData.md +++ b/docusaurus/docs/ssr/renderToStringWithData.md @@ -8,7 +8,6 @@ description: 'Explanation for renderToStringWithData() in react-isomorphic-data' ## `renderToStringWithData()` Params * `tree: React.ReactElement` -* `dataClient: DataClient` This function return a `Promise` that will resolve to a `string`, containing the markup for the React app. @@ -28,7 +27,9 @@ import App from './App'; // react-isomorphic-data needs fetch to be available in the global scope global.fetch = fetch; -express.get('/*', async (req, res) => { +const server = express(); + +server.get('/*', async (req, res) => { const dataClient = createDataClient({ initialCache: {}, ssr: true, // set this to true on server side @@ -55,5 +56,5 @@ express.get('/*', async (req, res) => { `); -} +}); ``` diff --git a/examples/ssr/package.json b/examples/ssr/package.json index 8dc342e..3de7008 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -27,6 +27,7 @@ "express": "^4.17.1", "hoist-non-react-statics": "^3.3.0", "node-fetch": "^2.6.0", + "node-stdev": "^1.0.1", "razzle": "^3.0.0", "razzle-plugin-typescript": "^3.0.0", "react": "^16.10.1", diff --git a/examples/ssr/result-normal-ssr.json b/examples/ssr/result-normal-ssr.json new file mode 100644 index 0000000..3073bb4 --- /dev/null +++ b/examples/ssr/result-normal-ssr.json @@ -0,0 +1 @@ +{"name":"normal-ssr","average":11.492283940594056,"stdev":4.21,"runs":101} \ No newline at end of file diff --git a/examples/ssr/result-ssr-prepass-no-multi-rendering-prod.json b/examples/ssr/result-ssr-prepass-no-multi-rendering-prod.json new file mode 100644 index 0000000..d8c70d4 --- /dev/null +++ b/examples/ssr/result-ssr-prepass-no-multi-rendering-prod.json @@ -0,0 +1 @@ +{"name":"ssr-prepass-no-multi-rendering-prod","average":9.321476039215687,"stdev":2.23,"runs":102} \ No newline at end of file diff --git a/examples/ssr/result-ssr-prepass-no-multi-rendering.json b/examples/ssr/result-ssr-prepass-no-multi-rendering.json new file mode 100644 index 0000000..6c1f427 --- /dev/null +++ b/examples/ssr/result-ssr-prepass-no-multi-rendering.json @@ -0,0 +1 @@ +{"name":"ssr-prepass-no-multi-rendering","average":9.96071270588235,"stdev":4.14,"runs":102} \ No newline at end of file diff --git a/examples/ssr/result-ssr-prepass.json b/examples/ssr/result-ssr-prepass.json new file mode 100644 index 0000000..164e992 --- /dev/null +++ b/examples/ssr/result-ssr-prepass.json @@ -0,0 +1 @@ +{"name":"ssr-prepass","average":10.909492742574258,"stdev":2.45,"runs":101} \ No newline at end of file diff --git a/examples/ssr/src/server.tsx b/examples/ssr/src/server.tsx index 2f4bce8..fe41570 100644 --- a/examples/ssr/src/server.tsx +++ b/examples/ssr/src/server.tsx @@ -14,6 +14,34 @@ const globalAny: any = global; globalAny.fetch = fetch; let assets: any; +const time: [number, number][] = []; +const methodName = 'ssr-prepass-no-multi-rendering-prod'; + +const getResult = () => { + console.info("================ RESULT ================"); + const durations = time.map(t => (t[0] + t[1] / 1e9) * 1e3); + + durations.forEach((d, i) => { + console.info(`Run ${i} took `, d, "ms"); + }); + + console.info("================ SUMMARY ================"); + console.info(`[${methodName}]`); + console.info( + "Average is:", + durations.reduce((a, b) => a + b) / durations.length, + "ms" + ); + console.info("Stdev is:", require("node-stdev").population(durations), "ms"); + + // uncomment this to write result into a json file + // require('fs').writeFileSync(`./result-${methodName}.json`, JSON.stringify({ + // name: methodName, + // average: durations.reduce((a, b) => a + b) / durations.length, + // stdev: require("node-stdev").population(durations), + // runs: durations.length, + // })); +} const syncLoadAssets = () => { // eslint-disable-next-line @@ -70,7 +98,11 @@ server.get('/*', async (req: express.Request, res: express.Response) => { let markup; // pass the same dataClient instance you are passing to your provider here try { + const start = process.hrtime(); markup = await renderToStringWithData(reactApp, dataClient); + time.push(process.hrtime(start)); + + getResult(); } catch (err) { console.error('Error while trying to getDataFromTree', err); } diff --git a/packages/react-isomorphic-data/README.md b/packages/react-isomorphic-data/README.md index 3968709..c36c6f2 100644 --- a/packages/react-isomorphic-data/README.md +++ b/packages/react-isomorphic-data/README.md @@ -7,7 +7,7 @@ NOTE: This project is still very much work in progress, use at your own risk ⚠ ### Features - React hooks -- SSR support +- SSR support with Suspense using [react-ssr-prepass](https://github.com/FormidableLabs/react-ssr-prepass) (No multi-rendering on the server!) - Simple built-in cache - TypeScript support - [Testing utilities](https://react-isomorphic-data.netlify.com/docs/testing/writing-tests) @@ -67,7 +67,9 @@ import App from './App'; // react-isomorphic-data needs fetch to be available in the global scope global.fetch = fetch; -express.get('/*', async (req: express.Request, res: express.Response) => { +const server = express(); + +server.get('/*', async (req: express.Request, res: express.Response) => { const dataClient = createDataClient({ initialCache: {}, ssr: true, @@ -84,7 +86,7 @@ express.get('/*', async (req: express.Request, res: express.Response) => { ); try { - await getDataFromTree(reactApp, dataClient); + await getDataFromTree(reactApp); } catch (err) { console.error('Error while trying to getDataFromTree', err); } @@ -108,7 +110,7 @@ express.get('/*', async (req: express.Request, res: express.Response) => { ` ); -} +}); ``` ### Documentations [![Netlify Status](https://api.netlify.com/api/v1/badges/81844630-ff7d-4bf6-95f0-9f170ba6e421/deploy-status)](https://app.netlify.com/sites/unruffled-austin-36e969/deploys) diff --git a/packages/react-isomorphic-data/package.json b/packages/react-isomorphic-data/package.json index 16be943..fa84e16 100644 --- a/packages/react-isomorphic-data/package.json +++ b/packages/react-isomorphic-data/package.json @@ -60,6 +60,7 @@ "prettier": "^1.18.2", "react": "^16.8.0", "react-dom": "^16.8.0", + "react-ssr-prepass": "^1.1.2", "rollup": "^1.19.3", "rollup-plugin-commonjs": "^10.0.2", "rollup-plugin-json": "^4.0.0", diff --git a/packages/react-isomorphic-data/src/common/Client.ts b/packages/react-isomorphic-data/src/common/Client.ts index 396c8fc..684ee6f 100644 --- a/packages/react-isomorphic-data/src/common/Client.ts +++ b/packages/react-isomorphic-data/src/common/Client.ts @@ -9,7 +9,6 @@ export const createDataClient = ( return { cache: initialCache ? { ...initialCache } : {}, - pendingPromiseFactories: [], ssr: ssr || false, test: test || false, headers: headers || {}, diff --git a/packages/react-isomorphic-data/src/common/types.ts b/packages/react-isomorphic-data/src/common/types.ts index 7a9b870..b74b352 100644 --- a/packages/react-isomorphic-data/src/common/types.ts +++ b/packages/react-isomorphic-data/src/common/types.ts @@ -3,7 +3,6 @@ export interface DataResource { }; export interface DataClient { cache: Record; - pendingPromiseFactories: { () : Promise }[]; toBePrefetched: Record; ssr: boolean; test: boolean; diff --git a/packages/react-isomorphic-data/src/hooks/utils/useBaseData.ts b/packages/react-isomorphic-data/src/hooks/utils/useBaseData.ts index e2c46f7..c96ba71 100644 --- a/packages/react-isomorphic-data/src/hooks/utils/useBaseData.ts +++ b/packages/react-isomorphic-data/src/hooks/utils/useBaseData.ts @@ -131,8 +131,10 @@ const useBaseData = ( // if this data is supposed to be fetched during SSR if (isSSR) { if (!promisePushed.current && !lazy && !dataFromCache) { - client.pendingPromiseFactories.push(memoizedFetchData); promisePushed.current = true; + + // throw a promise here. react-ssr-prepass will handle the suspension + throw memoizedFetchData(); } } diff --git a/packages/react-isomorphic-data/src/ssr/__tests__/renderToStringWithData.test.tsx b/packages/react-isomorphic-data/src/ssr/__tests__/renderToStringWithData.test.tsx index 39345ac..3f82e6f 100644 --- a/packages/react-isomorphic-data/src/ssr/__tests__/renderToStringWithData.test.tsx +++ b/packages/react-isomorphic-data/src/ssr/__tests__/renderToStringWithData.test.tsx @@ -34,7 +34,7 @@ describe('Server-side rendering utilities test', () => { ); - await getDataFromTree(App, client); + await getDataFromTree(App); expect(retrieveFromCache(client.cache, url)).toStrictEqual({ message: 'Hello world!' }); }); @@ -53,7 +53,7 @@ describe('Server-side rendering utilities test', () => { ); - const markup = await renderToStringWithData(App, client); + const markup = await renderToStringWithData(App); expect(markup).toContain('Hello world!'); }); @@ -83,7 +83,7 @@ describe('Server-side rendering utilities test', () => { ); - const markup = await renderToStringWithData(App, client); + const markup = await renderToStringWithData(App); expect(markup).toContain('ComponentB: Hello world!'); }); @@ -118,7 +118,7 @@ describe('Server-side rendering utilities test', () => { ); - const markup = await renderToStringWithData(App, client); + const markup = await renderToStringWithData(App); expect(markup).toContain('ComponentB: Hello world!'); }); @@ -145,7 +145,7 @@ describe('Server-side rendering utilities test', () => { ); - const markup = await renderToStringWithData(App, client); + const markup = await renderToStringWithData(App); expect(markup).toContain('loading...'); @@ -179,7 +179,7 @@ describe('Server-side rendering utilities test', () => { ); - const markup = await renderToStringWithData(App, client); + const markup = await renderToStringWithData(App); expect(markup).toContain('loading...'); diff --git a/packages/react-isomorphic-data/src/ssr/getDataFromTree.ts b/packages/react-isomorphic-data/src/ssr/getDataFromTree.ts index 13d6070..892ced8 100644 --- a/packages/react-isomorphic-data/src/ssr/getDataFromTree.ts +++ b/packages/react-isomorphic-data/src/ssr/getDataFromTree.ts @@ -1,13 +1,9 @@ import * as React from 'react'; -import ReactDOMServer from 'react-dom/server'; import baseGetData from './utils/baseGetData'; -import { DataClient } from '../common/types'; -const { renderToStaticMarkup } = ReactDOMServer; - -const getDataFromTree = async (tree: React.ReactElement, client: DataClient): Promise => { - return baseGetData(tree, client, renderToStaticMarkup); +const getDataFromTree = async (tree: React.ReactElement): Promise => { + return baseGetData(tree); } export default getDataFromTree; diff --git a/packages/react-isomorphic-data/src/ssr/renderToStringWithData.ts b/packages/react-isomorphic-data/src/ssr/renderToStringWithData.ts index a85cad1..a23372c 100644 --- a/packages/react-isomorphic-data/src/ssr/renderToStringWithData.ts +++ b/packages/react-isomorphic-data/src/ssr/renderToStringWithData.ts @@ -2,12 +2,13 @@ import * as React from 'react'; import ReactDOMServer from 'react-dom/server'; import baseGetData from './utils/baseGetData'; -import { DataClient } from '../common/types'; const { renderToString } = ReactDOMServer; -const getDataFromTree = async (tree: React.ReactElement, client: DataClient): Promise => { - return baseGetData(tree, client, renderToString); +const getDataFromTree = async (tree: React.ReactElement): Promise => { + await baseGetData(tree); + + return renderToString(tree); }; export default getDataFromTree; diff --git a/packages/react-isomorphic-data/src/ssr/utils/baseGetData.ts b/packages/react-isomorphic-data/src/ssr/utils/baseGetData.ts index 55ade65..165f732 100644 --- a/packages/react-isomorphic-data/src/ssr/utils/baseGetData.ts +++ b/packages/react-isomorphic-data/src/ssr/utils/baseGetData.ts @@ -1,39 +1,13 @@ import * as React from 'react'; -import ReactDOMServer from 'react-dom/server'; - -import { DataClient } from '../../common/types'; - -const { renderToStaticMarkup } = ReactDOMServer; +import ssrPrepass from 'react-ssr-prepass' const baseGetData = async ( - tree: React.ReactElement, - client: DataClient, - renderFunction: (tree: React.ReactElement) => string = renderToStaticMarkup, -): Promise => { - let prevPendingPromisesLength = 0; - let lastMarkup: string; - - while (true) { - lastMarkup = renderFunction(tree); - - const currPendingPromisesLength = client.pendingPromiseFactories.length; - - if (currPendingPromisesLength > prevPendingPromisesLength) { - const arrayOfPromises = client.pendingPromiseFactories.map((p, index) => { - if (index >= prevPendingPromisesLength) { - return p(); - } else { - return new Promise((resolve) => resolve()); - } - }); - - prevPendingPromisesLength = currPendingPromisesLength; + tree: React.ReactElement +): Promise => { + // we use `react-ssr-prepass` that will automatically suspends on thrown promise + await ssrPrepass(tree); - await Promise.all(arrayOfPromises); - } else { - return lastMarkup; - } - } + return; }; export default baseGetData; diff --git a/yarn.lock b/yarn.lock index 214ad6e..2e15fa5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9500,7 +9500,7 @@ lodash@4.17.14: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw== -lodash@4.17.15, lodash@^4, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: +lodash@4.17.15, lodash@^4, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -10265,6 +10265,13 @@ node-releases@^1.0.0-alpha.11, node-releases@^1.1.38: dependencies: semver "^6.3.0" +node-stdev@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/node-stdev/-/node-stdev-1.0.1.tgz#7a4ba4ae44123683b9f4f06a25e0ec88b1ff1c54" + integrity sha1-ekukrkQSNoO59PBqJeDsiLH/HFQ= + dependencies: + lodash "^4.17.2" + "nopt@2 || 3": version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -10443,6 +10450,11 @@ object-is@^1.0.1: resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6" integrity sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY= +object-is@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.2.tgz#6b80eb84fe451498f65007982f035a5b445edec4" + integrity sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ== + object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.0.6, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -12243,6 +12255,13 @@ react-router@5.1.2: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" +react-ssr-prepass@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/react-ssr-prepass/-/react-ssr-prepass-1.1.2.tgz#3a7b60798e8f8a7002cd34fad48420937d3285f7" + integrity sha512-z1FN7yO+a+C5Y0wDLhsseIBX3yn823cXjs5MuyKRtrSDIVNsYKUIB7++vc4uqI5eMSr8kpYjFgc1i6iJRUuJdQ== + dependencies: + object-is "^1.0.2" + react@^16.10.1, react@^16.8.0: version "16.11.0" resolved "https://registry.yarnpkg.com/react/-/react-16.11.0.tgz#d294545fe62299ccee83363599bf904e4a07fdbb"