Under the following conditions, styled-components
will create duplicate global styles during Server-Side Rendering:
- App implements SSR stragies from both
styled-components
and@apollo/client
- App has at least one
useQuery
call - App has at least one
createGlobalStyle
component in the tree
The application in test.js
is a minimal React application with:
- One component created with
createGlobalStyle
- One component created with
styled.div
- One component that makes a
useQuery
call styled-components
SSR support viaServerStyleSheet
/collectStyles
/getStyleTags
/babel-plugin-styled-components
@apollo/client
SSR support viagetMarkupFromTree
This is the barebones Apollo + Styled Components setup, to help illustrate the bug. Note that there are 2 copies of the html
element styles in the SSR output, but there should only be one.
<html>
<style data-styled="true" data-styled-version="5.3.3">
.kERQPB {
position: relative;
} /*!sc*/
data-styled.g1[id='sc-bdvvtL'] {
content: 'kERQPB,';
} /*!sc*/
html {
display: block;
} /*!sc*/
data-styled.g2[id='sc-global-dxkSA1'] {
content: 'sc-global-dxkSA1,';
} /*!sc*/
html {
display: block;
} /*!sc*/
data-styled.g3[id='sc-global-dxkSA2'] {
content: 'sc-global-dxkSA2,';
} /*!sc*/
</style>
<div class="sc-bdvvtL kERQPB">response from GraphQL server</div>
</html>
This is the barebones setup from the first test case, but with a hack! The code hooks into Apollo Client's getMarkupFromTree
and, before each React render, resets the global styles (gs
) object on the ServerStyleSheet
so it's empty. The idea is that we only care about CSS for the very last render, so we'll ignore the global styles until the end.
This hack is far from perfect. Although the global declarations are not duplicated, there's still superfluous output (note the 1 extra data-styled.g*
class).
<html>
<style data-styled="true" data-styled-version="5.3.3">
.kERQPB {
position: relative;
} /*!sc*/
data-styled.g1[id='sc-bdvvtL'] {
content: 'kERQPB,';
} /*!sc*/
html {
display: block;
} /*!sc*/
data-styled.g2[id='sc-global-dxkSA1'] {
content: 'sc-global-dxkSA1,';
} /*!sc*/
</style>
<div class="sc-bdvvtL kERQPB">response from GraphQL server</div>
</html>
I believe the issue is a disconnect between how Apollo Client's getMarkupFromTree
/getDataFromTree
works, and how styled-components
expects SSR to work.
Apollo Client works by calling ReactDOMServer.renderToString
many times in a row. This is necessary because there's no way to figure out ahead of time how many useQuery
calls are nested in the component tree.
However, styled-components
expects to only run through renderToString
once. Ideally, we'd only generate styles on the very last call to renderToString
. However, due to the architecture of Apollo Client, there is no way to know how many React renders will be necessary.
I believe a minimal fix is possible if the ServerStyleSheet
in styled-components
provided an API to clear/reset the stylesheet.
When using Apollo Client w/ SSR, you only use the markup from the final render. Ideally, we'd also just capture styles from the final render.