Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: Add async loaders #12699

Merged
merged 22 commits into from Oct 12, 2020
Merged

Core: Add async loaders #12699

merged 22 commits into from Oct 12, 2020

Conversation

@shilman
Copy link
Member

@shilman shilman commented Oct 8, 2020

Issue: #10009

What I did

Asynchronous loaders to provide external data to stories:

// TodoItem.stories.js

import React from 'react';
import fetch from 'node-fetch';
import { TodoItem } from './TodoItem';

export const Primary = (args, { loaded: { todo } }) => <TodoItem {...args} {...todo} />;
Primary.loaders = [
  async () => ({
    todo: (await fetch('https://jsonplaceholder.typicode.com/todos/1')).json(),
  }),
];
  • Global/component/story loaders
  • Added unboundStoryFn to story store, deprecated storyFn
  • Examples in React/HTML
  • Unit tests
  • Documentation
  • MDX support
  • Loading UI => #12702
  • Storyshots support => #12703
  • Docs inline rendering support => #12726

How to test

See updated stories in official-storybook and html-kitchen-sink

@shilman shilman modified the milestones: 6.1 perf, 6.1 async Oct 8, 2020
@shilman shilman marked this pull request as ready for review Oct 10, 2020
@shilman shilman requested a review from UsulPro as a code owner Oct 10, 2020
Copy link
Member

@tmeasday tmeasday left a comment

Looks great, some comments, mainly documentation

MIGRATION.md Outdated Show resolved Hide resolved
MIGRATION.md Show resolved Hide resolved
docs/writing-stories/loaders.md Outdated Show resolved Hide resolved
docs/writing-stories/loaders.md Show resolved Hide resolved
docs/writing-stories/loaders.md Outdated Show resolved Hide resolved
const addLoaderDeprecationWarning = deprecate(
() => {},
`\`addLoader\` is deprecated, and will be removed in Storybook 7.0.
Instead, use \`export const loaders = [];\` in your \`preview.js\`.
Read more at https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#deprecated-addparameters-and-adddecorator).`
);
Comment on lines +53 to +58

This comment has been minimized.

@tmeasday

tmeasday Oct 12, 2020
Member

We don't need this we aren't exporting addLoader

This comment has been minimized.

@shilman

shilman Oct 12, 2020
Author Member

We are exporting it because that's what the virtual module code calls

export const decorateStory = (storyFn: StoryFn, decorator: DecoratorFunction) => {
return (context: StoryContext = defaultContext) =>
decorator(
// You cannot override the parameters key, it is fixed

This comment has been minimized.

@tmeasday

tmeasday Oct 12, 2020
Member

Can we make an issue (7.0 I guess) to add some more things to this list? Doesn't make sense to be able to override storyId as an example.

lib/client-api/src/story_store.ts Outdated Show resolved Hide resolved
lib/core/src/client/preview/start.test.ts Outdated Show resolved Hide resolved
lib/core/src/client/preview/start.test.ts Outdated Show resolved Hide resolved
shilman and others added 3 commits Oct 12, 2020
Co-authored-by: Tom Coleman <tom@thesnail.org>
@shilman shilman modified the milestones: 6.1 async, 6.1 perf Oct 12, 2020
@shilman shilman mentioned this pull request Oct 12, 2020
@shilman shilman merged commit dd08c4b into next Oct 12, 2020
35 of 41 checks passed
35 of 41 checks passed
Automention
Details
CLI Fixtures
Details
Danger JS
Details
Core Unit Tests
Details
Docs (Storybook) TeamCity build failed
Details
ci/circleci: examples-v2 Your tests failed on CircleCI
Details
ci/circleci: examples-v2-yarn-2 Your tests failed on CircleCI
Details
test Workflow: test
Details
UI Review Go review the new and updated UI.
Details
UI Tests 2 changes must be accepted as baselines.
Details
Pages changed 17 new files uploaded
Details
Redirect rules No redirect rules processed
Details
Aggregate Examples (Examples) TeamCity build finished
Details
Build (Storybook) TeamCity build finished
Details
Coverage (Storybook) TeamCity build finished
Details
Danger All green. Jolly good show.
Details
DeepScan 0 new and 0 fixed issues
Details
E2E (Storybook) TeamCity build finished
Details
Header rules 1 header rule processed
Details
Lint (Storybook) TeamCity build finished
Details
Mixed content No mixed content detected
Details
Smoke Tests (Storybook) TeamCity build finished
Details
Storybook Publish 513 stories published.
Details
Test (Storybook) TeamCity build finished
Details
Test Workflow (Storybook) TeamCity build finished
Details
ci/circleci: build Your tests passed on CircleCI!
Details
ci/circleci: chromatic Your tests passed on CircleCI!
Details
ci/circleci: coverage Your tests passed on CircleCI!
Details
ci/circleci: e2e Your tests passed on CircleCI!
Details
ci/circleci: examples Your tests passed on CircleCI!
Details
ci/circleci: frontpage Your tests passed on CircleCI!
Details
ci/circleci: install-e2e-deps Your tests passed on CircleCI!
Details
ci/circleci: install-examples-deps Your tests passed on CircleCI!
Details
ci/circleci: lint Your tests passed on CircleCI!
Details
ci/circleci: packtracker Your tests passed on CircleCI!
Details
ci/circleci: publish Your tests passed on CircleCI!
Details
ci/circleci: smoke-tests Your tests passed on CircleCI!
Details
ci/circleci: test Your tests passed on CircleCI!
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
deploy Workflow: deploy
Details
deploy/netlify Deploy preview ready!
Details
@shilman shilman deleted the 1009-async-loaders branch Oct 12, 2020
@shilman shilman mentioned this pull request Oct 12, 2020
0 of 5 tasks complete
@agrohs
Copy link

@agrohs agrohs commented Oct 13, 2020

How would this work if we need to have an async decorator? Something to this regard:

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,
]
@tmeasday
Copy link
Member

@tmeasday tmeasday commented Oct 14, 2020

@agrohs: you would just refactor things slightly, and add the loader + decorator together:

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

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

Having said that, can you tell us a little bit about your use case? Although this is an escape hatch to support it, we sort of consider async loading of data to be an anti-pattern, so I'm keen to hear about use cases to change my mind on that.

@agrohs
Copy link

@agrohs agrohs commented Oct 14, 2020

Thanks @tmeasday, that's exactly what I did and seems to be working as expected now...cheers!

@agrohs
Copy link

@agrohs agrohs commented Oct 14, 2020

one minor detail/question is in trying to prevent the loader from running again if its data has already been loaded (or not changed)? Something similar to the following (though it looks like as of now, we don't have access to loaded in the initial payload of the storyContext sent into the loader??)

export const loaders = [async ({ loaded: { activeContext: previousContext }}) => {
  if (previousContext) {
    return previousContext
  }
  return Promise.resolve({ activeContext: { foo: bar } })
}

export const decorators = (Story, { loaded: { activeContext } }) => {
  return (
    <StatefulProvider value={activeContext}>
        <Story {...storyContext} />
    </StatefulProvider>
  )
}
@tmeasday
Copy link
Member

@tmeasday tmeasday commented Oct 14, 2020

Hmm, I would probably suggest using some external cache here -- I don't know if SB should be doing the caching -- we pass the full context into the loader so for instance the loader could fetch different data depending on the context.id. So I don't really think we can do anything sensible in terms of caching the previous value without introducing surface area for problems.

You could do something as simple as using a global var here:

let previousContext;
export const loaders = [async ({ loaded: { activeContext: previousContext }}) => {
  if (previousContext) {
    return previousContext
  }
  return Promise.resolve({ activeContext: { foo: bar } }).then(v => { previousContext = v; return v });
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

3 participants
You can’t perform that action at this time.