Skip to content

Commit

Permalink
[JSS 14][React sample] Race condition, under load React app may rende…
Browse files Browse the repository at this point in the history
…r HTML from a different route (#455)

* [JSS 14][React sample] Race condition, under load React app may render HTML from a different route

* Fix for NotFound page
  • Loading branch information
sc-illiakovalenko authored Sep 29, 2020
1 parent e8a8368 commit f2792e4
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 81 deletions.
9 changes: 6 additions & 3 deletions samples/react/server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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
Expand All @@ -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(
<AppRoot path={path} Router={StaticRouter} graphQLClient={graphQLClient} />
<AppRoot
path={path}
Router={StaticRouter}
graphQLClient={graphQLClient}
ssrState={state}
/>
)
)
.then((renderedAppHtml) =>
Expand Down
72 changes: 54 additions & 18 deletions samples/react/src/AppRoot.js
Original file line number Diff line number Diff line change
@@ -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).
Expand All @@ -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) => <RouteHandler route={props} />;
return (
<ApolloProvider client={graphQLClient}>
<SitecoreContext componentFactory={componentFactory} contextFactory={SitecoreContextFactory}>
<Router location={path} context={{}}>
<Switch>
{routePatterns.map((routePattern) => (
<Route key={routePattern} path={routePattern} render={routeRenderFunction} />
))}
</Switch>
</Router>
</SitecoreContext>
</ApolloProvider>
);
};
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) =>
<RouteHandler
route={props}
ssrRenderComplete={this.state.ssrRenderComplete}
setSsrRenderComplete={this.setSsrRenderComplete}
/>;
return (
<ApolloProvider client={graphQLClient}>
<SitecoreContext componentFactory={componentFactory} contextFactory={this.state.contextFactory}>
<Router location={path} context={{}}>
<Switch>
{routePatterns.map((routePattern) => (
<Route key={routePattern} path={routePattern} render={routeRenderFunction} />
))}
</Switch>
</Router>
</SitecoreContext>
</ApolloProvider>
);
}
}

export default AppRoot;
88 changes: 44 additions & 44 deletions samples/react/src/RouteHandler.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand All @@ -72,18 +50,44 @@ 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;
}

componentWillUnmount() {
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
*/
Expand All @@ -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)
)
}
});
}
Expand Down Expand Up @@ -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 (
<div>
<Helmet>
Expand All @@ -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.
Expand Down
5 changes: 1 addition & 4 deletions samples/react/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -71,6 +67,7 @@ i18ninit(initLanguage).then(() => {
path={window.location.pathname}
Router={BrowserRouter}
graphQLClient={graphQLClient}
ssrState={__JSS_STATE__}
/>,
rootElement
);
Expand Down
12 changes: 0 additions & 12 deletions samples/react/src/lib/SitecoreContextFactory.js

This file was deleted.

0 comments on commit f2792e4

Please sign in to comment.