Storybook: single stories are pulling in every other stories' stylesheets?

Created on 21 Mar 2017  ·  67Comments  ·  Source: storybookjs/storybook

Hi,

I'm trying to figure out why i'm having this particular issue. I'm dynamically loading stories like this:

function loadStories() {
    const req = require.context('../components', true, /story.js$/);
    req.keys().forEach(filename => req(filename));
}
configure(loadStories, module);

and each story.js file has a respective sass or css file that is imported into it (for the purpose of story specific styles that are outside of the component that story.js would import, to display:

import './story.sass';

I have about 4 stories right now and this is the source of each story iframe... loading every stylesheet:

screen shot 2017-03-21 at 9 56 22 am

Is this normal behavior...?

--

demo

https://github.com/moimikey/729-single-stories-pulling-all-stylesheets

bug todo

Most helpful comment

Putting up a shadow root around stories would instantly solve this problem without @ndelangen 's performance concerns

All 67 comments

also, webpack is emitting all of those stories, so i'm wondering if it's how i've configured webpack?

screen shot 2017-03-21 at 11 27 10 am

i tried to use a decorator also, and this ended up being half broken, as i was able to at least isolate the additional stylesheet to the story, but as i traversed through others, those styles would break unless i did a hard refresh.

.addDecorator((getStory) => {
  require('./story.sass');
  return getStory();
})

@arunoda @mnmtanish

Interesting!, do you have a repo that demonstrates this?

@ndelangen will make one shortly

Still trying to find the time to check this out..

thanks ;D it's a weird one. i've given it a look, but i'm uncertain where to start.

So what I think is happenings is all the CSS files are getting picked up by webpack style-loader and just injected into the head. wether they are used or not.

But how to fix?

What I've done for my CSS is use CSS-modules. and import the generated classnames into my JS. Even if all CSS is injected together into the head, it doesn't matter, because it's guaranteed to be unique classes/selectors.

It doesn't really solve the exact problem you're having. But I think this is the intended behaviour of the style-loader.

this is true. i (and my company) are scoping with css, but we're doing a huge code redevelopment, so we have shared styles, thus a combination of styleName and className. so what ends up affecting storybook are those "outsider" css files.

i'll look into storybook code again tonight after i've had some beers and figure out how to perhaps solve this, implementation wise.

@moimikey for your problem I guess you have to skip the style-loader and load css files manually with a decorator. Something like this perhaps:

.addDecorator(getStory => (
  <div>
    <link ... />
    {getStory()}
  </div>
))

Wow .... that never crossed my mind... I'll try that.

ick but then again, using sass... x_x. i even tried an inline require. but i didn't have much luck. that would be an excellent solution for straight up css though :)

so @mnmtanish. thank you for your guidance. i've solved my issue with your inspiration:

.addDecorator((getStory) => {
  require('./story.sass');
  return getStory();
})

mmm so the only issue then now is that when you navigate from story to story, it's stacking the styles :(

Maybe the style loader can be configured to insert it somewhere other than HEAD so it can be removed. I haven't done this so not sure if it's even possible. Check these out.

Isn't it the responsibility of react storybook to load the component in complete isolation and thus only have JavaScript/CSS ran that is related to the current selected story?

Is this related to https://github.com/storybooks/react-storybook/issues/686?

@ConneXNL yes. good point. this is true... ;X

@mnmtanish my next response of course leads to another issue, insertAt might be useful, but it only available in the latest version of style-loader, which is not possible to use with storybook, because it uses [email protected]. storybook is still using 0.x. the latest possible version we can use is [email protected] :(...

Hey @moimikey maybe you can try the approach the last mentioned with the alpha release?

Isn't it better/safer to create a new iframe element when switching stories?

That'd be safe, and also bad for performance.

The new iframe would have to parse the javascript, parse the CSS, connect to the postmessage-channel, reconnect to the websocket, and probably more stuff.

Maybe removing <style> elements on story-switch is enough to resolve this issue?

Of course there are no global styles and everything is properly namespaced, this wouldn't be an issue outright. Wishful thinking I guess. 😃

If someone can prove the above statements are false or can demonstrate there's no negative consequences, I'm all ears!

I did some tests today. The decorator pattern worked well so that styles are only injected once you switch to the story. However, I had the same issue where the styles aren't removed when switching stories.

I played around with removing styles in a decorator but it seems the required style is only applied once. Is it possible to re-trigger a require()? I tried using singleton: false but that didn't fix the problem.

I'm almost hesitant to even suggest this, but you could try busting the webpack cache:
https://webpack.github.io/docs/api-in-modules.html#advanced

This is webpack 1 docs, but might still work.

Idea: you could write an decorator that removes all <style>...</style>'s when switching stories. To clean-up the styles not relevant to the current story.

I am almost there. In the webpack config I use
'style-loader/useable' instead of 'style-loader',

This adds an API for you to work with. .use() to add the styles, unuse() to remove them. In my stories file I use the decorator like:

.addDecorator((c) => <ReactStylesheet stylesheets={[require('./stories.scss')]}>{ c() }</ReactStylesheet> )

Using the following React Component to add and remove the styles.

import * as React from 'react';

export class ReactStylesheet extends React.Component{

    componentWillUnmount(){
        let stylesheets = Array.isArray(this.props.stylesheets) ? this.props.stylesheets : [this.props.stylesheets];
        stylesheets.forEach((stylesheet) => {
            console.log("Unmounting....");
            stylesheet.unuse();
        });

    }

    componentDidMount(){
         let stylesheets = Array.isArray(this.props.stylesheets) ? this.props.stylesheets : [this.props.stylesheets];
        stylesheets.forEach((stylesheet) => {
            console.log("Mounting....");
            stylesheet.use();
        });
    }

    render(){
        return this.props.children;
    }
}

Changing the stylesheet correctly hot reloads the styles. Switching to a different story unuse() is called and the stylesheets are cleaned. However, the method breaks when re-adding the styles as it ads the non hrm updated version of the stylesheet. Doing any change after that also throws this error:

Uncaught (in promise) TypeError: Cannot read property 'refs' of undefined
    at update (webpack:///./~/style-loader/addStyles.js?:63:4)
    at eval (webpack:///./src/Component/stories.scss?:32:4)
    at Object.hotApply [as apply] (http://dev.test:6006/static/preview.bundle.js:499:14)
    at cb (webpack:///(webpack)-hot-middleware/process-update.js?:52:36)
    at eval (webpack:///(webpack)-hot-middleware/process-update.js?:68:13)
    at <anonymous>

I am not sure how to update the require statement to point to the latest HRM version.

Fantastic investigative work! I looked around for something like this, before and wasn't able to find it.

Anything we can do on this side to help @ConneXNL ?

the solutions are getting close. the idea of stylesheet.use() and unuse was foreign to me, but this seems like it's getting on the right track.

this is also another interesting thing for sandboxing storybook https://github.com/Wildhoney/ReactShadow

Hi everyone! Seems like there hasn't been much going on in this issue lately. If there are still questions, comments, or bugs, please feel free to continue the discussion. Unfortunately, we don't have time to get to every issue. We are always open to contributions so please send us a pull request if you would like to help. Inactive issues will be closed after 60 days. Thanks!

@ConneXNL to finish this issue up, do you think you could help us with improving the docs in this regard?

If you can't find a good place for it, just punk it down somewhere in markdown format. I'll take care of placing it in.

🙇

Hi everyone! Seems like there hasn't been much going on in this issue lately. If there are still questions, comments, or bugs, please feel free to continue the discussion. Unfortunately, we don't have time to get to every issue. We are always open to contributions so please send us a pull request if you would like to help. Inactive issues will be closed after 60 days. Thanks!

Hey there, it's me again! I am going close this issue to help our maintainers focus on the current development roadmap instead. If the issue mentioned is still a concern, please open a new ticket and mention this old one. Cheers and thanks for using Storybook!

Same issue here, storybook doesn't isolate each story, making it unusable for any visual testing/ acceptance testing.

Putting up a shadow root around stories would instantly solve this problem without @ndelangen 's performance concerns

@bennypowers interesting! would you have a code sample on how to achieve that? 🙇

Might be interesting for @shilman too.

Hi. I'm also experiencing this issue
Was this fixed or is there any workaround?

@ndelangen

The quickest path to satisfaction here is probably @moimikey's suggestion to use ReactShadow

The strategy to take might be to wrap the root in a ReactShadow component, then bring in the styles with adoptedStyleSheets (or a <style> element for non-supporting browsers)

https://github.com/storybookjs/storybook/blob/ba74d889fcfd87849a6ae9369f5e4176e8149d33/lib/core/src/client/preview/start.js#L253

Please re-open this, this issue makes adding custom styles on a per-story basis exceedingly difficult. I have totally separate MDX stories that implement custom styling for their examples, and globally including all styles from every story makes this use case untenable.

edit: Thank you!!!

I hope for this to be solved by March 21st, 2020.

@moimikey Any interest in taking this on? Best way to ensure it's done by a certain date ... 😉

IMO we should add parameters or addon to handle this feature instead of adding special functionality just for stylesheets. It'll cause inconsistent behavior between stylesheets and scripts. But maybe it's a good time to consider how "isolation" should be?

I wrote a quick PoC for the addon approach, for who wonder it's possible.
https://github.com/pocka/storybook-addon-css

@pocka You are AWESOME! 💯

yazzzzz. cheers @pocka

Worth noting @pocka's solution doesn't work if you're using MDX stories because of mdx-js/mdx#894.

Edit: My bad, it definitely does! You need to have style-loader 1.x+ and then do something like this:

--- a/components/grid/GridChild.stories.mdx
+++ b/components/grid/GridChild.stories.mdx
@@ -1,7 +1,9 @@
 import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
 import { GridContainer, GridRow, GridChild } from './';
 import '../../shared/critical-path.scss';
-import 'o-grid/demos/src/scss/_demos.scss';
+import demoStylesModule from '!!style-loader?injectType=lazyStyleTag!css-loader!sass-loader!o-grid/demos/src/scss/_demos.scss?story';
+
+export const demoStyles = Promise.resolve(demoStylesModule);

 <Meta title="Core|Grid/GridChild" component={GridChild} />

@@ -37,7 +39,12 @@ You supply it a `colspan` prop in one of the following formats:
     ```

 <Preview>
-  <Story name="Default unresponsive columns">
+  <Story
+    name="Default unresponsive columns"
+    parameters={{
+      styles: [demoStyles],
+    }}
+  >
     <GridContainer>
       <GridRow>
         <GridChild colspan="1">

@aendrew
Thank you for pointing out that, I completely forgot about MDX :no_mouth:
I updated PoC and added MDX examples.

With the addon approach (per story styles), in contrast to the file-scoping approach (per file styles), the Docs tab will get all stylesheets of every story. In my example, "foo" and "baz" story importing foo.css and "bar" story importing bar.css, then Docs tab gets both foo.css and bar.css. I think this is unavoidable and I don't know this is acceptable or not.

@pocka I think that approach might work out well with https://github.com/storybookjs/storybook/tree/next/addons/cssresources WDYT?

@ndelangen
Ah, that's right!

foo.story = {
  parameters: {
    cssresources: [
      {
        id: 'foo',
        code: `<style>${require('!to-string-loader!css-loader!./foo.css')}</style>`,
        picked: true
      }
    ]
  }
}

One concern: if a user imports very large stylesheets, the "CSS Resources" tab will be messy.

@pocka right, I think the cssresources addon can be vastly improved in that department. Do you feel like taking that on?

@ndelangen
Yes :smiley:

By the way, what do you think about letting users to write raw-webpack-query? (like !to-string-loader!...) I think we need a lot of black magic in the addon code if we want to get rid of this...

I think we support babel-macros by default, so users could be using macro-preval to inject file-contents into the bundle as well?

I didn't know that, I'll take a look at it soon!

Hi there, i'm experiencing the same issue.

For anyone facing the issue, please try this workaround. (It needs a little deep webpack knowledge though).


@ndelangen I took a look at the macro but what it enables is "loading CSS files from file system", right? I think many users want to import SASS, Less, Stylus, CSS files with PostCSS, or etc, so that approach wouldn't satisfy the needs. My current idea is CSSResource addon adding a rule that imports CSS file with to-string-loader (or file-loader) so that the user could import a CSS file then use it for the addon.

// adding a rule like this
{
  test: /\.css$/,
  resourceQuery: /cssresources/,
  use: ['to-string-loader', 'css-loader', 'postcss-loader']
}

For pre-processors, an option to customize test and use is also needed. It's possible to pick a rule for style file then modify it with oneOf but it would be so complicated...

What do you think?

@pocka yeah that sounds like an interesting concept!

Hi, wondering if this is still being worked on? I am also experiencing the issue, I am aware of the work around but would like to know if a fix will be available any time soon.

Hi, can you provide a example of the workaround with a Vue Story.?

As far as I can tell, the workaround still results in stylesheets stacking after initial view, at least if you're using the Docs add-on. 😕

With no disrespect intended, I find @pocka's addon approach rather lacking, as it doesn't isolate styles that have been imported in component files as opposed to in the story files, which I think is the more common pattern. My personal desire—I suspect this may be shared by others in this thread—is to be able to have an import './Button.css' inside of Button.jsx that only gets used in story files where Button.jsx is imported. Per-story styling, in the manner that @pocka has provided, isn't nearly as important to me as making sure that no component that doesn't itself import Button (directly or indirectly) isn't affected by any CSS rules from Button.css. (The concern here is wanting to make sure that OtherWidget.css, say, isn't lacking some rules that inadvertently ended up in Button.css instead—maybe they got overlooked in a refactoring or something—and missing it because the stories for OtherWidget get all the statically-imported CSS of the entire app, so OtherWidget still looks fine in Storybook.)

What I think I would do instead is change all CSS loaders to inject with lazyStyleTag, and then use webpack API to generate a new module that groups the CSS modules by the story files that ultimately require them and listens to story change events to turn the appropriate modules on and off.
Has this approach already been considered and discarded, or are there any issues with it that you see now? I think I can do it all in a Storybook addon, but it might be cleaner if integrated into Storybook as a core feature.

If you want strong style encapsulation, browsers ship with it. At the risk of exposing my own ignorance here, I'm still not sure why all this userland JavaScript (with its associated complexity costs) is necessary to accomplish something that's built-in and ready to go.

Why not just render each story's DOM into a shadow root with something like this

customElements.define('encapsulated-story', class EncapsulatedStory extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  /* not sure why we'd need this getter, but let's say */
  get storyHTML() {
    return this.shadowRoot.innerHTML;
  }

  set storyHTML(string) {
    this.shadowRoot.innerHTML = storyHTML;
  }
});

and whenever the story changes

encapsulatedStory.storyHTML = theStoryDOMStringWithAllStyleTags;

and done? Here, theStoryDOMStringWithAllStyleTags is just the concatenation of a story's HTML with all associated styles concated as inline <style> tags. Story's could style the host element with the :host selector, as normal.

That's a bare-minumum starting point, which could be built on later on perhaps with some library code, but at least it will accomplish the goal of strong style encapsulation without the need for all these new proposed APIs.

How does that work with webpack, though? Webpack packages everything up into a JavaScript bundle, not a DOM string; and the way webpack is currently configured, it packages all stories up into a single bundle. I don't think a shadow root helps when the JavaScript being (hot-re)loaded inserts styles directly into the document's head. You need to do some hairy webpack configuration one way or another to change that.

Using the shadow DOM to completely isolate each story would also mean replicating the style tags shared by many stories into each one; using a shared style as bundled by webpack will be more efficient. Maybe not enough to make a huge difference, but possibly enough to offset the benefits, if there are any, of using the shadow DOM instead of using lazyStyleTag (which I think is the only piece of complexity that shadow roots would save you).

I am also interested on seeing this fixed sooner or later.

That'd be safe, and also bad for performance.

The new iframe would have to parse the javascript, parse the CSS, connect to the postmessage-channel, reconnect to the websocket, and probably more stuff.

@ndelangen sorry to quote you from 2017, but is this still your perspective, that reloading an iframe is too expensive? Browsers do this type of thing constantly; they're probably very optimized for it. In this case it would be even faster than a regular page load, since there are no network requests involved.

To me, the benefit outweighs the cost, because I would very much prefer a fresh iframe for each story. I would tolerate a delay of as much as 600ms for such a luxury.

(My use case is that I'm trying to render some legacy angularjs components in storybook, and let's just say that the components aren't very pure. They're very stateful; they have side effects and make use of angularjs services which are also very stateful. Things break in unexpected ways.)

One idea for an API surface is to configure storybook in .storybook/manager.js.

addons.setConfig({
  refreshBetweenStories: true,
})

This could be considered a UI setting if you tilt your head the right way.

There would be no runtime cost for people who do not enable this flag, and for those who do enable it, they _really_ need it, so any such delay would be tolerable.

If you want this fixed, please upvote by adding a 👍 to the issue description. We use this to help prioritize!

.addDecorator((getStory) => {
require('./story.sass');
return getStory();
})

HI!!!!
where can I put this one?!!!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ZigGreen picture ZigGreen  ·  3Comments

arunoda picture arunoda  ·  3Comments

sakulstra picture sakulstra  ·  3Comments

alexanbj picture alexanbj  ·  3Comments

zvictor picture zvictor  ·  3Comments