Buckle up 馃殫 , this is weird.
Gatsby appears to be failing to apply the correct CSS-in-JS generated classes after reading a theme (or indicator thereof) from localStorage. I would normally not suspect Gatsby to be at fault (and it may not be, still) but the same code copy-pasted to a CRA site works as expected, so I'm logging this here in case it is something Gatsby is doing that can be corrected.
Example:
~Code: https://github.com/wKovacs64/gatsby-cssinjs-theming-lost-on-reload~ (removed)
~Deployed: https://confident-wescoff-abc41b.netlify.com/~ (removed)
Same/similar code in CRA (no bug):
~Code: https://github.com/wKovacs64/cra-cssinjs-theming-saved-through-reload~ (removed)
~Deployed: https://blissful-mirzakhani-8b00cb.netlify.com/~ (removed)
Page should render with last selected theme.
Page renders with default theme. Specifically, the generated CSS classes from the default theme are applied instead of the classes from the theme you had selected and persisted (and read back in). The value in localStorage will be correct (your non-default theme) as will the value in your component render function.
I expected to fault emotion at first, but when I swapped it out for styled-components, the bug remained. There is a styled-components branch on the reproduction repo linked above if you'd rather try that. They are very similar now (API-wise, not under the hood), so it's possible the bug is present in both of them, but when I replicated the process in a create-react-app site, the bug was not present so I've circled back to Gatsby. I also tried using a standard class with setState to eliminate hooks, which produced the same result (bug still present). I feel like this has to be my fault, something I'm doing incorrectly, but...?
System:
OS: macOS 10.14.2
CPU: (12) x64 Intel(R) Core(TM) i9-8950HK CPU @ 2.90GHz
Shell: 3.2.57 - /bin/bash
Binaries:
Node: 11.6.0 - /usr/local/bin/node
Yarn: 1.12.3 - /usr/local/bin/yarn
npm: 6.5.0 - /usr/local/bin/npm
Languages:
Python: 2.7.15 - /usr/local/bin/python
Browsers:
Chrome: 71.0.3578.98
Firefox: 61.0.2
Safari: 12.0.2
npmPackages:
gatsby: ^2.0.76 => 2.0.76
gatsby-image: ^2.0.20 => 2.0.25
gatsby-plugin-manifest: ^2.0.9 => 2.0.12
gatsby-plugin-offline: ^2.0.16 => 2.0.20
gatsby-plugin-react-helmet: ^3.0.2 => 3.0.5
gatsby-plugin-sharp: ^2.0.14 => 2.0.16
gatsby-source-filesystem: ^2.0.8 => 2.0.12
gatsby-transformer-sharp: ^2.1.8 => 2.1.9
It seems like it's the combination of SSR + React Hooks + CSS-in-JS + theming that causes this. In my tests, it worked with class components but not hooks, and with client-side rendering (CRA, gatsby develop) but not SSR (gatsby build). The next step would be to try this in a non-Gatsby SSR environment to see if the issue is specific to Gatsby or if it's on the side of the CSS-in-JS libs.
Uhh, this is react hydration related issue.
The <div> with background color has css-tgyek8 class (white background) in DOM (what's statically rendered to html), but css-1jgbt2s class (dark background) in React vDom (what's rendered client side)
From https://reactjs.org/docs/react-dom.html#hydrate :
React expects that the rendered content is identical between the server and the client. It can patch up differences in text content, but you should treat mismatches as bugs and fix them. In development mode, React warns about mismatches during hydration. There are no guarantees that attribute differences will be patched up in case of mismatches. This is important for performance reasons because in most apps, mismatches are rare, and so validating all markup would be prohibitively expensive.
And this is exactly case of "There are no guarantees that attribute differences will be patched up in case of mismatches." (class is the attribute specifically in this case)
There is open issue to converge dev and production builds ( https://github.com/gatsbyjs/gatsby/issues/10706 ), so the behaviour is the same - it won't fix hydration issue, but it will make it behave same way in develop to catch that earlier
Uhh, this is react hydration related issue.
Ah, sure enough! I should have compared against Next.js or another SSR solution rather than CRA. Thanks.
Hi guys, so as I understood it's not possible to persist theme with css-in-js + gatsby?
Hi guys, so as I understood it's not possible to persist theme with css-in-js + gatsby?
It's possible, I was just doing something wrong (wish I hadn't deleted those reproductions so I could remember exactly what - oops).
Maybe you can remember some tip for me? 馃槄
Have you tried it and run into trouble? I'm happy to help if I can.
yep, I am moving my personal website to gatsby and adding blog there.
I am using styled components, and already created a theme toggler.
I had a /src/state/localStorage.js for persisting state.
But I removed it(https://github.com/dmitriyaa/dmitriyaa.github.io/commit/300aa3946fd7c2a5aa63cf864f0594ad4100bb4a) because didn't find solution
Would be awesome if you could have a look.
Here is the development branch:
https://github.com/dmitriyaa/dmitriyaa.github.io/tree/implementation-3.0-gatsby
Here is live website: https://dmitriyaa.github.io
If you need more info please let me know. This is my first time chatting in github :D
Yeah, it appears you're running into the same issue I did because you're restoring the persisted theme "too early" and React can't reconcile the hydrated version with the static version in the Gatsby build output.
One way around this is to set the theme after the app has hydrated (using React.useEffect, perhaps - but this will take some refactoring in your particular app, probably requiring you to move the theme provider lower down in the tree). The downside of this is you will see a "flash of default-themed content" on page load because it will render the default theme before the effect runs and re-renders with the non-default theme. If you can live with the flash, you're done. If you can't, keep reading...
One way to avoid the flash is to change the styles to use CSS custom properties and inject a small script at the top of body to read the preferred theme and apply a corresponding class to the body tag. See this Twitter thread by Dan Abramov where he implemented this on his own blog site.
Unfortunately for me, I am targeting a browser that does not support CSS custom properties, so I had to do it a bit differently and duplicate all my themed style rules. It's gross and I don't like it, but it works in all browsers and doesn't flash:
/* some early JS applies `dark` or `light` class to `body` based on persisted theme */
import { light, dark } from '../theme';
const ThemedDiv = styled.div`
body.light & {
color: ${light.text};
background-color: ${light.background};
}
body.dark & {
color: ${dark.text};
background-color: ${dark.background};
}
`;
Side note: I currently use the use-dark-mode hook to manage the preferred theme and gatsby-plugin-use-dark-mode to inject the corresponding JS snippet.
P.S. You probably don't want/need Redux, unless you've got plans for a whole bunch of other app-level state.
Thank you, I will try to experiment tomorrow.
I also thought that state was restored too early.
p.s. I used redux only for learning purposes :)
I have a similar issue even without using themes. Just using scss, nothing fancy and page refresh looses the css until I click on some link on the site. Any ideas how to resolve this issue?
I have had this issue on my work project for quite a while and it has been something we have on and off debugged.
Our site shows a particular view at / and during a search /?search=something it shows a different set of components. As explained from above...
React expects that the rendered content is identical between the server and the client. It can patch up differences in text content, but you should treat mismatches as bugs and fix them. In development mode, React warns about mismatches during hydration. There are no guarantees that attribute differences will be patched up in case of mismatches. This is important for performance reasons because in most apps, mismatches are rare, and so validating all markup would be prohibitively expensive.
This clearly explains that there was a mismatch between the __hydrated html__ and the __client side render__. We were getting __extremely bizarre css and html issues__: paragraph tags being appended to containers they weren't marked up in.
Our two possible solutions were:
simplify the home page so that the hydrated state of our html matched the client side react code all the time
this was a no go, we liked having search at the root page
find a common hydration state that would not allow for mismatched html between server and client
we went with this solution by using a somewhat elegant, yet hacky solution.
As described in the same ReactDom.hydrate docs
f you intentionally need to render something different on the server and the client, you can do a two-pass rendering. Components that render something different on the client can read a state variable like this.state.isClient, which you can set to true in...
Our index code now looks something like this which has fixed our issue
export const Index = ({
location,
client,
data: { allGithubRaw, allDevhubSiphon, allEventbriteEvents },
}) => {
// this forces the component to re render on the client as there will be a mistmatch between
// html properties on reloads of this page when a search comes in. This is a known effect
// of reacts hydration process https://reactjs.org/docs/react-dom.html#hydrate
// eslint-disable-next-line
const [isClient, setClient] = useState(false);
useEffect(() => {
setClient(true);
}, []);
// ...
let content;
if (!isClient) {
// durring SSR this is our common hydration state...NO CONTENT
content = null;
} else if (resourcesNotFound) {
content = (
<div style={{ padding: '10px' }}>
<Alert
style={{ margin: '0 auto', maxWidth: '400px' }}
color="info"
data-testid={TEST_IDS.alert}
>
No resources found :(
</Alert>
</div>
);
} else if (thereIsASearch) {
// if there is no query render the topics
content = (
<SearchResults
title="Foo bar"
resources={resourcesToSearchAgainst}
results={results}
/>
);
} else {
content = <TopicsPreview />;
}
return (
<Layout>
<Masthead
query={query}
searchSourcesLoading={searchGate.loading}
location={location}
resultCount={results && results.length}
/>
{content}
</Layout>
);
};
// ...
export default withApollo(Index);
@wKovacs64 - was wondering if you could point me in the right direction regarding the useEffect implementation you noted.
I have context provider to expose all components to theme values and now anytime I hard refresh after changing to my dark theme half of the app isn't displaying the new values (this is the confusing part). Anything inside of my GlobalStyles component has an updated theme value as expected (on refresh, when revisiting a page after a preferred theme is set via local storage, or when toggling the theme)
All my css-in-js emotion components that reference theme values aren't updating upon a hard refresh and I can't quite figure that out. This is the file I'm working with: https://github.com/coreybruyere/coreybruyere-v3/blob/develop/src/context/theme-context.jsx
In my case, gatsby-plugin-minify was making this problem, which led the production build to reload all styles after a page was loaded completely in the production build.
Most helpful comment
Yeah, it appears you're running into the same issue I did because you're restoring the persisted theme "too early" and React can't reconcile the hydrated version with the static version in the Gatsby build output.
One way around this is to set the theme after the app has hydrated (using
React.useEffect, perhaps - but this will take some refactoring in your particular app, probably requiring you to move the theme provider lower down in the tree). The downside of this is you will see a "flash of default-themed content" on page load because it will render the default theme before the effect runs and re-renders with the non-default theme. If you can live with the flash, you're done. If you can't, keep reading...One way to avoid the flash is to change the styles to use CSS custom properties and inject a small script at the top of
bodyto read the preferred theme and apply a corresponding class to thebodytag. See this Twitter thread by Dan Abramov where he implemented this on his own blog site.Unfortunately for me, I am targeting a browser that does not support CSS custom properties, so I had to do it a bit differently and duplicate all my themed style rules. It's gross and I don't like it, but it works in all browsers and doesn't flash:
Side note: I currently use the use-dark-mode hook to manage the preferred theme and gatsby-plugin-use-dark-mode to inject the corresponding JS snippet.
P.S. You probably don't want/need Redux, unless you've got plans for a whole bunch of other app-level state.