diff --git a/packages/pwa-kit-dev/package-lock.json b/packages/pwa-kit-dev/package-lock.json
index e8be12439a..b73df640a4 100644
--- a/packages/pwa-kit-dev/package-lock.json
+++ b/packages/pwa-kit-dev/package-lock.json
@@ -1813,6 +1813,23 @@
"@sinonjs/commons": "^1.7.0"
}
},
+ "@tanstack/query-core": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.0.10.tgz",
+ "integrity": "sha512-9LsABpZXkWZHi4P1ozRETEDXQocLAxVzQaIhganxbNuz/uA3PsCAJxJTiQrknG5htLMzOF5MqM9G10e6DCxV1A==",
+ "dev": true
+ },
+ "@tanstack/react-query": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.0.10.tgz",
+ "integrity": "sha512-Wn5QhZUE5wvr6rGClV7KeQIUsdTmYR9mgmMZen7DSRWauHW2UTynFg3Kkf6pw+XlxxOLsyLWwz/Q6q1lSpM3TQ==",
+ "dev": true,
+ "requires": {
+ "@tanstack/query-core": "^4.0.0-beta.1",
+ "@types/use-sync-external-store": "^0.0.3",
+ "use-sync-external-store": "^1.2.0"
+ }
+ },
"@tootallnate/once": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
@@ -1945,6 +1962,12 @@
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="
},
+ "@types/use-sync-external-store": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
+ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==",
+ "dev": true
+ },
"@types/yargs": {
"version": "15.0.14",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz",
@@ -10199,6 +10222,12 @@
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
},
+ "use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "dev": true
+ },
"util": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
diff --git a/packages/pwa-kit-dev/package.json b/packages/pwa-kit-dev/package.json
index d70846f55c..8f8aa64945 100644
--- a/packages/pwa-kit-dev/package.json
+++ b/packages/pwa-kit-dev/package.json
@@ -109,6 +109,7 @@
},
"devDependencies": {
"@loadable/component": "^5.15.0",
+ "@tanstack/react-query": "^4.0.10",
"internal-lib-build": "^2.3.0-dev",
"nock": "^13.1.1",
"superagent": "^6.1.0",
diff --git a/packages/pwa-kit-dev/src/configs/webpack/config.js b/packages/pwa-kit-dev/src/configs/webpack/config.js
index 2c81dd0995..8a9e173774 100644
--- a/packages/pwa-kit-dev/src/configs/webpack/config.js
+++ b/packages/pwa-kit-dev/src/configs/webpack/config.js
@@ -120,6 +120,7 @@ const baseConfig = (target) => {
'@loadable/webpack-plugin': findInProjectThenSDK(
'@loadable/webpack-plugin'
),
+ '@tanstack/react-query': findInProjectThenSDK('@tanstack/react-query'),
'svg-sprite-loader': findInProjectThenSDK('svg-sprite-loader'),
react: findInProjectThenSDK('react'),
'react-router-dom': findInProjectThenSDK('react-router-dom'),
diff --git a/packages/pwa-kit-react-sdk/CHANGELOG.md b/packages/pwa-kit-react-sdk/CHANGELOG.md
index 2faa6177b1..2f64bbaddf 100644
--- a/packages/pwa-kit-react-sdk/CHANGELOG.md
+++ b/packages/pwa-kit-react-sdk/CHANGELOG.md
@@ -1,3 +1,6 @@
+## To be released
+- Integrate `react-query`. [#693](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/693)
+
## v2.3.0-dev (Aug 25, 2022)
## v2.2.0 (Aug 25, 2022)
## v2.1.0 (Jul 05, 2022)
diff --git a/packages/pwa-kit-react-sdk/package-lock.json b/packages/pwa-kit-react-sdk/package-lock.json
index 66858ceff9..885794eaef 100644
--- a/packages/pwa-kit-react-sdk/package-lock.json
+++ b/packages/pwa-kit-react-sdk/package-lock.json
@@ -96,6 +96,29 @@
"integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==",
"dev": true
},
+ "@tanstack/query-core": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.2.3.tgz",
+ "integrity": "sha512-zdt5lYWs1dZaA3IxJbCgtAfHZJScRZONpiLL7YkeOkrme5MfjQqTpjq7LYbzpyuwPOh2Jx68le0PLl57JFv5hQ==",
+ "dev": true
+ },
+ "@tanstack/react-query": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.2.3.tgz",
+ "integrity": "sha512-JLaMOxoJTkiAu7QpevRCt2uI/0vd3E8K/rSlCuRgWlcW5DeJDFpDS5kfzmLO5MOcD97fgsJRrDbxDORxR1FdJA==",
+ "dev": true,
+ "requires": {
+ "@tanstack/query-core": "4.2.3",
+ "@types/use-sync-external-store": "^0.0.3",
+ "use-sync-external-store": "^1.2.0"
+ }
+ },
+ "@types/use-sync-external-store": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
+ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==",
+ "dev": true
+ },
"@wojtekmaj/enzyme-adapter-react-17": {
"version": "0.6.7",
"resolved": "https://registry.npmjs.org/@wojtekmaj/enzyme-adapter-react-17/-/enzyme-adapter-react-17-0.6.7.tgz",
@@ -2454,6 +2477,11 @@
"shallowequal": "^1.0.1"
}
},
+ "react-ssr-prepass": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz",
+ "integrity": "sha512-yFNHrlVEReVYKsLI5lF05tZoHveA5pGzjFbFJY/3pOqqjGOmMmqx83N4hIjN2n6E1AOa+eQEUxs3CgRnPmT0RQ=="
+ },
"react-test-renderer": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz",
@@ -3376,6 +3404,12 @@
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
},
+ "use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "dev": true
+ },
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/packages/pwa-kit-react-sdk/package.json b/packages/pwa-kit-react-sdk/package.json
index e5090c96ae..32710759e1 100644
--- a/packages/pwa-kit-react-sdk/package.json
+++ b/packages/pwa-kit-react-sdk/package.json
@@ -50,6 +50,7 @@
"minimatch": "3.0.4",
"mkdirp": "^1.0.4",
"prop-types": "^15.6.0",
+ "react-ssr-prepass": "^1.5.0",
"pwa-kit-runtime": "^2.3.0-dev",
"react-uid": "^2.2.0",
"serialize-javascript": "^6.0.0",
@@ -59,6 +60,7 @@
},
"devDependencies": {
"@loadable/component": "^5.15.0",
+ "@tanstack/react-query": "^4.0.10",
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.6",
"enzyme": "^3.8.0",
"enzyme-adapter-react-16": "1.15.2",
@@ -76,9 +78,15 @@
},
"peerDependencies": {
"@loadable/component": "^5.15.0",
+ "@tanstack/react-query": "^4.0.10",
"react": ">=16.14 || <18",
"react-dom": ">=16.14 || <18",
"react-helmet": "6",
"react-router-dom": "^5.1.2"
+ },
+ "peerDependenciesMeta": {
+ "@tanstack/react-query": {
+ "optional": true
+ }
}
}
diff --git a/packages/pwa-kit-react-sdk/setup-jest.js b/packages/pwa-kit-react-sdk/setup-jest.js
index d23b745814..11d151bc12 100644
--- a/packages/pwa-kit-react-sdk/setup-jest.js
+++ b/packages/pwa-kit-react-sdk/setup-jest.js
@@ -31,6 +31,7 @@ jest.mock('pwa-kit-runtime/utils/ssr-config', () => {
'**/*.json'
],
ssrParameters: {
+ ssrPrepassEnabled: true,
ssrFunctionNodeVersion: '14.x',
proxyConfigs: [
{
diff --git a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx
index 3c4f9a1a1f..947c86b22f 100644
--- a/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx
+++ b/packages/pwa-kit-react-sdk/src/ssr/browser/main.jsx
@@ -8,12 +8,13 @@
import React from 'react'
import ReactDOM from 'react-dom'
import {BrowserRouter as Router} from 'react-router-dom'
-import DeviceContext from '../universal/device-context'
+import {DeviceContext, ExpressContext} from '../universal/contexts'
import App from '../universal/components/_app'
import AppConfig from '../universal/components/_app-config'
import Switch from '../universal/components/switch'
-import {getRoutes, routeComponent} from '../universal/components/route-component'
import {loadableReady} from '@loadable/component'
+import {withReactQuery} from '../universal/components'
+import {getRoutes} from '../universal/utils'
/* istanbul ignore next */
export const registerServiceWorker = (url) => {
@@ -54,7 +55,6 @@ export const start = () => {
// AppConfig.restore *must* come before getRoutes()
AppConfig.restore(locals, window.__PRELOADED_STATE__.__STATE_MANAGEMENT_LIBRARY)
- const routes = getRoutes(locals)
// We need to tell the routeComponent HOC when the app is hydrating in order to
// prevent pages from re-fetching data on the first client-side render. The
@@ -66,25 +66,37 @@ export const start = () => {
// been warned.
window.__HYDRATING__ = true
- const WrappedApp = routeComponent(App, false, locals)
+ // const WrappedApp = routeComponent(App, false, locals)
+ const WrappedApp = withReactQuery(App)
+
+ // NOTE: It's kinda weird how frozn state is loaded in the JSX here. Would be nice if it was
+ // "added" via or in, the hoc.
+ let routes = getRoutes(locals)
+
+ if (WrappedApp.enhanceRoutes) {
+ routes = WrappedApp.enhanceRoutes(routes)
+ }
+
const error = window.__ERROR__
return Promise.resolve()
.then(() => new Promise((resolve) => loadableReady(resolve)))
.then(() => {
ReactDOM.hydrate(
-
-
-
-
-
-
- ,
+
+
+
+
+
+
+
+
+ ,
rootEl,
() => {
window.__HYDRATING__ = false
diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js
index 5f3c86f4d5..a863d65dbe 100644
--- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js
+++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.js
@@ -17,8 +17,9 @@ import {ChunkExtractor} from '@loadable/server'
import {StaticRouter as Router, matchPath} from 'react-router-dom'
import serialize from 'serialize-javascript'
-import {getAssetUrl} from '../universal/utils'
+import {getAssetUrl, getRoutes} from '../universal/utils'
import DeviceContext from '../universal/device-context'
+import {ExpressContext} from '../universal/contexts'
import Document from '../universal/components/_document'
import App from '../universal/components/_app'
@@ -26,7 +27,6 @@ import Throw404 from '../universal/components/throw-404'
import AppConfig from '../universal/components/_app-config'
import Switch from '../universal/components/switch'
-import {getRoutes, routeComponent} from '../universal/components/route-component'
import * as errors from '../universal/errors'
import {detectDeviceType, isRemote} from 'pwa-kit-runtime/utils/ssr-server'
import {proxyConfigs} from 'pwa-kit-runtime/utils/ssr-shared'
@@ -34,6 +34,8 @@ import {getConfig} from 'pwa-kit-runtime/utils/ssr-config'
import sprite from 'svg-sprite-loader/runtime/sprite.build'
+import {withErrorHandling, withLoadableResolver, withReactQuery} from '../universal/components'
+
const CWD = process.cwd()
const BUNDLES_PATH = path.resolve(CWD, 'build/loadable-stats.json')
@@ -72,52 +74,6 @@ const logAndFormatError = (err) => {
}
}
-const initAppState = async ({App, component, match, route, req, res, location}) => {
- if (component === Throw404) {
- // Don't init if there was no match
- return {
- error: new errors.HTTPNotFound('Not found'),
- appState: {}
- }
- }
-
- const {params} = match
-
- const components = [App, route.component]
- const promises = components.map((c) =>
- c.getProps
- ? c.getProps({
- req,
- res,
- params,
- location
- })
- : Promise.resolve({})
- )
- let returnVal = {}
-
- try {
- const [appProps, pageProps] = await Promise.all(promises)
- const appState = {
- appProps,
- pageProps,
- __STATE_MANAGEMENT_LIBRARY: AppConfig.freeze(res.locals)
- }
-
- returnVal = {
- error: undefined,
- appState: appState
- }
- } catch (error) {
- returnVal = {
- error: error || new Error(),
- appState: {}
- }
- }
-
- return returnVal
-}
-
/**
* This is the main react-rendering function for SSR. It is an Express handler.
*
@@ -136,8 +92,27 @@ export const render = async (req, res, next) => {
// to inject arguments into the wrapped component's getProps methods.
AppConfig.restore(res.locals)
- const routes = getRoutes(res.locals)
- const WrappedApp = routeComponent(App, false, res.locals)
+ // NOTE: I think the recommened functionality by Oli is to not apply this enchancement if there is an
+ // existing api applied to the application? I need to get clarification on this.
+ // NOTE: We shouldn't have to wrap the App with withLoadableResolver since it's required to be in the
+ // bundle, we can probably clean up the logive for getProps somehow.
+ const WrappedApp = withReactQuery(App)
+ const deviceType = detectDeviceType(req)
+
+ WrappedApp.initStaticContext({
+ req,
+ res
+ })
+
+ // Get routes and wrap with resolver and error handlers
+ let routes = getRoutes(res.locals).map(({component, ...rest}) => ({
+ component: component ? withErrorHandling(withLoadableResolver(component)) : component,
+ ...rest
+ }))
+
+ if (WrappedApp.enhanceRoutes) {
+ routes = WrappedApp.enhanceRoutes(routes, true, res.locals)
+ }
const [pathname, search] = req.originalUrl.split('?')
const location = {
@@ -162,30 +137,74 @@ export const render = async (req, res, next) => {
const component = await route.component.getComponent()
// Step 3 - Init the app state
- const {appState, error: appStateError} = await initAppState({
- App: WrappedApp,
- component,
- match,
- route,
- req,
- res,
- location
- })
+ let appState = {}
+ let routerContext = {}
+ let error
+ let routeError
+ let appStateError
+ let html
+
+ // Step 3. Bail if there is a 404.
+ if (component === Throw404) {
+ routeError = new errors.HTTPNotFound('Not found')
+ }
+
+ if (!routeError) {
+ const AppJSX = getAppJSX(req, res, error, {
+ App: WrappedApp,
+ appState,
+ deviceType,
+ location,
+ routerContext,
+ routes
+ })
+
+ try {
+ // Get all the data fetching promises for the various API's attached to the App component.
+ // This is the react query, and getProps enhancements.
+ const allPromises = WrappedApp.getDataPromises({
+ App,
+ AppJSX,
+ location,
+ req,
+ res,
+ deviceType,
+ route,
+ routes,
+ match
+ })
+ const data = await Promise.all(allPromises)
+
+ appState = data.reduce(
+ (acc, appState = {}) => ({
+ ...acc,
+ ...appState
+ }),
+ {}
+ )
+ } catch (e) {
+ appStateError = logAndFormatError(e || new Error())
+ }
+ }
+
+ // Support the AppConfig freeze API.
+ appState.__STATE_MANAGEMENT_LIBRARY = AppConfig.freeze(res.locals)
// Step 4 - Render the App
- let renderResult
const args = {
App: WrappedApp,
appState,
- appStateError: appStateError && logAndFormatError(appStateError),
+ appStateError,
+ routeError,
routes,
req,
res,
location,
config
}
+
try {
- renderResult = renderApp(args)
+ ;({html, routerContext, error} = await renderApp(args))
} catch (e) {
// This is an unrecoverable error.
// (errors handled by the AppErrorBoundary are considered recoverable)
@@ -197,7 +216,6 @@ export const render = async (req, res, next) => {
// Step 5 - Determine what is going to happen, redirect, or send html with
// the correct status code.
- const {html, routerContext, error} = renderResult
const redirectUrl = routerContext.url
const status = (error && error.status) || res.statusCode
@@ -208,29 +226,42 @@ export const render = async (req, res, next) => {
}
}
-const renderAppHtml = (req, res, error, appData) => {
- const {App, appState, routes, routerContext, location, extractor, deviceType} = appData
-
- let appJSX = (
-
-
-
-
-
-
-
+const getAppJSX = (req, res, error, appData) => {
+ const {App, appState = {}, deviceType, location, routerContext, routes} = appData
+
+ return (
+
+
+
+
+
+
+
+
+
)
+}
- appJSX = extractor.collectChunks(appJSX)
+const renderAppHtml = (req, res, error, appData) => {
+ const {extractor} = appData
+ const appJSX = extractor.collectChunks(getAppJSX(req, res, error, appData))
return ReactDOMServer.renderToString(appJSX)
}
-const renderApp = (args) => {
- const {req, res, appStateError, App, appState, location, routes, config} = args
+const renderApp = async (args) => {
+ const {req, res, appStateError, routeError, App, appState, location, routes, config} = args
const deviceType = detectDeviceType(req)
const extractor = new ChunkExtractor({statsFile: BUNDLES_PATH, publicPath: getAssetUrl()})
const routerContext = {}
- const appData = {App, appState, location, routes, routerContext, deviceType, extractor}
+ const appData = {
+ App,
+ appState,
+ location,
+ routes,
+ routerContext,
+ deviceType,
+ extractor
+ }
const ssrOnly = 'mobify_server_only' in req.query || '__server_only' in req.query
const prettyPrint = 'mobify_pretty' in req.query || '__pretty_print' in req.query
@@ -266,7 +297,8 @@ const renderApp = (args) => {
const helmet = Helmet.renderStatic()
// Return the first error encountered during the rendering pipeline.
- const error = appStateError || renderError
+ const error = routeError || appStateError || renderError
+
// Remove the stacktrace when executing remotely as to not leak any important
// information to users about our system.
if (error && isRemote()) {
@@ -281,6 +313,7 @@ const renderApp = (args) => {
// object, client-side, by code in ssr/browser/main.jsx.
//
// Do *not* add to these without a very good reason - globals are a liability.
+
const windowGlobals = {
__CONFIG__: config,
__DEVICE_TYPE__: deviceType,
diff --git a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js
index 6400201294..44447e2cd9 100644
--- a/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js
+++ b/packages/pwa-kit-react-sdk/src/ssr/server/react-rendering.test.js
@@ -39,12 +39,23 @@ const mobile =
const tablet =
'Mozilla/5.0 (iPad; CPU OS 6_1 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10B141 Safari/8536.25'
+jest.mock('../universal/components/_app', () => {
+ const actual = jest.requireActual('../universal/components/_app')
+ const {withLegacyGetProps} = jest.requireActual('../universal/components')
+
+ return {
+ __esModule: true,
+ default: withLegacyGetProps(actual.default)
+ }
+})
+
jest.mock('../universal/routes', () => {
const React = require('react')
const PropTypes = require('prop-types')
const errors = require('../universal/errors')
const {Redirect} = require('react-router-dom')
const {Helmet} = require('react-helmet')
+ const {useQuery} = require('@tanstack/react-query')
// Test utility to exercise paths that work with @loadable/component.
const fakeLoadable = (Wrapped) => {
@@ -127,6 +138,7 @@ jest.mock('../universal/routes', () => {
class GetPropsRejectsWithEmptyString extends React.Component {
static getProps() {
+ console.log('GETPROPS REJECT')
return Promise.reject('')
}
@@ -228,6 +240,26 @@ jest.mock('../universal/routes', () => {
}
}
+ const UseQueryResolvesObject = () => {
+ const {data, isLoading} = useQuery(['use-query-resolves-object'], async () => ({
+ prop: 'prop-value'
+ }))
+ return
{isLoading ? 'loading' : data.prop}
+ }
+
+ const DisabledUseQueryIsntResolved = () => {
+ const {data, isLoading} = useQuery(
+ ['use-query-resolves-object'],
+ async () => ({
+ prop: 'prop-value'
+ }),
+ {
+ enabled: false
+ }
+ )
+ return {isLoading ? 'loading' : data.prop}
+ }
+
GetPropsReturnsObject.propTypes = {
prop: PropTypes.node
}
@@ -282,6 +314,14 @@ jest.mock('../universal/routes', () => {
{
path: '/xss/',
component: XSSPage
+ },
+ {
+ path: '/use-query-resolves-object/',
+ component: UseQueryResolvesObject
+ },
+ {
+ path: '/disabled-use-query-isnt-resolved/',
+ component: DisabledUseQueryIsntResolved
}
]
}
@@ -618,6 +658,24 @@ describe('The Node SSR Environment', () => {
shouldIncludeErrorStack ? 'Error: ' : 'Internal Server Error'
)
}
+ },
+ {
+ description: `Works if the user resolves an Object with useQuery`,
+ req: {url: '/use-query-resolves-object/'},
+ assertions: (res) => {
+ expect(res.statusCode).toBe(200)
+ const html = res.text
+ expect(html).toEqual(expect.stringContaining('prop-value
'))
+ }
+ },
+ {
+ description: `Disabled useQuery queries aren't run on the server`,
+ req: {url: '/disabled-use-query-isnt-resolved/'},
+ assertions: (res) => {
+ expect(res.statusCode).toBe(200)
+ const html = res.text
+ expect(html).toEqual(expect.stringContaining('loading
'))
+ }
}
]
diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/index.js b/packages/pwa-kit-react-sdk/src/ssr/universal/components/index.js
new file mode 100644
index 0000000000..af48902056
--- /dev/null
+++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/index.js
@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2022, Salesforce, Inc.
+ * All rights reserved.
+ * SPDX-License-Identifier: BSD-3-Clause
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
+ */
+import withReactQuery from './with-react-query'
+import withLegacyGetProps from './with-legacy-get-props'
+import withLoadableResolver from './with-loadable-resolver'
+import withErrorHandling from './with-error-handling'
+
+export {withErrorHandling, withLegacyGetProps, withLoadableResolver, withReactQuery}
diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/switch/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/switch/index.jsx
index eee989daf3..96a5918085 100644
--- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/switch/index.jsx
+++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/switch/index.jsx
@@ -25,14 +25,14 @@ const Switch = (props) => {
{!error && (
-
+
{routes.map((route, i) => {
const {component: Component, ...routeProps} = route
return (
-
+
)
diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-error-handling/index.jsx b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-error-handling/index.jsx
new file mode 100644
index 0000000000..b93d2e6206
--- /dev/null
+++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-error-handling/index.jsx
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2021, salesforce.com, inc.
+ * All rights reserved.
+ * SPDX-License-Identifier: BSD-3-Clause
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
+ */
+import React from 'react'
+import hoistNonReactStatic from 'hoist-non-react-statics'
+import {AppErrorContext} from '../../components/app-error-boundary'
+
+/**
+ * @private
+ */
+export const withErrorHandling = (Wrapped) => {
+ /* istanbul ignore next */
+ const wrappedComponentName = Wrapped.displayName || Wrapped.name
+
+ const WithErrorHandling = (props) => (
+
+ {(ctx) => }
+
+ )
+
+ // Expose statics from the wrapped component on the HOC
+ hoistNonReactStatic(WithErrorHandling, Wrapped)
+
+ WithErrorHandling.displayName = `withErrorHandling(${wrappedComponentName})`
+
+ return WithErrorHandling
+}
+
+export default withErrorHandling
diff --git a/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.jsx
similarity index 69%
rename from packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js
rename to packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.jsx
index 96f041a9db..a732da2ff1 100644
--- a/packages/pwa-kit-react-sdk/src/ssr/universal/components/route-component/index.js
+++ b/packages/pwa-kit-react-sdk/src/ssr/universal/components/with-legacy-get-props/index.jsx
@@ -8,11 +8,12 @@ import PropTypes from 'prop-types'
import React from 'react'
import {withRouter} from 'react-router-dom'
import hoistNonReactStatic from 'hoist-non-react-statics'
-import {AppErrorContext} from '../../components/app-error-boundary'
-import Throw404 from '../../components/throw-404'
import AppConfig from '../../components/_app-config'
-import routes from '../../routes'
import {pages as pageEvents} from '../../events'
+import {compose} from '../../utils'
+
+// const USAGE_WARNING = `This HOC can only be used on your PWA-Kit App component. We cannot guarantee its functionality if used elsewhere.`
+const STATE_KEY = '__LEGACY_GET_PROPS__'
const noop = () => undefined
@@ -28,33 +29,17 @@ const now = () => {
: Date.now()
}
-/**
- * @private
- */
-const withErrorHandling = (Wrapped) => {
- /* istanbul ignore next */
- const wrappedComponentName = Wrapped.displayName || Wrapped.name
-
- const WithErrorHandling = (props) => (
-
- {(ctx) => }
-
- )
-
- // Expose statics from the wrapped component on the HOC
- hoistNonReactStatic(WithErrorHandling, Wrapped)
-
- WithErrorHandling.displayName = `WithErrorHandling(${wrappedComponentName})`
- return WithErrorHandling
-}
-
/**
* The `routeComponent` HOC is automatically used on every component in a project's
* route-config. It provides an interface, via static methods on React components,
* that can be used to fetch data on the server and on the client, seamlessly.
*/
-export const routeComponent = (Wrapped, isPage, locals) => {
- const extraArgs = AppConfig.extraGetPropsArgs(locals)
+// @private
+ export const routeComponent = (Wrapped, isPage, locals, extraGetPropsArgsFn) => {
+ // NOTE: At the point in time when the API is being wrapped by `withLegacyGetProps` the __PRELOADED_STATE__ object doesn't
+ // exist. Meaning we can't access it. We need to find a solution to this.
+ let _preloadedProps
+ let extraArgs
/* istanbul ignore next */
const wrappedComponentName = Wrapped.displayName || Wrapped.name
@@ -62,11 +47,21 @@ export const routeComponent = (Wrapped, isPage, locals) => {
class RouteComponent extends React.Component {
constructor(props, context) {
super(props, context)
+
+ // Initialize the preloaded state from the internal "global" variable when rendering on server,
+ // and from the frozen state when rendered on the client.
+ // Ideally this logic would exist inside the `withLegacyGetProps` HOC, but because we "enhance"
+ // the routes before we get data, there is no way to pass data down to them.
+ const preloadedState =
+ typeof window !== 'undefined' ?
+ window?.__PRELOADED_STATE__?.[STATE_KEY]?.[`${isPage ? 'page' : 'app'}Props`]
+ : _preloadedProps
+
this.state = {
childProps: {
// When serverside or hydrating, forward props from the frozen app state
// to the wrapped component.
- ...(isServerSide || isHydrating() ? this.props.preloadedProps : undefined),
+ ...(isServerSide || isHydrating() ? this.props.preloadedProps || preloadedState : undefined),
isLoading: false
}
}
@@ -102,12 +97,18 @@ export const routeComponent = (Wrapped, isPage, locals) => {
* @return {Promise}
*/
static async shouldGetProps(args) {
+ let component
const defaultImpl = () => {
const {previousLocation, location} = args
return !previousLocation || previousLocation.pathname !== location.pathname
}
- const component = await RouteComponent.getComponent()
+ if (Wrapped.getComponent) {
+ component = await Wrapped.getComponent()
+ } else {
+ component = Wrapped
+ }
+
return component.shouldGetProps ? component.shouldGetProps(args) : defaultImpl()
}
@@ -153,37 +154,24 @@ export const routeComponent = (Wrapped, isPage, locals) => {
*/
// eslint-disable-next-line
static getProps(args) {
- RouteComponent._latestPropsPromise = RouteComponent.getComponent().then((component) =>
- component.getProps ? component.getProps({...args, ...extraArgs}) : Promise.resolve()
- )
- return RouteComponent._latestPropsPromise
- }
+ RouteComponent._latestPropsPromise = Promise.resolve()
+ .then(() => {
+ return Wrapped.getComponent ? Wrapped.getComponent() : Wrapped
+ })
+ .then((component) => {
+ if (!extraArgs && extraGetPropsArgsFn) {
+ extraArgs = extraGetPropsArgsFn()
+ }
+
+ return component.getProps ? component.getProps({...args, ...extraArgs}) : Promise.resolve()
+ })
+ .then((props) => {
+ _preloadedProps = props
- /**
- * Get the underlying component this HoC wraps. This handles loading of
- * `@loadable/component` components.
- *
- * @return {Promise}
- */
- static async getComponent() {
- return Wrapped.load
- ? Wrapped.load().then((module) => module.default)
- : Promise.resolve(Wrapped)
- }
+ return props
+ })
- /**
- * Route-components implement `getTemplateName()` to return a readable
- * name for the component that is used internally for analytics-tracking –
- * eg. performance/page-view events.
- *
- * If not implemented defaults to the `displayName` of the React component.
- *
- * @return {Promise}
- */
- static async getTemplateName() {
- return RouteComponent.getComponent().then((c) =>
- c.getTemplateName ? c.getTemplateName() : Promise.resolve(wrappedComponentName)
- )
+ return RouteComponent._latestPropsPromise
}
/**
@@ -261,8 +249,7 @@ export const routeComponent = (Wrapped, isPage, locals) => {
//
// Since the time is overwhelmingly spent fetching data on soft-navs,
// we think this is a good approximation in both cases.
-
- const templateName = await RouteComponent.getTemplateName()
+ const templateName = await Wrapped.getTemplateName()
const start = now()
@@ -361,6 +348,7 @@ export const routeComponent = (Wrapped, isPage, locals) => {
}
}
+
RouteComponent.displayName = `routeComponent(${wrappedComponentName})`
RouteComponent.defaultProps = {
@@ -383,28 +371,120 @@ export const routeComponent = (Wrapped, isPage, locals) => {
getProps: true,
getTemplateName: true
}
+
hoistNonReactStatic(RouteComponent, Wrapped, excludes)
- return withErrorHandling(withRouter(RouteComponent))
+ return RouteComponent
}
/**
- * Wrap all the components found in the application's route config with the
- * route-component HOC so that they all support `getProps` methods server-side
- * and client-side in the same way.
- *
- * @private
+ * The `routeComponent` HOC is automatically used on every component in a project's
+ * route-config. It provides an interface, via static methods on React components,
+ * that can be used to fetch data on the server and on the client, seamlessly.
*/
-export const getRoutes = (locals) => {
- let _routes = routes
- if (typeof routes === 'function') {
- _routes = routes()
+export const withLegacyGetProps = (Wrapped, options) => {
+ let _staticContext = {}
+
+ // Wrapped =
+ // compose(
+ // routeComponent,
+ // withRouter
+ // )(Wrapped, false, {}, () => {
+ // return options.extraGetPropsArgs ? options.extraGetPropsArgs(_staticContext) : {}
+ // }) // Sketchy
+
+ Wrapped = routeComponent(withRouter(Wrapped), false, {}, () => {
+ return options.extraGetPropsArgs ? options.extraGetPropsArgs(_staticContext) : {}
+ }) // Sketchy
+
+ /* istanbul ignore next */
+ const wrappedComponentName = Wrapped.displayName || Wrapped.name
+
+ const WrappedComponent = ({...passThroughProps}) => {
+ return
}
- const allRoutes = [..._routes, {path: '*', component: Throw404}]
- return allRoutes.map(({component, ...rest}) => {
- return {
- component: component ? routeComponent(component, true, locals) : component,
+
+ /**
+ * Enhance route components with the `withLegacyGetProps` higher order component.
+ *
+ * @param {Object[]} routes
+ * @param {Boolean} isPage
+ * @param {Object} locals
+ *
+ * @return {Object[]}
+ */
+ WrappedComponent.enhanceRoutes = (routes = [], isPage, locals) => {
+ routes = Wrapped.enhanceRoutes ? Wrapped.enhanceRoutes(routes) : routes
+
+ return routes.map(({component, ...rest}) => ({
+ component: component ? routeComponent(withRouter(component), true, locals) : component,
...rest
+ }))
+ }
+
+ /**
+ * Returns the `getProps` promises for the matched App and Page components, this includes
+ * any promises returned by the child components `getDataPromises` implementation if one
+ * exists.
+ *
+ * @param {Object} renderContext
+ * @return {Promise