question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

A cause of client/server className mismatch in SSR with react-apollo

See original GitHub issue

If you are using react-apollo’s getDataFromTree, be aware that it does a full render traversal of the page in order to trigger all of the data fetching. This includes Material-UI classname generation. For the subsequent renderToString, be sure to use a fresh generateClassName and JssProvider, so that the sequence numbers in the classNames are reset to 0.

I have to say that debugging this has been one of my more miserable experiences in recent memory, taking about 2 days. withStyles is extremely complex, and there are legitimate causes for “redundant” classname generation, like multiple themes, that have to be understood and ruled out. I ultimately found that emitting trace logs from it allowed me to diff client and server behavior. It might make sense to add such a trace capability to withStyles. I found it useful to emit trace for just one component, selected by name, showing each invocation, the stylesCreator index, whether it generated, and what the first emitted classname was.

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Reactions:17
  • Comments:44 (22 by maintainers)

github_iconTop GitHub Comments

3reactions
oliviertassinaricommented, Jan 17, 2019

To be noted, @material-ui/styles is no longer using the index counter for the static style sheets. It’s hashing the class names instead. So, people will only see the issue with dynamic styles going forward.

2reactions
HriBBcommented, Dec 4, 2018
// @flow

import fetch from 'node-fetch'

import React from 'react'
import { renderToString } from 'react-dom/server'

import { Helmet } from 'react-helmet'
import { Provider as ReduxProvider } from 'react-redux'
import { Route, StaticRouter as Router } from 'react-router'

import { ApolloClient } from 'apollo-client'
import { createHttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { ApolloProvider, getDataFromTree } from 'react-apollo'

import JssProvider from 'react-jss/lib/JssProvider'
import { SheetsRegistry } from 'react-jss/lib/jss'
import { MuiThemeProvider, createGenerateClassName } from '@material-ui/core/styles'

//import { MuiThemeProvider } from '@material-ui/core/styles'
//import { createGenerateClassName } from 'styles/debug'

// TODO: maybe switch react-loadable for
// https://github.com/theKashey/react-imported-component
// https://github.com/faceyspacey/react-universal-component
import Loadable from '@7rulnik/react-loadable'
import { getBundles } from '@7rulnik/react-loadable/webpack'
import stats from '../public/stats.json'
import hash from '../public/hash.js'

import postcss from 'postcss'
import autoprefixer from 'autoprefixer'
import cssnano from 'cssnano'

import { createStore } from 'store'
import { createJss } from 'styles/jss'
import { createTheme } from 'styles/theme'

import { AppView } from 'app/view'

const prefixer = postcss([autoprefixer])
const minifier = postcss([cssnano])

export type Context = {
  url: string,
  header: Object,
}

export const renderApp = async (ctx: Context) => {
  // redux store
  const store = createStore()

  // apollo client
  const client = new ApolloClient({
    //ssrMode: true,
    link: createHttpLink({
      uri: 'http://localhost:4000/graphql',
      credentials: 'include',
      headers: {
        cookie: ctx.header.cookie,
      },
      fetch,
    }),
    cache: new InMemoryCache(),
  })

  // material-ui props
  const sheetsRegistry = new SheetsRegistry()
  const sheetsManager = new Map()
  const generateClassName = createGenerateClassName()
  const jss = createJss()
  const theme = createTheme()

  // track modules
  const modules = []

  // server app
  const App = ({ disableStylesGeneration }) => (
    <Router location={ctx.url} context={{}}>
      <ReduxProvider store={store}>
        <ApolloProvider client={client}>
          <JssProvider registry={sheetsRegistry} jss={jss} generateClassName={generateClassName}>
            <MuiThemeProvider disableStylesGeneration={disableStylesGeneration} sheetsManager={sheetsManager} theme={theme}>
              <Route path={'/:page?'} component={AppView} />
            </MuiThemeProvider>
          </JssProvider>
        </ApolloProvider>
      </ReduxProvider>
    </Router>
  )

  // get data from tree
  // capture loadable modules to inject scripts for the client
  // disable styles generation to prevent client-server className mismatch
  await getDataFromTree(
    <App disableStylesGeneration />
  )
    
  // render app to string, generating styles
  const html = await renderToString(
    <Loadable.Capture report={moduleName => modules.push(moduleName)}>
      <App />
    </Loadable.Capture>
  )

  // extract helmet data
  const helmet = Helmet.renderStatic()

  // extract apollo data
  const data = client.extract()

  // extract and minify styles
  let css = sheetsRegistry.toString()
  if (process.env.NODE_ENV === 'production') {
    const options = { from: undefined }
    const result1 = await prefixer.process(css, options)
    css = result1.css
    const result2 = await minifier.process(css, options)
    css = result2.css
  }

  // extract required scripts
  const bundles = getBundles(stats, modules).filter(({ file }) => file.endsWith('.js'))
  const scripts = [
    `/public/manifest.bundle.js?${hash}`,
    ...bundles.map(({ file }) => `/public/${file}?${hash}`),
    `/public/main.bundle.js?${hash}`,
  ]

  // finally parse template
  const body = `<!doctype html>
<html>
  <head>
    ${helmet.title.toString()}
    ${helmet.meta.toString()}
    ${helmet.link.toString()}
    ${scripts.map(script => `<link rel="preload" href="${script}" as="script" />`).join('')}
    <style id="jss-server-side">${css}</style>
  </head>
  <body>
    <div id="climbuddy">${html}</div>
    <script>window.__APOLLO_STATE__=${JSON.stringify(data)}</script>
    <script async src="https://www.googletagmanager.com/gtag/js?id=UA-28297519-1"></script>
    <script>
      window.dataLayer = window.dataLayer || [];
      function gtag(){dataLayer.push(arguments);}
      gtag('js', new Date());
      gtag('config', 'UA-28297519-1');
    </script>
    ${scripts.map(script => `<script async src="${script}"></script>`).join('\n')}
  </body>
</html>`

  return body
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

A cause of client/server className mismatch in SSR ... - GitHub
withStyles is extremely complex, and there are legitimate causes for "redundant" classname generation, like multiple themes, that have to be ...
Read more >
Local state management - Apollo GraphQL Docs
This tells Apollo Client to fetch the field data locally (either from the cache or using a local resolver), instead of sending it...
Read more >
rofrischmann/fela - Gitter
Apollo GraphQL causes client-server mismatch if we attempt to assign className using the fela StyleSheet rendering... Not sure if this is a fela...
Read more >
Class name hydration mismatch in server and client, (Warning
I'm using Material-UI version 1.2.1 . This is my Index component as a root component: import React, {Component} ...
Read more >
SSR with Apollo Client & React and Intro to Prisma - YouTube
SSR with Apollo Client & React and Intro to Prisma, GraphQL API for your ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found