styled-components / styled-components

Visual primitives for the component age. Use the best bits of ES6 and CSS to style your apps without stress 💅
https://styled-components.com
MIT License
40.46k stars 2.5k forks source link

Missing client side styles when using getStyleElement with SSR #1395

Closed mhriess closed 6 years ago

mhriess commented 6 years ago

styled-components: 2.4.0 babel-plugin-styled-components: 1.3.0 NextJS: 4.2.1

Apologies in advance if this is an obvious issue- I got no response on Spectrum.

I have NextJS setup as instructed in the styled components/NextJS docs. The page renders with all required styles for all visible elements, but any "hidden" elements (ie. a modal that doesn't show until user clicks, styled text hidden in a drawer, etc) has a SC class name generated for it, but no associated styles.

If I switch getStyleElement() for getStyleTags(), the missing styles are included in the server rendered styles, though any styles included via injectGlobal`` are lost. Am I missing something totally obvious?

kitten commented 6 years ago

Can you post some reproductions / code snippets / etc please? There’s more details on what we’d need to help you in the issue template. 😉

mhriess commented 6 years ago

I know! Sorry- I tried to put together a simple repro case in a standalone repo but naturally it works there.

Here's a Modal component which exhibits this behavior:

import * as React from 'react';
import styled from 'styled-components';

import { CircleXIcon } from '../icons/CircleXIcon';
import { mainTheme } from '../../styles/theme';

const ModalContainer = styled.div `
    background: ${props => props.theme.palette.white};
    border-radius: 10px;
    box-shadow: 0 4px 12px 0 rgba(0,0,0,0.50);
    left: 50%;
    min-height: 6rem;
    min-width: 6rem;
    overflow-y: auto;
    padding: .5rem 1em 1em 1rem;
    position: fixed;
    transform: translate(-50%, -50%);
    top: 50%;
`;

const CloseContainer = styled.div `
    display: flex;
    justify-content: flex-end;
    margin: .5rem 0;
`;

export interface ModalProps {
    children?: React.ReactNode;
    className?: string;
    height?: number | string;
    onClose: () => void;
    width?: number | string;
}

export const Modal: React.SFC<ModalProps> = (props: ModalProps) =>
    <ModalContainer className={props.className}>
        <CloseContainer onClick={props.onClose}>
            <CircleXIcon fill={mainTheme.palette.black} />
        </CloseContainer>

        {props.children}
    </ModalContainer>

Usage:

const ScheduleConsultationModal = styled(Modal) `
    height: 42rem;
    overflow-y: hidden;
    width: 55rem;
`;

It's rendered via:

{
                    isScheduleConsultModalShowing
                        ?   <ScheduleConsultationModal onClose={this.toggleRequestConsult}>
                                <ScheduleConsultation />
                            </ScheduleConsultationModal>
                        :   null
                }

This styled component also exhibits the same behavior- if no "Caption" is visible on the initial render, no styles will be generated for it when it's revealed:

export const Caption = span `
    line-height: 18.6px;
    font-family: 'Open Sans', sans-serif;
    font-size: .8rem;
`;
kitten commented 6 years ago

That code looks reasonable.

any "hidden" elements (ie. a modal that doesn't show until user clicks, styled text hidden in a drawer, etc) has a SC class name generated for it, but no associated styles.

This sounds like the correct behaviour. We inject placeholder classes for all components that serve to identify it via its styled component id. This means that any component that isn't rendered won't have any styles in your SSR result.

If I switch getStyleElement() for getStyleTags(), the missing styles are included in the server rendered styles, though any styles included via injectGlobal`` are lost

The injectGlobal styles should definitely be included. I'd need some more SSR code maybe to understand what's going on there, so I can make sure it follows the expected use case and is written correctly.

mhriess commented 6 years ago

I have more context to add but I haven't figured out how this causes the issue. This seems to happen only with StyledComponents I'm importing from a standalone module of StyledComponents. This module is intended to be a shareable library of generic components that get imported into other applications.

If I copy+paste the same Modal code above directly into the NextJS app where it's being imported, it works fine.

Both the library and the app are using the same version of SC - 2.4.0.

kitten commented 6 years ago

@mhriess

This module is intended to be a shareable library of generic components

You don't happen to have forgotten to make styled-components a peer of both, so that it's being caused to be bundled twice? This leads to internal conflicts between the style sheets and caches.

https://github.com/styled-components/styled-components/issues/992 https://github.com/styled-components/styled-components/issues/987

We don't intend to solve this btw, as it's not expected/recommended to import/bundle two different versions/instances of a module twice

mhriess commented 6 years ago

This could definitely be the problem, but I haven't managed to solve it.

I have two packages- Shared (library of generic React components in TypeScript) and App (one of several NextJS apps that consume Shared and also uses SC for its own specific components).

Shared package.json:

...
"peerDependencies": {
  "styled-components": "2.4.0"
 },
"devDependencies": {
  "styled-components": "2.4.0"
},
...

App package.json:

...
"dependencies: {
  "shared": "file:../shared",
  "styled-components": "2.4.0"
},
"peerDependencies": {
    "styled-components": "2.4.0"
 }
...

Thanks again for all the help so far.

gribnoysup commented 6 years ago

Hi @mhriess! I am not very experienced with Next.js but if you can control your build config, you can solve this issue. I created a repo reproducing your problem in a more generic way and described a possible solution in webpack.config.js file

TL;DR: You probably have styled-components installed both in shared and in app folder and default node_modules resolve behavior puts two modules in your app. You can change default resolving behavior or specifically alias styled-components to resolve only in one place.

@mxstbr @philpl Do you think this is possible to warn somehow if there are several instances of styled-components on the page? Can this be discovered somehow? I'd love to try to solve the issue (I actually encountered something like this myself about a year ago 🤣) if you think that this is acceptable and possible

kitten commented 6 years ago

@gribnoysup thanks for jumping in here :+1: @mhriess generally, the other issue will have a couple of solutions for this. If you're not about to switch to a monorepo, the easiest fix is to use babel/webpack to change/specify the explicit import path.

@gribnoysup If you'd like to pick the warning task up, there's an open issue for it :pray: https://github.com/styled-components/styled-components/issues/1011

mhriess commented 6 years ago

@gribnoysup What's the max internet kudos I can offer? This fixed my issue 🙏. I really appreciate it, and all the help from @philpl.

I'll close the issue, but I'm also curious if you have a few more minutes- what led you to this solution in the first place? I don't have a ton of experience with node module resolution intricacies so I'd love any tips on debugging these kinds of issues in the future.

gribnoysup commented 6 years ago

@mhriess glad I could help! 😸

I think when I encountered this one of the first things I did was the same one that you tried: I just moved components from shared to the app and it just started working. This made me think that there was something wrong with the build setup because the code wasn't changed, only the file position.

I used webpack-bundle-analyzer to look at the bundle structure and noticed that the styled-components library is included twice in the bundle. You can try this yourself with the example repo I provided. If you add webpack-bundle-analyzer plugin to the webpack config, you will see something like this:

image

Notice how styled-components is imported twice. You also can see that the second module is coming from the shared/node_modules.

To figure out how webpack resolves modules I just went to webpack docs. Thankfully it was already at the time when webpack 2.0 docs were live. They are structured well, search works great, and overall they are written pretty well.

We are interested in the module resolution process, more specifically in Module paths resolution part for now. We are not overriding anything in the config from the start, so we want to know what are the "reasonable defaults" for resolve.modules property that is mentioned in this part of the article.

And here is our answer:

... similarly to how Node scans for node_modules, by looking through the current directory as well as it's ancestors (i.e. ./node_modules, ../node_modules, and on) ...

and

resolve.modules defaults to:

modules: ["node_modules"]

If you want to add a directory to search in that takes precedence over node_modules/:

modules: [path.resolve(__dirname, "src"), "node_modules"]