diff --git a/samples/react/server/server.js b/samples/react/server/server.js index 4d4d4e6918..6c0779315b 100644 --- a/samples/react/server/server.js +++ b/samples/react/server/server.js @@ -7,7 +7,6 @@ import GraphQLClientFactory from '../src/lib/GraphQLClientFactory'; import config from '../src/temp/config'; import i18ninit from '../src/i18n'; import AppRoot, { routePatterns } from '../src/AppRoot'; -import { setServerSideRenderingState } from '../src/RouteHandler'; import { getHtmlTemplate } from './htmlTemplateFactory'; /** Asserts that a string replace actually replaced something */ @@ -45,7 +44,6 @@ export const appName = config.jssAppName; export function renderView(callback, path, data, viewBag) { try { const state = parseServerData(data, viewBag); - setServerSideRenderingState(state); /* GraphQL Data @@ -64,7 +62,12 @@ export function renderView(callback, path, data, viewBag) { // is included in the SSR'ed markup instead of whatever the 'loading' state is. // Not using GraphQL? Use ReactDOMServer.renderToString() instead. renderToStringWithData( - + ) ) .then((renderedAppHtml) => diff --git a/samples/react/src/AppRoot.js b/samples/react/src/AppRoot.js index 90f14a3839..a9e0e614db 100644 --- a/samples/react/src/AppRoot.js +++ b/samples/react/src/AppRoot.js @@ -1,9 +1,8 @@ import React from 'react'; -import { SitecoreContext } from '@sitecore-jss/sitecore-jss-react'; +import { SitecoreContext, SitecoreContextFactory } from '@sitecore-jss/sitecore-jss-react'; import { Route, Switch } from 'react-router-dom'; import { ApolloProvider } from 'react-apollo'; import componentFactory from './temp/componentFactory'; -import SitecoreContextFactory from './lib/SitecoreContextFactory'; import RouteHandler from './RouteHandler'; // This is the main JSX entry point of the app invoked by the renderer (server or client rendering). @@ -22,21 +21,58 @@ export const routePatterns = [ // Not needed if not using connected GraphQL. // SitecoreContext: provides component resolution and context services via withSitecoreContext // Router: provides a basic routing setup that will resolve Sitecore item routes and allow for language URL prefixes. -const AppRoot = ({ path, Router, graphQLClient }) => { - const routeRenderFunction = (props) => ; - return ( - - - - - {routePatterns.map((routePattern) => ( - - ))} - - - - - ); -}; +class AppRoot extends React.Component { + constructor(props) { + super(props); + + this.state = { + ssrRenderComplete: false, + contextFactory: new SitecoreContextFactory() + } + + if (props.ssrState && props.ssrState.sitecore && props.ssrState.sitecore.route) { + // set the initial sitecore context data if we got SSR initial state + this.state.contextFactory.setSitecoreContext({ + route: props.ssrState.sitecore.route, + itemId: props.ssrState.sitecore.route.itemId, + ...props.ssrState.sitecore.context, + }); + } else if (props.ssrState) { + this.state.contextFactory.setSitecoreContext(props.ssrState.sitecore.context) + } else { + this.state.contextFactory.setSitecoreContext(null); + } + } + + setSsrRenderComplete = ssrRenderComplete => ( + this.setState({ + ssrRenderComplete + }) + ) + + render() { + const { path, Router, graphQLClient } = this.props; + + const routeRenderFunction = (props) => + ; + return ( + + + + + {routePatterns.map((routePattern) => ( + + ))} + + + + + ); + } +} export default AppRoot; diff --git a/samples/react/src/RouteHandler.js b/samples/react/src/RouteHandler.js index 9e5220affa..b507d0adf5 100644 --- a/samples/react/src/RouteHandler.js +++ b/samples/react/src/RouteHandler.js @@ -1,8 +1,7 @@ import React from 'react'; import i18n from 'i18next'; import Helmet from 'react-helmet'; -import { isExperienceEditorActive, dataApi } from '@sitecore-jss/sitecore-jss-react'; -import SitecoreContextFactory from './lib/SitecoreContextFactory'; +import { isExperienceEditorActive, dataApi, withSitecoreContext } from '@sitecore-jss/sitecore-jss-react'; import { dataFetcher } from './dataFetcher'; import config from './temp/config'; import Layout from './Layout'; @@ -14,54 +13,33 @@ import NotFound from './NotFound'; // So react-router delegates all route rendering to this handler, which attempts to get the right // route data from Sitecore - and if none exists, renders the not found component. -let ssrInitialState = null; - -export default class RouteHandler extends React.Component { +class RouteHandler extends React.Component { constructor(props) { super(props); this.state = { notFound: true, - routeData: ssrInitialState, // null when client-side rendering defaultLanguage: config.defaultLanguage, }; - if (ssrInitialState && ssrInitialState.sitecore && ssrInitialState.sitecore.route) { - // set the initial sitecore context data if we got SSR initial state - SitecoreContextFactory.setSitecoreContext({ - route: ssrInitialState.sitecore.route, - itemId: ssrInitialState.sitecore.route.itemId, - ...ssrInitialState.sitecore.context, - }); - } + const routeData = this.extractRouteData(); // route data from react-router - if route was resolved, it's not a 404 - if (props.route !== null) { + if (routeData !== null) { this.state.notFound = false; } // if we have an initial SSR state, and that state doesn't have a valid route data, // then this is a 404 route. - if (ssrInitialState && (!ssrInitialState.sitecore || !ssrInitialState.sitecore.route)) { + if (routeData && (!routeData.sitecore || !routeData.sitecore.route)) { this.state.notFound = true; } // if we have an SSR state, and that state has language data, set the current language // (this makes the language of content follow the Sitecore context language cookie) // note that a route-based language (i.e. /de-DE) will override this default; this is for home. - if (ssrInitialState && ssrInitialState.context && ssrInitialState.context.language) { - this.state.defaultLanguage = ssrInitialState.context.language; - } - - // once we initialize the route handler, we've "used up" the SSR data, - // if it existed, so we want to clear it now that it's in react state. - // future route changes that might destroy/remount this component should ignore any SSR data. - // EXCEPTION: Unless we are still SSR-ing. Because SSR can re-render the component twice - // (once to find GraphQL queries that need to run, the second time to refresh the view with - // GraphQL query results) - // We test for SSR by checking for Node-specific process.env variable. - if (typeof window !== 'undefined') { - ssrInitialState = null; + if (routeData && routeData.context && routeData.context.language) { + this.state.defaultLanguage = routeData.context.language; } this.componentIsMounted = false; @@ -72,11 +50,24 @@ export default class RouteHandler extends React.Component { } componentDidMount() { - // if no existing routeData is present (from SSR), get Layout Service fetching the route data - if (!this.state.routeData) { + const routeData = this.extractRouteData(); + + // if no existing routeData is present (from SSR), get Layout Service fetching the route data or SSR render is complete + if (!routeData || this.props.ssrRenderComplete) { this.updateRouteData(); } + // once we initialize the route handler, we've "used up" the SSR data, + // if it existed, so we want to clear it now that it's in react state. + // future route changes that might destroy/remount this component should ignore any SSR data. + // EXCEPTION: Unless we are still SSR-ing. Because SSR can re-render the component twice + // (once to find GraphQL queries that need to run, the second time to refresh the view with + // GraphQL query results) + // We test for SSR by checking for Node-specific process.env variable. + if (typeof window !== "undefined" && !this.props.ssrRenderComplete && this.props.setSsrRenderComplete) { + this.props.setSsrRenderComplete(true); + } + this.componentIsMounted = true; } @@ -84,6 +75,19 @@ export default class RouteHandler extends React.Component { this.componentIsMounted = false; } + extractRouteData = () => { + if (!this.props.sitecoreContext) return null; + + const { route, ...context } = this.props.sitecoreContext; + + return { + sitecore: { + route, + context + } + } + } + /** * Loads route data from Sitecore Layout Service into state.routeData */ @@ -99,14 +103,16 @@ export default class RouteHandler extends React.Component { getRouteData(sitecoreRoutePath, language).then((routeData) => { if (routeData !== null && routeData.sitecore && routeData.sitecore.route) { // set the sitecore context data and push the new route - SitecoreContextFactory.setSitecoreContext({ + this.props.updateSitecoreContext({ route: routeData.sitecore.route, itemId: routeData.sitecore.route.itemId, ...routeData.sitecore.context, }); - this.setState({ routeData, notFound: false }); + this.setState({ notFound: false }); } else { - this.setState({ routeData, notFound: true }); + this.setState({ notFound: true }, () => + this.props.updateSitecoreContext(routeData.sitecore.context) + ) } }); } @@ -156,12 +162,13 @@ export default class RouteHandler extends React.Component { } render() { - const { notFound, routeData } = this.state; + const { notFound } = this.state; + const routeData = this.extractRouteData(); // no route data for the current route in Sitecore - show not found component. // Note: this is client-side only 404 handling. Server-side 404 handling is the responsibility // of the server being used (i.e. node-headless-ssr-proxy and Sitecore intergrated rendering know how to send 404 status codes). - if (notFound) { + if (notFound && routeData) { return (
@@ -183,14 +190,7 @@ export default class RouteHandler extends React.Component { } } -/** - * Sets the initial state provided by server-side rendering. - * Setting this state will bypass initial route data fetch calls. - * @param {object} ssrState - */ -export function setServerSideRenderingState(ssrState) { - ssrInitialState = ssrState; -} +export default withSitecoreContext({ updatable: true })(RouteHandler) /** * Gets route data from Sitecore. This data is used to construct the component layout for a JSS route. diff --git a/samples/react/src/index.js b/samples/react/src/index.js index b9418e068f..2b439d25cd 100644 --- a/samples/react/src/index.js +++ b/samples/react/src/index.js @@ -5,7 +5,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; import AppRoot from './AppRoot'; -import { setServerSideRenderingState } from './RouteHandler'; import GraphQLClientFactory from './lib/GraphQLClientFactory'; import config from './temp/config'; import i18ninit from './i18n'; @@ -35,9 +34,6 @@ if (ssrRawJson) { __JSS_STATE__ = JSON.parse(ssrRawJson.innerHTML); } if (__JSS_STATE__) { - // push the initial SSR state into the route handler, where it will be used - setServerSideRenderingState(__JSS_STATE__); - // when React initializes from a SSR-based initial state, you need to render with `hydrate` instead of `render` renderFunction = ReactDOM.hydrate; @@ -71,6 +67,7 @@ i18ninit(initLanguage).then(() => { path={window.location.pathname} Router={BrowserRouter} graphQLClient={graphQLClient} + ssrState={__JSS_STATE__} />, rootElement ); diff --git a/samples/react/src/lib/SitecoreContextFactory.js b/samples/react/src/lib/SitecoreContextFactory.js deleted file mode 100644 index 0da5d113a7..0000000000 --- a/samples/react/src/lib/SitecoreContextFactory.js +++ /dev/null @@ -1,12 +0,0 @@ -import { SitecoreContextFactory } from '@sitecore-jss/sitecore-jss-react'; - -/* - The SitecoreContextFactory stores the current Sitecore context for the app. - For example, whether the page is currently being edited, or the current language. - Note that the export makes this essentially a singleton, so we can store state in it. - - The Sitecore context is generally updated on route change (/src/index.js). - The `withSitecoreContext()` higher order component from `sitecore-jss-react` - can be used to access the context from within a component. -*/ -export default new SitecoreContextFactory();