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

Created on 4 Mar 2018  路  44Comments  路  Source: mui-org/material-ui

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.

docs

Most helpful comment

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.

All 44 comments

I am facing the same issue. As @estaub already said, it's super tricky to debug. I suggest that we reopen this issue find a good solution or at least mention this in the docs.

@oliviertassinari what do you think?

Ohh wait ... it is mentioned in the docs ... nevermind.

This includes Material-UI classname generation.

@estaub Oh, I haven't seen this issue. I'm using getDataFromTree too. I believe the root of this issue is that Apollo isn't traversing the React tree like react-dom is doing it: https://github.com/facebook/react/issues/12640.

Also, generating the class name twice on the server require some CPU resource. Generating them once requires ~30% of what react needs. So, I would encourage you to use the disableStylesGeneration option when traversing the React tree with Apollo. In the future, I want to work on some caching capabilities to cut this cost to 0% for repeated page requests.

@HriBB I have tried my best to add meaningful debug messages. From the opened issues, I know the story isn't great yet.

Thanks for the disableStylesGeneration tip.

the root of this issue is that Apollo isn't traversing the React tree like react-dom is doing it

That may well be a problem in some context, but it's not the one I ran into. The one I ran into is the obvious one, once you understand what getDataFromTree and style-generation is doing, namely:

If you use the same generateClassName for both getDataFromTree and renderToString, without using disableStylesGenerationwhile in getDataFromTree, the sequence number in the classname during generateClassName is always wrong, never matching, because it's not starting from 0.

Once I fixed that, I haven't run into any mismatches that weren't ultimately attributable to real skewage, due to needing to do different things in SSR rendering than in client-side. But these can be hair-tearingly difficult to resolve without the kind of generation-logging (and subsequent manual diffing) I described above. Once you have that log, it's usually a piece of cake; just look at the first difference, and it usually points at or near to the problem.

@estaub So, you are suggesting that we should have a debug mode for the style generation that logs into the console?

@oliviertassinari Yes, or a wrapper, and doc explaining what to do when you have className skew, in the SSR section.

Thanks for all your work! -Ed

Yes, or a wrapper, and doc explaining what to do when you have className skew, in the SSR section.

@estaub I agree, we could add an FAQ section about it! Great idea.
But I would rather start with documenting the "missing CSS in production" issue. I have seen this one more often.

It's super painful to debug SSR. @oliviertassinari or @estaub is there some example on how to use disableStylesGeneration?

@HriBB I have not experimented with it yet, but here's what I think. disableStylesGeneration deals with the original problem I described. If you are already using a new generateClassName and JssProvider, as I described, it won't help any additional problems you are having - it will just be faster.

What exactly is the problem you are having? Is it React complaining about the classNames not matching, or something else?

If it's className matching, I recommend wrapping your generateClassName for SSR render and client render in a logger that gives you the classname and maybe the inputs. Diff the log from SSR render against the log from client render, looking for where they first diverge. You will hopefully be able to see that it's a place where you understand how and why the server and client output diverge.

@estaub I am not yet 100% sure what the problem is. The website blinks, when the SSR styles are removed on the client. I checked the server and client output really quickly, and some classNames are different for sure. Will debug now, by wrapping generateClassName ...

@HriBB If you have mismatch, the browser console will have messages about it if you are running React in dev mode.

@estaub thanks for all your help. Really appreciate it ;)

I've done some debugging, and I see the problem. There is a mismatch between server and client classNames. The order is different. Probably I am doing something wrong. Not sure yet about the HTML, need to debug that as well ...

Note that I am using material-ui v1, react & react-dom v16.3.2, new context API and dynamic imports with react-loadable. Not sure if it matters.

client classNames

...
MuiDrawer-docked-124
MuiDrawer-paper-125
MuiDrawer-paperAnchorLeft-126
MuiDrawer-paperAnchorRight-127
MuiDrawer-paperAnchorTop-128
MuiDrawer-paperAnchorBottom-129
MuiDrawer-paperAnchorDockedLeft-130
MuiDrawer-paperAnchorDockedTop-131
MuiDrawer-paperAnchorDockedRight-132
MuiDrawer-paperAnchorDockedBottom-133
MuiDrawer-modal-134
MuiModal-root-135
MuiModal-hidden-136

// mismatch starts here

AppContent-normal-137
AppContent-full-138
AppContent-fluid-139
AppContent-bottomToolbar-140
AppContentSpinner-spinner-141
AppContentSpinner-centered-142
Connect-AppSnackbar--close-143
MuiSnackbar-root-144
MuiSnackbar-anchorOriginTopCenter-145
MuiSnackbar-anchorOriginBottomCenter-146
MuiSnackbar-anchorOriginTopRight-147
MuiSnackbar-anchorOriginBottomRight-148
MuiSnackbar-anchorOriginTopLeft-149
MuiSnackbar-anchorOriginBottomLeft-150
Connect-AppDialog--content-151
MuiDialog-root-152
MuiDialog-paper-153
MuiDialog-paperWidthXs-154
MuiDialog-paperWidthSm-155
MuiDialog-paperWidthMd-156
MuiDialog-paperFullWidth-157
MuiDialog-paperFullScreen-158
HomeView-root-159
HomeView-hero-160
HomeView-heroContent-161
HomeView-heroText-162
HomeView-heroButton-163
HomeView-content-164
MuiGrid-container-165
MuiGrid-item-166
MuiGrid-zeroMinWidth-167
MuiGrid-direction-xs-column-168
MuiGrid-direction-xs-column-reverse-169
MuiGrid-direction-xs-row-reverse-170
MuiGrid-wrap-xs-nowrap-171
MuiGrid-wrap-xs-wrap-reverse-172
MuiGrid-align-items-xs-center-173
MuiGrid-align-items-xs-flex-start-174
MuiGrid-align-items-xs-flex-end-175
MuiGrid-align-items-xs-baseline-176
MuiGrid-align-content-xs-center-177
MuiGrid-align-content-xs-flex-start-178
MuiGrid-align-content-xs-flex-end-179
MuiGrid-align-content-xs-space-between-180
MuiGrid-align-content-xs-space-around-181
MuiGrid-justify-xs-center-182
MuiGrid-justify-xs-flex-end-183
MuiGrid-justify-xs-space-between-184
MuiGrid-justify-xs-space-around-185
MuiGrid-spacing-xs-8-186
MuiGrid-spacing-xs-16-187
MuiGrid-spacing-xs-24-188
MuiGrid-spacing-xs-32-189
MuiGrid-spacing-xs-40-190
MuiGrid-grid-xs-191
MuiGrid-grid-xs-1-192
MuiGrid-grid-xs-2-193
MuiGrid-grid-xs-3-194
MuiGrid-grid-xs-4-195
MuiGrid-grid-xs-5-196
MuiGrid-grid-xs-6-197
MuiGrid-grid-xs-7-198
MuiGrid-grid-xs-8-199
MuiGrid-grid-xs-9-200
MuiGrid-grid-xs-10-201
MuiGrid-grid-xs-11-202
MuiGrid-grid-xs-12-203
MuiGrid-grid-sm-204
MuiGrid-grid-sm-1-205
MuiGrid-grid-sm-2-206
MuiGrid-grid-sm-3-207
MuiGrid-grid-sm-4-208
MuiGrid-grid-sm-5-209
MuiGrid-grid-sm-6-210
MuiGrid-grid-sm-7-211
MuiGrid-grid-sm-8-212
MuiGrid-grid-sm-9-213
MuiGrid-grid-sm-10-214
MuiGrid-grid-sm-11-215
MuiGrid-grid-sm-12-216
MuiGrid-grid-md-217
MuiGrid-grid-md-1-218
MuiGrid-grid-md-2-219
MuiGrid-grid-md-3-220
MuiGrid-grid-md-4-221
MuiGrid-grid-md-5-222
MuiGrid-grid-md-6-223
MuiGrid-grid-md-7-224
MuiGrid-grid-md-8-225
MuiGrid-grid-md-9-226
MuiGrid-grid-md-10-227
MuiGrid-grid-md-11-228
MuiGrid-grid-md-12-229
MuiGrid-grid-lg-230
MuiGrid-grid-lg-1-231
MuiGrid-grid-lg-2-232
MuiGrid-grid-lg-3-233
MuiGrid-grid-lg-4-234
MuiGrid-grid-lg-5-235
MuiGrid-grid-lg-6-236
MuiGrid-grid-lg-7-237
MuiGrid-grid-lg-8-238
MuiGrid-grid-lg-9-239
MuiGrid-grid-lg-10-240
MuiGrid-grid-lg-11-241
MuiGrid-grid-lg-12-242
MuiGrid-grid-xl-243
MuiGrid-grid-xl-1-244
MuiGrid-grid-xl-2-245
MuiGrid-grid-xl-3-246
MuiGrid-grid-xl-4-247
MuiGrid-grid-xl-5-248
MuiGrid-grid-xl-6-249
MuiGrid-grid-xl-7-250
MuiGrid-grid-xl-8-251
MuiGrid-grid-xl-9-252
MuiGrid-grid-xl-10-253
MuiGrid-grid-xl-11-254
MuiGrid-grid-xl-12-255
Section-root-256
ResponsiveIframe-root-257
Ratio169-root-258

server classNames

...
MuiDrawer-docked-124
MuiDrawer-paper-125
MuiDrawer-paperAnchorLeft-126
MuiDrawer-paperAnchorRight-127
MuiDrawer-paperAnchorTop-128
MuiDrawer-paperAnchorBottom-129
MuiDrawer-paperAnchorDockedLeft-130
MuiDrawer-paperAnchorDockedTop-131
MuiDrawer-paperAnchorDockedRight-132
MuiDrawer-paperAnchorDockedBottom-133
MuiDrawer-modal-134
MuiModal-root-135
MuiModal-hidden-136

// mismatch starts here

HomeView-root-137
HomeView-hero-138
HomeView-heroContent-139
HomeView-heroText-140
HomeView-heroButton-141
HomeView-content-142
AppContent-normal-143
AppContent-full-144
AppContent-fluid-145
AppContent-bottomToolbar-146
MuiGrid-container-147
MuiGrid-item-148
MuiGrid-zeroMinWidth-149
MuiGrid-direction-xs-column-150
MuiGrid-direction-xs-column-reverse-151
MuiGrid-direction-xs-row-reverse-152
MuiGrid-wrap-xs-nowrap-153
MuiGrid-wrap-xs-wrap-reverse-154
MuiGrid-align-items-xs-center-155
MuiGrid-align-items-xs-flex-start-156
MuiGrid-align-items-xs-flex-end-157
MuiGrid-align-items-xs-baseline-158
MuiGrid-align-content-xs-center-159
MuiGrid-align-content-xs-flex-start-160
MuiGrid-align-content-xs-flex-end-161
MuiGrid-align-content-xs-space-between-162
MuiGrid-align-content-xs-space-around-163
MuiGrid-justify-xs-center-164
MuiGrid-justify-xs-flex-end-165
MuiGrid-justify-xs-space-between-166
MuiGrid-justify-xs-space-around-167
MuiGrid-spacing-xs-8-168
MuiGrid-spacing-xs-16-169
MuiGrid-spacing-xs-24-170
MuiGrid-spacing-xs-32-171
MuiGrid-spacing-xs-40-172
MuiGrid-grid-xs-173
MuiGrid-grid-xs-1-174
MuiGrid-grid-xs-2-175
MuiGrid-grid-xs-3-176
MuiGrid-grid-xs-4-177
MuiGrid-grid-xs-5-178
MuiGrid-grid-xs-6-179
MuiGrid-grid-xs-7-180
MuiGrid-grid-xs-8-181
MuiGrid-grid-xs-9-182
MuiGrid-grid-xs-10-183
MuiGrid-grid-xs-11-184
MuiGrid-grid-xs-12-185
MuiGrid-grid-sm-186
MuiGrid-grid-sm-1-187
MuiGrid-grid-sm-2-188
MuiGrid-grid-sm-3-189
MuiGrid-grid-sm-4-190
MuiGrid-grid-sm-5-191
MuiGrid-grid-sm-6-192
MuiGrid-grid-sm-7-193
MuiGrid-grid-sm-8-194
MuiGrid-grid-sm-9-195
MuiGrid-grid-sm-10-196
MuiGrid-grid-sm-11-197
MuiGrid-grid-sm-12-198
MuiGrid-grid-md-199
MuiGrid-grid-md-1-200
MuiGrid-grid-md-2-201
MuiGrid-grid-md-3-202
MuiGrid-grid-md-4-203
MuiGrid-grid-md-5-204
MuiGrid-grid-md-6-205
MuiGrid-grid-md-7-206
MuiGrid-grid-md-8-207
MuiGrid-grid-md-9-208
MuiGrid-grid-md-10-209
MuiGrid-grid-md-11-210
MuiGrid-grid-md-12-211
MuiGrid-grid-lg-212
MuiGrid-grid-lg-1-213
MuiGrid-grid-lg-2-214
MuiGrid-grid-lg-3-215
MuiGrid-grid-lg-4-216
MuiGrid-grid-lg-5-217
MuiGrid-grid-lg-6-218
MuiGrid-grid-lg-7-219
MuiGrid-grid-lg-8-220
MuiGrid-grid-lg-9-221
MuiGrid-grid-lg-10-222
MuiGrid-grid-lg-11-223
MuiGrid-grid-lg-12-224
MuiGrid-grid-xl-225
MuiGrid-grid-xl-1-226
MuiGrid-grid-xl-2-227
MuiGrid-grid-xl-3-228
MuiGrid-grid-xl-4-229
MuiGrid-grid-xl-5-230
MuiGrid-grid-xl-6-231
MuiGrid-grid-xl-7-232
MuiGrid-grid-xl-8-233
MuiGrid-grid-xl-9-234
MuiGrid-grid-xl-10-235
MuiGrid-grid-xl-11-236
MuiGrid-grid-xl-12-237
Section-root-238
ResponsiveIframe-root-239
Ratio169-root-240
Connect-AppSnackbar--close-241
MuiSnackbar-root-242
MuiSnackbar-anchorOriginTopCenter-243
MuiSnackbar-anchorOriginBottomCenter-244
MuiSnackbar-anchorOriginTopRight-245
MuiSnackbar-anchorOriginBottomRight-246
MuiSnackbar-anchorOriginTopLeft-247
MuiSnackbar-anchorOriginBottomLeft-248
Connect-AppDialog--content-249
MuiDialog-root-250
MuiDialog-paper-251
MuiDialog-paperWidthXs-252
MuiDialog-paperWidthSm-253
MuiDialog-paperWidthMd-254
MuiDialog-paperFullWidth-255
MuiDialog-paperFullScreen-256

Here's a full gist showing the mismatch and my server and client code.

I also get an error Warning: Did not expect server HTML to contain a <div> in <div>.

Warning: Did not expect server HTML to contain a <div> in <div>.
printWarning @ warning.js:33
warning @ warning.js:57
warnForDeletedHydratableElement$1 @ react-dom.development.js:15483
didNotHydrateInstance @ react-dom.development.js:16399
deleteHydratableInstance @ react-dom.development.js:10557
popHydrationState @ react-dom.development.js:10764
completeWork @ react-dom.development.js:9360
completeUnitOfWork @ react-dom.development.js:11671
performUnitOfWork @ react-dom.development.js:11831
workLoop @ react-dom.development.js:11843
renderRoot @ react-dom.development.js:11874
performWorkOnRoot @ react-dom.development.js:12449
performWork @ react-dom.development.js:12370
performSyncWork @ react-dom.development.js:12347
requestWork @ react-dom.development.js:12247
scheduleWorkImpl @ react-dom.development.js:12122
scheduleWork @ react-dom.development.js:12082
scheduleRootUpdate @ react-dom.development.js:12710
updateContainerAtExpirationTime @ react-dom.development.js:12738
updateContainer @ react-dom.development.js:12765
push../node_modules/react-dom/cjs/react-dom.development.js.ReactRoot.render @ react-dom.development.js:16069
(anonymous) @ react-dom.development.js:16488
unbatchedUpdates @ react-dom.development.js:12557
legacyRenderSubtreeIntoContainer @ react-dom.development.js:16484
hydrate @ react-dom.development.js:16540
(anonymous) @ index.js:22
./src/index.js @ main.bundle.js?e505b7983ebbe50c7bc4:106765
__webpack_require__ @ bootstrap:81
checkDeferredModules @ bootstrap:43
webpackJsonpCallback @ bootstrap:30
(anonymous) @ bootstrap:193
(anonymous) @ bootstrap:198

@HriBB Class generation is performed within withStyles, and therefore happens statically, when a module is loaded (first imported). The load order here suggests that the SSR and client renderers are either rendering different things or at least rendering them in a different order for some reason. The causes are going to be specific to your application; look hard at the diffs and think about what they are telling you about the render order. If it's useful to temporarily just do the server render, to eyeball it, disable your call to ReactDOM.hydrate.

@estaub you were right. After observing the classNames and HTML, I am pretty sure I found the reason for client-server classNames mismatch in my app: dynamic import()

After I

// @flow

// disable dynamic import and
/*
import { LoadableView } from 'ui/content'

export const DynamicHomeView = LoadableView({
  loader: () => import(/* webpackChunkName: "home.view" * './View'),
})
*/

// enable static import
export { default as HomeView } from './View'

server and client classNames match and SSR works as expected, no flash after SSR styles removal.

My guess is that on the server, dynamic imports are resolved immediately, thus no loading component is shown. On the client however, browser is loading the dynamic import chunk asynchronously and displaying a loading component, which in my case is a custom <LoadingView />

@estaub and @oliviertassinari do you guys do code splitting?

Do you guys do code splitting?

@HriBB We are using Next.js. It's doing it for us.

@HriBB I do use dynamic loading, without Next, so it's probably not much help.
Consider doing a higher-level dummy import of your <LoadingView/> such that it will be imported before used, at the same point on both client and server.

(If you ever let your IDE clean up your imports for you, include a dummy reference!)

@estaub & @oliviertassinari I think I just cracked it. It was a long and painful journey, but finally I was able to put all pieces together.

Using a fork of @7rulnik/react-loadable that supports webpack4, I had to change the way I load async bundles. On the server I had to capture and inject active bundles between manifest and main bundle. On the client I had to wait for bundles to load, and then hydrate. It works perfectly.

classNames mismatch was caused by react-loadable on the client, which was showing loading component, because the chunk was not yet loaded.

Would be cool if there were some more examples and documentation. For newbies or anyone else basically, I would suggest to use Next.js or some other boilerplate.

I can help with examples and docs ;)

@estaub do you have an example of how you are using getDataFromTree and how you are refreshing generateClassName and JssProvider?

@shotcowboystyle Sorry, nothing that's at all concise.

I have a component creator function that creates my root container - it returns a React component. I run this twice, once to feed to getDataFromTree and once for the server-side render itself. For each, I create a new classname generator using createGenerateClassName. @oliviertassinari points out above that there's a more efficient way to handle this, using disableStylesGeneration. I haven't tried it yet.

One tip on the docs: be sure to at least skim everything that _might_ contain content relevant to any particular problem you have. Don't assume that, because you've found one place where _x_ is discussed, that you've found everything about _x_. I've burned myself this way more than once.

@HriBB can you give an example for your solution? I have the exact same Problem over here!

// @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
}

Thank you for the example @HriBB ! I was able to get my issue resolved using this disableStylesGeneration solution. I was initially using the renderToStringWithData method to do everything in one line, but looks like using the getDataFromTree and renderToString separately is needed in this case to have the two different app components. I've been going crazy trying to solve this and am really glad I found this thread. I had all my ssr pages working fine that didn't have Apollo queries being loaded with them, so I figured it had something to do with Apollo.

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.

After upgrade to 4.0 same issue: broken MUI styles with React-Apollo SSR.
Above recipe made with different App components with:
<StylesProvider disableGeneration={true} generateClassName={createGenerateClassName()}>
for getDataFromTree and
<StylesProvider disableGeneration={false} generateClassName={createGenerateClassName()}>
for renderToString and don't help!
how do you guys solve it?
my component tree:

const AppComponent = ({disableStylesGeneration})=>sheets.collect(<ChunkExtractorManager extractor={webExtractor}>
        <ApolloProvider client={client}>
            <HelmetProvider context={helmetContext}>
                <StyleSheetManager sheet={sheet.instance}>
                    <StylesProvider disableGeneration={disableStylesGeneration} generateClassName={createGenerateClassName()}>
                        <ThemeProvider theme={theme}>
                            <StaticRouter location={url} context={context}>
                                <App/>
                            </StaticRouter>
                        </ThemeProvider>
                    </StylesProvider>
                </StyleSheetManager>
            </HelmetProvider>
        </ApolloProvider>
    </ChunkExtractorManager>);

@valorloff We have a similar issue, though we have a version working without disableStylesGeneration. Still an issue, but here is how we have it working:

import {
  ServerStyleSheets,
  StylesProvider,
  createGenerateClassName,
} from '@material-ui/styles';
import { ApolloProvider, getDataFromTree } from 'react-apollo';
const SSRApp = () => (
  <ApolloProvider client={client}>
    <StaticRouter location={req.url} context={context}>
      <StyleSheetManager sheet={sheet.instance}>
        <StylesProvider injectFirst generateClassName={generateClassName}>
          <ThemeProvider>
            <App />
          </ThemeProvider>
        </StylesProvider>
      </StyleSheetManager>
    </StaticRouter>
  </ApolloProvider>
);



md5-55bedced476c6229bea2c61ea02bf07d



try {
  await getDataFromTree(<SSRApp />);
} catch (error) {
  // don't halt execution, we still want to render with limited data
  // https://github.com/apollographql/react-apollo/issues/1403
}
const sheets = new ServerStyleSheets();
const content = renderToString(
  sheets.collect(<SSRApp />),
);

We get all the correct styles. sheets.collect is only run once, separately from getDataFromTree.

@mlenser big thanks for response!
You right, quite logical put sheets.collect( into renderToString(
Your pattern is not match different from what I have:

return getDataFromTree(<SSRApp/>).then(() => {
        const initState = client.extract();
        const sheets = new ServerStyleSheets();
        try {
            const content = renderToString(sheets.collect(<SSRApp/>));
        ...................................................

after moving sheets.collect i get error:

getDataFromTree error TypeError: str.replace is not a function
    at escape (/home/valaoffice/projects/lc0/node_modules/@material-ui/styles/node_modules/jss/dist/jss.cjs.js:178:49)
    at new StyleRule (/home/valaoffice/projects/lc0/node_modules/@material-ui/styles/node_modules/jss/dist/jss.cjs.js:263:34)
    at Array.onCreateRule (/home/valaoffice/projects/lc0/node_modules/@material-ui/styles/node_modules/jss/dist/jss.cjs.js:355:12)
...........................................................................

I feel, we close to the solution.
And what's strange: I thought that sheets.collect(node) and StylesProvider - is same provider, and they doing the same things, no?

@valorloff I think if you move the const content = renderToString(sheets.collect(<SSRApp/>)); outside the then it should work like mine does.

TBH I don't fully understand why it works in our case, just trying to provide an example for you.

@mlenser can you show client part of app?

@mlenser I second @valorloff's request above. I notice you're using generateClassName={generateClassName} but we cannot see the definition of this value. Are you simply using createGenerateClassName() to create a new instance or are you doing some custom plumbing? I assume this is the default behaviour if you were to omit the StylesProvider component entirely.

This issue is incredibly frustrating to diagnose, debug and has no definite resolution available anywhere that I could find. Are there any standing PRs/discussions to actually address this? I find issues related to this in both repositories dating back almost 2-3 years now.

@dkushner thanks for response!
I ran into this issue during moving from Redux (there were no problems) to Apollo.
Naturally i tried all possible variations of generateClassName=true/false, but @oliviertassinari says "It's no longer needed. You can follow the new API".
Now i unsuccessfully experimenting without old API:
const SSRApp = () => <StaticRouter location={url} context={context}> <HelmetProvider context={helmetContext}> <ApolloProvider client={client}> <ThemeProvider theme={theme}> <App/> </ThemeProvider> </ApolloProvider> </HelmetProvider> </StaticRouter>; return getDataFromTree(<SSRApp/>).then(() => { try { const sheets = new ServerStyleSheets(); const content = renderToString(sheets.collect(<SSRApp/>)); ...
mismatch between client/server classNames still occurs, and I don't know what else can think about!
I thought that the reason in loadable-components, I commented out dynamic modules and remove webExtractor from getDataFromTree. No results. I also could not find fresh discussions on this issue.
Thanks again for the help

Let's add an apollo example in the documentation. I think that we can base the demo on next.js.

Sorry for the delay guys

Client part:

const createClient = () => {
  const client = createApolloClient();
  // Allow the passed state to be garbage-collected
  delete window.__APOLLO_STATE__;
  return client;
};
const generateClassName = createGenerateClassName();

hydrate(
  <ApolloProvider client={createClient()}>
    <BrowserRouter>
      <StylesProvider injectFirst generateClassName={generateClassName}>
        <ThemeProvider>
          <App />
        </ThemeProvider>
      </StylesProvider>
    </BrowserRouter>
  </ApolloProvider>,
  document.getElementById('root'),
);

FWIW, in case recent folks missed it in this long(!!!) issue, I wrote a while back

If [React is complaining about className mismatch], I recommend wrapping your generateClassName for SSR render and client render in a logger that gives you the classname and maybe the inputs. Diff the log from SSR render against the log from client render, looking for where they first diverge. You will hopefully be able to see that it's a place where you understand how and why the server and client output diverge.

I think that we can base the demo on next.js.

better vanilla React, please

 <StylesProvider injectFirst generateClassName={generateClassName}>
    <ThemeProvider>
      <App />
    </ThemeProvider>
  </StylesProvider>
</BrowserRouter>

,
document.getElementById('root'),

@mlenser Thanks!
surprisingly.. that it is working pattern ....

understand how and why the server and client output diverge.

I'm with console.log clear see discrepancies, for example:
on server:

.productComponent-mainDiv-268 {
  flex: 1;
  width: 99%;
  margin-top
...

on client:

.productComponent-mainDiv-273 {
  flex: 1;
  width: 99%;
  margin-top: 50px;
...

@estaub how can i use it in issue research?

@valorloff Ya, it feels like a bit magic keeping server and client in sync. I'm glad the solution that we came up with works for you too!

@mlenser i mean, surprisingly.. that it is working pattern for you, but not for me :(
I have the same pattern as you, but ..unfortunately i have client/server mismatch

can you show createApolloClient() function?
I'm import global apolloClient with client/server switchers like ssrMode: !process.browser,
how much can it affect to class generation ?

@valorloff You almost literally want to do a diff - not just find a single classname pair that's different.
Look for the point(s) of divergence - the places where the previous classnames were in sync, but this pair of classnames are not. Then consider what is different between these pairs. Problems you may find usually will be one of:

  • reordering
  • inclusion of some classes on client but not server, or vice versa

BTW, the fact that the numbers are different by 5 is a clue, too. The reordering or module(s) that are included on client but not on server must have 5 classes.

The reordering or module(s) that are included on client but not on server must have 5 classes.

@estaub you mean reordering jsx wrapping or reordering imports ?
for example, client imports:

import React from 'react'
import {hydrate} from 'react-dom'
import {BrowserRouter} from 'react-router-dom';
import {loadableReady} from '@loadable/component';
import App from './App';
import theme from "../modules/theme/index.js";
import {ThemeProvider} from '@material-ui/styles';
import client from "../api/graphql/client.js";
import {ApolloProvider} from "react-apollo";
import 'core-js';
import {HelmetProvider} from 'react-helmet-async';

how technically can reorder of them affect to classNames generation?

@valorloff I mean reordering of classname generation. Again, find out exactly where in the generated classname sequences the classnames fall out of sync; it's usually a big clue.

@estaub
When you first encountered this issue, did you encounter it after implementation Apollo's SSR?
Have you encountered this before Apollo SSR?
If yes, why reordering became important with getDataFromTree and was absolutely no problem in Redux world (without Apollo)? Technically?

find out exactly where in the generated classname sequences the classnames fall out of sync; it's usually a big clue.

how can you affect to the order of classname generation ?

@valorloff I don't remember the details of my original encounters, it was over a year ago (see above).

Classname order differences are (in my experience) almost always due to module load order differences. The reasons for module load order being different can be diverse and are app-dependent; I hesitate to try to enumerate them.

Again, again, again: my reason for writing was to suggest acquiring a particular set of evidence to help diagnose the problem. If that doesn't seem useful, please ignore.

Hey Guys, if i changed hydrate to render on client part, then there is no any mismatch problem!
Can this narrow the search for the cause of the mismatch?
Please, can you give a clever thought?

Was this page helpful?
0 / 5 - 0 ratings