storybookjs / storybook

Storybook is the industry standard workshop for building, documenting, and testing UI components in isolation
https://storybook.js.org
MIT License
84.26k stars 9.27k forks source link

Support async stories #10009

Closed tmeasday closed 4 years ago

tmeasday commented 4 years ago

Consider supporting "async" stories in 6.0. What this may imply:

jamesarosen commented 4 years ago

This is still an issue, stalebot.

My use-case is preloading all the fonts before rendering the stories so that snapshots always have the correct fonts loaded.

tmeasday commented 4 years ago

So we've thrashed this out a bit and concluded that allowing arbitrary async-ness in stories and decorators isn't a great idea. Here's why:

  1. If any story or decorator can be async, then all decorators have to be async. This is problematic in particular for addons (at lot of which may not be!)

  2. In React, we can't have the decoratoredStoryFn be async and still render it as a component. So given 1. this would mean you couldn't use hooks in stories or decorators, which would be a problem for a lot of users.

Given that we've been considering an alternate way of introducing async-ness to stories: Loaders. Here's a sketch of an API:

// A CSF file
export default {
  title: 'MyComponent',
  loaders: [async () => ({ Component: await import('./MyComponent') })],
}

export const MyStory = (args, { Component }) => <Component {...args} />;

The idea is:

For the above example we could simplify things with a global loader, which is potentially available by default (just an idea):

// preview.js
export const loaders = [async ({ componentPath }) => ({ Component: await import(componentPath)],

// A CSF file
export default {
  title: 'MyComponent',
  componentPath: './MyComponent',
}

export const MyStory = (args, { Component }) => <Component {...args} />;
shilman commented 4 years ago

@tmeasday this is genius ❤️

richburdon commented 4 years ago

I have a different but related use case that I don't think this would resolve.

We use https://www.npmjs.com/package/storybook-react-router to configure routes, where params are used by hooks within containers that we want to test. E.g.,

// Some set-up hook
async init = () => {
  const item = await db.createItem();
  return item.id;
}

export default {
  title: 'Invitations',
  decorators: [
    new StoryRouter(null, {
      initialEntries: [`/item/${itemId}`] // Somehow access async set-up.
    })
  ]
};

const Test = () => {
  const { itemId } = useParams();
  const item = db.getItem(itemId);
  return <div>{item}</div>
};

export const withItem = () => {
  return (
    <Switch>
      <Route path='/item/:itemId' component={Test} />
    </Switch>
  );
};

Here, itemId is the ID of an object that should be asynchronously created (for the test) before the component navigates to the route. Do you have a suggestion for this use case?

tmeasday commented 4 years ago

@richburdon could your async decorator not be run in a loader?

shilman commented 4 years ago

Huzzah!! I just released https://github.com/storybookjs/storybook/releases/tag/v6.1.0-alpha.23 containing PR #12699 that references this issue. Upgrade today to try it out!

You can find this prerelease on the @next NPM tag.

Closing this issue. Please re-open if you think there's still more to do.

agrohs commented 4 years ago

Just confirming @shilman that I'm looking at the new code properly. What I was originally thinking (and was not working properly):

export const getActiveContext = () => {
  return Promise.resolve({ foo: bar }); // NOTE: this would call some long running api to bring back data before resolving
}

const withContextProvider = async (Story, storyContext) => {
  const activeContext = await getActiveContext(storyContext)
  return (
    <StatefulProvider value={activeContext}>
        <Story {...storyContext} />
    </StatefulProvider>
  )
}

export const decorators = [
  withContextProvider,
]

but once moving to the new v6.1.0-alpha.23, I am using the following:

export const loaders = [
  async () => ({
    activeContext: await Promise.resolve({ foo: bar }),
  })
]

const withContextProvider = (Story, {
  loaded: {
    activeContext
  }
}) => {
  return (
    <StatefulProvider value={activeContext}>
        <Story {...storyContext} />
    </StatefulProvider>
  )
}

export const decorators = [
  withContextProvider,
]

This "seems to" be mostly working (which is awesome), but just want to confirm I'm using the new async loaders properly??

shilman commented 4 years ago

LGTM @agrohs 👍

agrohs commented 4 years ago

Had everything working properly yesterday on alpha 23 and woke up this morn to all broken. Tried going back in commits and even everything running against 6.0.26 stable seems to be broken. Getting a very strange set of errors related to react hooks, but what's really bizarre about them is that the webpack'd debug stack seems to be showing URLs similar to: webpack://storybook_ui_dll//Users/shilman/projects/baseline/storybook/node_modules/react/cjs/react.development.js?:1439

Any ideas what may have happened @shilman

agrohs commented 4 years ago

This looks potentially related to the fact that storybook does not have pinned versions in its package.json AND a major new update of React went live yesterday (6.14.0) that has broken many projects across the web today: https://github.com/facebook/react/blob/master/CHANGELOG.md.

A similar issue of a dependency of a dependency wreaking havoc on a project happened several months back w/ moment.js b/c their versions were not pinned.

I was able to get this back resolved by adding a resolutions node in our projects package.json to ensure we don't use the upgraded version until working properly w all third parties:

  "resolutions": {
    "**/react": "16.13.1",
    "**/react-dom": "16.13.1",
    "**/react-is": "16.13.1"
  }

This will likely kill many hours for people today using storybook if it hasn't already. Just sharing to try and save some other people headache - but wondering in general why storybook does not have pinned versions in its package file??

shilman commented 4 years ago

@agrohs opened a new issue to discuss #12787

shilman commented 3 years ago

Good golly!! I just released https://github.com/storybookjs/storybook/releases/tag/v6.1.0 containing PR #12699 that references this issue. Upgrade today to the @latest NPM tag to try it out!

npx sb upgrade
jonnytest1 commented 2 years ago

is it possible to update the control panel argTypes in a loader ? i'm trying to asynchronously load in the optinos for a radio control

shilman commented 2 years ago

@jonnytest1 no, argTypes are statically defined

josefineflygge commented 11 months ago

is it possible to update the control panel argTypes in a loader ? i'm trying to asynchronously load in the optinos for a radio control

@jonnytest1 A bit late to the party here but did you ever find a workaround for this problem? Facing a similar issue

jonnytest1 commented 11 months ago

is it possible to update the control panel argTypes in a loader ? i'm trying to asynchronously load in the optinos for a radio control

@jonnytest1 A bit late to the party here but did you ever find a workaround for this problem? Facing a similar issue

probably not possible 🤔 at the end i just used some examples