Home App Docs Blog Github

A/B test variations styling doesn't look right on first load when using `emotion` css styling with Gatsby

In @builder.io/react@1.1.46 and above we introduced SSR support for a/b tests variations by default, those variations are rendered under template html tags so they don’t have any effect on rendering performance, the issue with using emotion css lib is that it doesn’t hoist the resulting styles all the way to the body tag, which could cause a flashing of unstyled content if you’re assigned to one of the tests groups of the content. To fix this issue we need to post process the resulting html to pluck all those styles tags that are tucked under the template tag up to the body tag,

In Gatsby you’d do that by ejecting html.js

cp .cache/default-html.js src/html.js

then we’re going to need a library to make those html manipulation easy, recommend cheerio

npm install cheerio

Now let’s use it to get all those style tags and make them global:

/**
 *
 * Gatsby does a two-pass render for HTML. It loops through your pages first
 * rendering only the body and then takes the result body HTML string and
 * passes it as the `body` prop to this component to complete the render.
 */
import cheerio from 'cheerio';

const postProcess = body => {
  let globalStyles = '';

  if (body.includes('<template')) {
    const $ = cheerio.load(body);
    const templates = $('template');

    templates.toArray().forEach(element => {
      const str = $(element).html();
      const styles = cheerio.load(String(str))('style');
      globalStyles += styles
        .toArray()
        .map(el => $(el).html())
        .join(' ');
    });
  }
  return { body, globalStyles };
};

Now let’s use that function on the body string passed to the html component:

export default function Html(props) {
  const { body, globalStyles } = postProcess(props.body);
  return (
    <html {...props.htmlAttributes}>
      <head>
        <meta charSet="utf-8" />
        <meta httpEquiv="x-ua-compatible" content="ie=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
        {props.headComponents}
      </head>
      <body {...props.bodyAttributes}>
        {props.preBodyComponents}
        <style dangerouslySetInnerHTML={{ __html: globalStyles }}></style>
        <div key="body" id="___gatsby" dangerouslySetInnerHTML={{ __html: body }} />
        {props.postBodyComponents}
      </body>
    </html>
  );
};

in the component above, we extracted the globalStyles string then added it under body tag:

  <style dangerouslySetInnerHTML={{ __html: globalStyles }}></style>

You can also play around with this starter which uses emotion CSS, supports SSR on a/b tests variations, and fixes the issues with emotion lib using the technique above.