Skip to content
Permalink
Browse files

[JSS 11.0][React sample] Race condition, under load React app may ren…

…der HTML from a different route (#453)
  • Loading branch information
sc-illiakovalenko committed Sep 28, 2020
1 parent b297630 commit 960386f329e294812a672ac439e4f75cb95cc76f
@@ -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 indexTemplate from '../build/index.html';

/** Asserts that a string replace actually replaced something */
@@ -46,8 +45,6 @@ export function renderView(callback, path, data, viewBag) {
try {
const state = parseServerData(data, viewBag);

setServerSideRenderingState(state);

/*
GraphQL Data
The Apollo Client needs to be initialized to make GraphQL available to the JSS app.
@@ -65,7 +62,11 @@ 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) => {
@@ -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,53 @@ 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 {
state = {
ssrRenderComplete: false,
contextFactory: new SitecoreContextFactory()
}

setSsrRenderComplete = ssrRenderComplete => {
this.setState({
ssrRenderComplete
})
}

render() {
const { path, Router, graphQLClient, ssrState } = this.props;

if (ssrState && ssrState.sitecore && ssrState.sitecore.route) {
// set the initial sitecore context data if we got SSR initial state
this.state.contextFactory.setSitecoreContext({
route: ssrState.sitecore.route,
itemId: ssrState.sitecore.route.itemId,
...ssrState.sitecore.context,
});
}

const routeRenderFunction = (props) =>
<RouteHandler
route={props}
ssrState={this.state.ssrRenderComplete ? null : ssrState}
contextFactory={this.state.contextFactory}
setSsrRenderComplete={this.setSsrRenderComplete}
ssrRenderComplete={this.state.ssrRenderComplete}
/>;

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;
@@ -2,7 +2,6 @@ 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 { dataFetcher } from './dataFetcher';
import config from './temp/config';
import Layout from './Layout';
@@ -14,27 +13,18 @@ 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 {
constructor(props) {
super(props);

const ssrInitialState = props.ssrState;

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,
});
}

// route data from react-router - if route was resolved, it's not a 404
if (props.route !== null) {
this.state.notFound = false;
@@ -53,17 +43,6 @@ export default class RouteHandler extends React.Component {
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;
}

this.componentIsMounted = false;
this.languageIsChanging = false;

@@ -72,6 +51,17 @@ export default class RouteHandler extends React.Component {
}

componentDidMount() {
// 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);
}

// if no existing routeData is present (from SSR), get Layout Service fetching the route data
if (!this.state.routeData) {
this.updateRouteData();
@@ -99,7 +89,7 @@ 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.contextFactory.setSitecoreContext({
route: routeData.sitecore.route,
itemId: routeData.sitecore.route.itemId,
...routeData.sitecore.context,
@@ -183,15 +173,6 @@ 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;
}

/**
* Gets route data from Sitecore. This data is used to construct the component layout for a JSS route.
* @param {string} route Route path to get data for (e.g. /about)
@@ -2,7 +2,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';
@@ -30,9 +29,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;
}
@@ -63,6 +59,7 @@ i18ninit().then(() => {
path={window.location.pathname}
Router={BrowserRouter}
graphQLClient={graphQLClient}
ssrState={__JSS_STATE__}
/>,
rootElement
);

This file was deleted.

0 comments on commit 960386f

Please sign in to comment.
You can’t perform that action at this time.