Home App Docs Blog Github

A/B test variations styling doesn’t often look right using `emotion` css styling with Next.js

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 rendered html to pluck all those styles tags that are tucked under the template tag up to the body tag.

In Next.js you can do that by extracting the styles into a custom document

To do this, you will first create a file at ./pages/_document.js, this will replace the default Document normally provided by Next.js

then we’re going to need a library to make it possible to extract the necessary styles, we recommend cheerio

npm install cheerio

Import cheerio into your _document file and then we can create a function to extract all the necessary styles

const extractABTestingStyles = (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 globalStyles
}

From here, you will run extractABTestStyles within the MyDocument component to pull all styles, and then set them inside the body using

<style dangerouslySetInnerHTML={{ __html: this.props.globalStyles }}></style>

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const originalRenderPage = ctx.renderPage

    let globalStyles = ''
    ctx.renderPage = async (options) => {
      const render = await originalRenderPage(options)
      globalStyles = extractABTestingStyles(render.html)
      return render
    }
    const initialProps = await Document.getInitialProps(ctx)
    return {
      ...initialProps,
      globalStyles,
    }
  }
  render() {
    return (
      <Html>
        <Head />
        <body>
          <style
            dangerouslySetInnerHTML={{
              __html: this.props.globalStyles,
            }}
          ></style>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument

And that should do it. You can see an example of how we have achieved the same in our Next.js/Shopify starter using the technique outlined above.

1 Like