Skip to content

Commit

Permalink
[JSS 12][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 (#454)
  • Loading branch information
sc-illiakovalenko committed Sep 28, 2020
1 parent 975651a commit a933c3c
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 80 deletions.
9 changes: 6 additions & 3 deletions samples/react/server/server.js
Expand Up @@ -7,7 +7,6 @@ import GraphQLClientFactory from '../src/lib/GraphQLClientFactory';
import config from '../src/temp/config'; import config from '../src/temp/config';
import i18ninit from '../src/i18n'; import i18ninit from '../src/i18n';
import AppRoot, { routePatterns } from '../src/AppRoot'; import AppRoot, { routePatterns } from '../src/AppRoot';
import { setServerSideRenderingState } from '../src/RouteHandler';
import { getHtmlTemplate } from './htmlTemplateFactory'; import { getHtmlTemplate } from './htmlTemplateFactory';


/** Asserts that a string replace actually replaced something */ /** 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) { export function renderView(callback, path, data, viewBag) {
try { try {
const state = parseServerData(data, viewBag); const state = parseServerData(data, viewBag);
setServerSideRenderingState(state);


/* /*
GraphQL Data 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. // is included in the SSR'ed markup instead of whatever the 'loading' state is.
// Not using GraphQL? Use ReactDOMServer.renderToString() instead. // Not using GraphQL? Use ReactDOMServer.renderToString() instead.
renderToStringWithData( renderToStringWithData(
<AppRoot path={path} Router={StaticRouter} graphQLClient={graphQLClient} /> <AppRoot
path={path}
Router={StaticRouter}
graphQLClient={graphQLClient}
ssrState={state}
/>
) )
) )
.then((renderedAppHtml) => .then((renderedAppHtml) =>
Expand Down
67 changes: 49 additions & 18 deletions samples/react/src/AppRoot.js
@@ -1,9 +1,8 @@
import React from 'react'; 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 { Route, Switch } from 'react-router-dom';
import { ApolloProvider } from 'react-apollo'; import { ApolloProvider } from 'react-apollo';
import componentFactory from './temp/componentFactory'; import componentFactory from './temp/componentFactory';
import SitecoreContextFactory from './lib/SitecoreContextFactory';
import RouteHandler from './RouteHandler'; import RouteHandler from './RouteHandler';


// This is the main JSX entry point of the app invoked by the renderer (server or client rendering). // This is the main JSX entry point of the app invoked by the renderer (server or client rendering).
Expand All @@ -22,21 +21,53 @@ export const routePatterns = [
// Not needed if not using connected GraphQL. // Not needed if not using connected GraphQL.
// SitecoreContext: provides component resolution and context services via withSitecoreContext // 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. // 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} />; class AppRoot extends React.Component {
return ( state = {
<ApolloProvider client={graphQLClient}> ssrRenderComplete: false,
<SitecoreContext componentFactory={componentFactory} contextFactory={SitecoreContextFactory}> contextFactory: new SitecoreContextFactory()
<Router location={path} context={{}}> }
<Switch>
{routePatterns.map((routePattern) => ( setSsrRenderComplete = ssrRenderComplete => {
<Route key={routePattern} path={routePattern} render={routeRenderFunction} /> this.setState({
))} ssrRenderComplete
</Switch> })
</Router> }
</SitecoreContext>
</ApolloProvider> 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; export default AppRoot;
47 changes: 14 additions & 33 deletions samples/react/src/RouteHandler.js
Expand Up @@ -2,7 +2,6 @@ import React from 'react';
import i18n from 'i18next'; import i18n from 'i18next';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { isExperienceEditorActive, dataApi } from '@sitecore-jss/sitecore-jss-react'; import { isExperienceEditorActive, dataApi } from '@sitecore-jss/sitecore-jss-react';
import SitecoreContextFactory from './lib/SitecoreContextFactory';
import { dataFetcher } from './dataFetcher'; import { dataFetcher } from './dataFetcher';
import config from './temp/config'; import config from './temp/config';
import Layout from './Layout'; import Layout from './Layout';
Expand All @@ -14,27 +13,18 @@ import NotFound from './NotFound';
// So react-router delegates all route rendering to this handler, which attempts to get the right // 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. // route data from Sitecore - and if none exists, renders the not found component.


let ssrInitialState = null;

export default class RouteHandler extends React.Component { export default class RouteHandler extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);


const ssrInitialState = props.ssrState;

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

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


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


componentDidMount() { 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 no existing routeData is present (from SSR), get Layout Service fetching the route data
if (!this.state.routeData) { if (!this.state.routeData) {
this.updateRouteData(); this.updateRouteData();
Expand Down Expand Up @@ -99,7 +89,7 @@ export default class RouteHandler extends React.Component {
getRouteData(sitecoreRoutePath, language).then((routeData) => { getRouteData(sitecoreRoutePath, language).then((routeData) => {
if (routeData !== null && routeData.sitecore && routeData.sitecore.route) { if (routeData !== null && routeData.sitecore && routeData.sitecore.route) {
// set the sitecore context data and push the new route // set the sitecore context data and push the new route
SitecoreContextFactory.setSitecoreContext({ this.props.contextFactory.setSitecoreContext({
route: routeData.sitecore.route, route: routeData.sitecore.route,
itemId: routeData.sitecore.route.itemId, itemId: routeData.sitecore.route.itemId,
...routeData.sitecore.context, ...routeData.sitecore.context,
Expand Down Expand Up @@ -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. * 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) * @param {string} route Route path to get data for (e.g. /about)
Expand Down
15 changes: 5 additions & 10 deletions samples/react/src/components/ContentBlock/index.js
@@ -1,22 +1,17 @@
import React from 'react'; import React from 'react';
import { RichText, withSitecoreContext } from '@sitecore-jss/sitecore-jss-react'; import { Text, RichText } from '@sitecore-jss/sitecore-jss-react';


/** /**
* A simple Content Block component, with a heading and rich text block. * A simple Content Block component, with a heading and rich text block.
* This is the most basic building block of a content site, and the most basic * This is the most basic building block of a content site, and the most basic
* JSS component that's useful. * JSS component that's useful.
*/ */
const ContentBlock = ({ fields, sitecoreContext }) => ( const ContentBlock = ({ fields }) => (
<React.Fragment> <React.Fragment>
<h2 className="display-5">WHAT is the airspeed velocity of an unladen swallow?</h2> <Text tag="h2" className="display-4" field={fields.heading} />
<h2 className="display-5">African or European?</h2>
{ sitecoreContext.pageEditing ? <marquee>I am an element that will only display in Experience Editor</marquee> : null }



<RichText className="contentDescription" field={fields.content} />
<div style={{marginTop: 50}}>
<RichText className="contentDescription" field={fields.content} />
</div>
</React.Fragment> </React.Fragment>
); );


export default withSitecoreContext()(ContentBlock); export default ContentBlock;
5 changes: 1 addition & 4 deletions samples/react/src/index.js
Expand Up @@ -2,7 +2,6 @@ import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import AppRoot from './AppRoot'; import AppRoot from './AppRoot';
import { setServerSideRenderingState } from './RouteHandler';
import GraphQLClientFactory from './lib/GraphQLClientFactory'; import GraphQLClientFactory from './lib/GraphQLClientFactory';
import config from './temp/config'; import config from './temp/config';
import i18ninit from './i18n'; import i18ninit from './i18n';
Expand Down Expand Up @@ -30,9 +29,6 @@ if (ssrRawJson) {
__JSS_STATE__ = JSON.parse(ssrRawJson.innerHTML); __JSS_STATE__ = JSON.parse(ssrRawJson.innerHTML);
} }
if (__JSS_STATE__) { 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` // when React initializes from a SSR-based initial state, you need to render with `hydrate` instead of `render`
renderFunction = ReactDOM.hydrate; renderFunction = ReactDOM.hydrate;
} }
Expand Down Expand Up @@ -63,6 +59,7 @@ i18ninit().then(() => {
path={window.location.pathname} path={window.location.pathname}
Router={BrowserRouter} Router={BrowserRouter}
graphQLClient={graphQLClient} graphQLClient={graphQLClient}
ssrState={__JSS_STATE__}
/>, />,
rootElement rootElement
); );
Expand Down
12 changes: 0 additions & 12 deletions samples/react/src/lib/SitecoreContextFactory.js

This file was deleted.

0 comments on commit a933c3c

Please sign in to comment.