diff --git a/.circleci/config.yml b/.circleci/config.yml
index 41926b33775f..faefe40405e5 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -3,11 +3,11 @@ version: 2.1
executors:
sb_node:
parameters:
- class:
- description: The Resource class
- type: enum
- enum: ["small", "medium", "large", "xlarge"]
- default: "medium"
+ class:
+ description: The Resource class
+ type: enum
+ enum: ['small', 'medium', 'large', 'xlarge']
+ default: 'medium'
working_directory: /tmp/storybook
docker:
- image: circleci/node:10-browsers
@@ -360,7 +360,6 @@ jobs:
name: Upload coverage
command: yarn coverage
-
workflows:
test:
jobs:
diff --git a/MIGRATION.md b/MIGRATION.md
index f9e4a8c3adc7..d4c1cc4bce53 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -2,6 +2,7 @@
- [From version 6.0.x to 6.1.0](#from-version-60x-to-610)
- [6.1 deprecations](#61-deprecations)
+ - [Deprecated storyFn](#deprecated-storyfn)
- [Deprecated onBeforeRender](#deprecated-onbeforerender)
- [Deprecated grid parameter](#deprecated-grid-parameter)
- [Deprecated package-composition disabled parameter](#deprecated-package-composition-disabled-parameter)
@@ -120,8 +121,8 @@
- [Addon story parameters](#addon-story-parameters)
- [From version 3.3.x to 3.4.x](#from-version-33x-to-34x)
- [From version 3.2.x to 3.3.x](#from-version-32x-to-33x)
- - [`babel-core` is now a peer dependency (#2494)](#babel-core-is-now-a-peer-dependency-2494)
- - [Base webpack config now contains vital plugins (#1775)](#base-webpack-config-now-contains-vital-plugins-1775)
+ - [`babel-core` is now a peer dependency #2494](#babel-core-is-now-a-peer-dependency-2494)
+ - [Base webpack config now contains vital plugins #1775](#base-webpack-config-now-contains-vital-plugins-1775)
- [Refactored Knobs](#refactored-knobs)
- [From version 3.1.x to 3.2.x](#from-version-31x-to-32x)
- [Moved TypeScript addons definitions](#moved-typescript-addons-definitions)
@@ -138,6 +139,27 @@
### 6.1 deprecations
+#### Deprecated storyFn
+
+Each item in the story store contains a field called `storyFn`, which is a fully decorated story that's applied to the denormalized story parameters. Starting in 6.0 we've stopped using this API internally, and have replaced it with a new field called `unboundStoryFn` which, unlike `storyFn`, must passed a story context, typically produced by `applyLoaders`;
+
+Before:
+
+```js
+const { storyFn } = store.fromId('some--id');
+console.log(storyFn());
+```
+
+After:
+
+```js
+const { unboundStoryFn, applyLoaders } = store.fromId('some--id');
+const context = await applyLoaders();
+console.log(unboundStoryFn(context));
+```
+
+If you're not using loaders, `storyFn` will work as before. If you are, you'll need to use the new approach.
+
#### Deprecated onBeforeRender
The `@storybook/addon-docs` previously accepted a `jsx` option called `onBeforeRender`, which was unfortunately named as it was called after the render.
@@ -1717,7 +1739,7 @@ There are no expected breaking changes in the 3.4.x release, but 3.4 contains a
It wasn't expected that there would be any breaking changes in this release, but unfortunately it turned out that there are some. We're revisiting our [release strategy](https://github.com/storybookjs/storybook/blob/master/RELEASES.md) to follow semver more strictly.
Also read on if you're using `addon-knobs`: we advise an update to your code for efficiency's sake.
-### `babel-core` is now a peer dependency ([#2494](https://github.com/storybookjs/storybook/pull/2494))
+### `babel-core` is now a peer dependency #2494
This affects you if you don't use babel in your project. You may need to add `babel-core` as dev dependency:
@@ -1727,7 +1749,7 @@ yarn add babel-core --dev
This was done to support different major versions of babel.
-### Base webpack config now contains vital plugins ([#1775](https://github.com/storybookjs/storybook/pull/1775))
+### Base webpack config now contains vital plugins #1775
This affects you if you use custom webpack config in [Full Control Mode](https://storybook.js.org/docs/react/configure/webpack#full-control-mode) while not preserving the plugins from `storybookBaseConfig`. Before `3.3`, preserving them was a recommendation, but now it [became](https://github.com/storybookjs/storybook/pull/2578) a requirement.
diff --git a/addons/docs/src/mdx/__testfixtures__/loaders.mdx b/addons/docs/src/mdx/__testfixtures__/loaders.mdx
new file mode 100644
index 000000000000..3497a9bf25b6
--- /dev/null
+++ b/addons/docs/src/mdx/__testfixtures__/loaders.mdx
@@ -0,0 +1,10 @@
+import { Button } from '@storybook/react/demo';
+import { Story, Meta } from '@storybook/addon-docs/blocks';
+
+ ({ foo: 1 })]} />
+
+# Story with loader
+
+ ({ bar: 2 })]}>
+
+
diff --git a/addons/docs/src/mdx/__testfixtures__/loaders.output.snapshot b/addons/docs/src/mdx/__testfixtures__/loaders.output.snapshot
new file mode 100644
index 000000000000..4afb60896767
--- /dev/null
+++ b/addons/docs/src/mdx/__testfixtures__/loaders.output.snapshot
@@ -0,0 +1,85 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`docs-mdx-compiler-plugin loaders.mdx 1`] = `
+"/* @jsx mdx */
+import { assertIsFn, AddContext } from '@storybook/addon-docs/blocks';
+
+import { Button } from '@storybook/react/demo';
+import { Story, Meta } from '@storybook/addon-docs/blocks';
+
+const makeShortcode = (name) =>
+ function MDXDefaultShortcode(props) {
+ console.warn(
+ 'Component ' +
+ name +
+ ' was not imported, exported, or provided by MDXProvider as global scope'
+ );
+ return
+ ({
+ bar: 2,
+ }),
+ ]}
+ mdxType=\\"Story\\"
+ >
+
+
+
+ );
+}
+
+MDXContent.isMDXComponent = true;
+
+export const one = () => ;
+one.storyName = 'one';
+one.parameters = { storySource: { source: '' } };
+one.loaders = [
+ async () => ({
+ bar: 2,
+ }),
+];
+
+const componentMeta = {
+ title: 'Button',
+ loaders: [
+ async () => ({
+ foo: 1,
+ }),
+ ],
+ includeStories: ['one'],
+};
+
+const mdxStoryNameToKey = { one: 'one' };
+
+componentMeta.parameters = componentMeta.parameters || {};
+componentMeta.parameters.docs = {
+ ...(componentMeta.parameters.docs || {}),
+ page: () => (
+
+
+
+ ),
+};
+
+export default componentMeta;
+"
+`;
diff --git a/addons/docs/src/mdx/mdx-compiler-plugin.js b/addons/docs/src/mdx/mdx-compiler-plugin.js
index c8ac5b77dbb3..58de1a0a5d8e 100644
--- a/addons/docs/src/mdx/mdx-compiler-plugin.js
+++ b/addons/docs/src/mdx/mdx-compiler-plugin.js
@@ -193,6 +193,13 @@ function genStoryExport(ast, context) {
statements.push(`${storyKey}.decorators = ${decos};`);
}
+ let loaders = getAttr(ast.openingElement, 'loaders');
+ loaders = loaders && loaders.expression;
+ if (loaders) {
+ const { code: loaderCode } = generate(loaders, {});
+ statements.push(`${storyKey}.loaders = ${loaderCode};`);
+ }
+
// eslint-disable-next-line no-param-reassign
context.storyNameToKey[storyName] = storyKey;
@@ -242,6 +249,7 @@ function genMeta(ast, options) {
id = id && `'${id.value}'`;
const parameters = genAttribute('parameters', ast.openingElement);
const decorators = genAttribute('decorators', ast.openingElement);
+ const loaders = genAttribute('loaders', ast.openingElement);
const component = genAttribute('component', ast.openingElement);
const subcomponents = genAttribute('subcomponents', ast.openingElement);
const args = genAttribute('args', ast.openingElement);
@@ -252,6 +260,7 @@ function genMeta(ast, options) {
id,
parameters,
decorators,
+ loaders,
component,
subcomponents,
args,
diff --git a/addons/storyshots/storyshots-core/src/api/index.ts b/addons/storyshots/storyshots-core/src/api/index.ts
index b40273c8fa7b..601697ff89d4 100644
--- a/addons/storyshots/storyshots-core/src/api/index.ts
+++ b/addons/storyshots/storyshots-core/src/api/index.ts
@@ -48,39 +48,37 @@ function testStorySnapshots(options: StoryshotsOptions = {}) {
stories2snapsConverter,
};
- const data = storybook
- .raw()
- .reduce(
- (acc, item) => {
- if (storyNameRegex && !item.name.match(storyNameRegex)) {
- return acc;
- }
+ const data = storybook.raw().reduce(
+ (acc, item) => {
+ if (storyNameRegex && !item.name.match(storyNameRegex)) {
+ return acc;
+ }
- if (storyKindRegex && !item.kind.match(storyKindRegex)) {
- return acc;
- }
+ if (storyKindRegex && !item.kind.match(storyKindRegex)) {
+ return acc;
+ }
- const { kind, storyFn: render, parameters } = item;
- const existing = acc.find((i: any) => i.kind === kind);
- const { fileName } = item.parameters;
+ const { kind, storyFn: render, parameters } = item;
+ const existing = acc.find((i: any) => i.kind === kind);
+ const { fileName } = item.parameters;
- if (!isDisabled(parameters.storyshots)) {
- if (existing) {
- existing.children.push({ ...item, render, fileName });
- } else {
- acc.push({
- kind,
- children: [{ ...item, render, fileName }],
- });
- }
+ if (!isDisabled(parameters.storyshots)) {
+ if (existing) {
+ existing.children.push({ ...item, render, fileName });
+ } else {
+ acc.push({
+ kind,
+ children: [{ ...item, render, fileName }],
+ });
}
- return acc;
- },
- [] as {
- kind: string;
- children: any[];
- }[]
- );
+ }
+ return acc;
+ },
+ [] as {
+ kind: string;
+ children: any[];
+ }[]
+ );
if (data.length) {
callTestMethodGlobals(testMethod);
diff --git a/app/html/src/client/preview/render.ts b/app/html/src/client/preview/render.ts
index bf1ce2782703..28aa86ad927c 100644
--- a/app/html/src/client/preview/render.ts
+++ b/app/html/src/client/preview/render.ts
@@ -14,7 +14,6 @@ export default function renderMain({
forceRender,
}: RenderContext) {
const element = storyFn();
-
showMain();
if (typeof element === 'string') {
rootElement.innerHTML = element;
diff --git a/docs/snippets/common/component-story-custom-source.js.mdx b/docs/snippets/common/component-story-custom-source.js.mdx
index 107466a8ca41..317c8d5a3d66 100644
--- a/docs/snippets/common/component-story-custom-source.js.mdx
+++ b/docs/snippets/common/component-story-custom-source.js.mdx
@@ -4,10 +4,10 @@
export const CustomSource = () => Template.bind({});
CustomSource.parameters = {
- docs: {
- source: {
- code: 'Some custom string here'
- }
+ docs: {
+ source: {
+ code: 'Some custom string here',
},
+ },
};
```
diff --git a/docs/snippets/react/loader-story.js.mdx b/docs/snippets/react/loader-story.js.mdx
new file mode 100644
index 000000000000..784d236364d3
--- /dev/null
+++ b/docs/snippets/react/loader-story.js.mdx
@@ -0,0 +1,14 @@
+```js
+// TodoItem.stories.js
+
+import React from 'react';
+import fetch from 'node-fetch';
+import { TodoItem } from './TodoItem';
+
+export const Primary = (args, { loaded: { todo } }) => ;
+Primary.loaders = [
+ async () => ({
+ todo: (await fetch('https://jsonplaceholder.typicode.com/todos/1')).json(),
+ }),
+];
+```
diff --git a/docs/snippets/react/storybook-preview-global-loader.js.mdx b/docs/snippets/react/storybook-preview-global-loader.js.mdx
new file mode 100644
index 000000000000..50a293ffac81
--- /dev/null
+++ b/docs/snippets/react/storybook-preview-global-loader.js.mdx
@@ -0,0 +1,12 @@
+```js
+// .storybook/preview.js
+
+import React from 'react';
+import fetch from 'node-fetch';
+
+export const loaders = [
+ async () => ({
+ currentUser: (await fetch('https://jsonplaceholder.typicode.com/users/1')).json(),
+ }),
+];
+```
diff --git a/docs/toc.js b/docs/toc.js
index 65b69f6a926e..edfdef628376 100644
--- a/docs/toc.js
+++ b/docs/toc.js
@@ -73,6 +73,11 @@ module.exports = {
title: 'Decorators',
type: 'link',
},
+ {
+ pathSegment: 'loaders',
+ title: 'Loaders',
+ type: 'link',
+ },
{
pathSegment: 'naming-components-and-hierarchy',
title: 'Naming components and hierarchy',
diff --git a/docs/writing-stories/loaders.md b/docs/writing-stories/loaders.md
new file mode 100644
index 000000000000..84c31a6cfd70
--- /dev/null
+++ b/docs/writing-stories/loaders.md
@@ -0,0 +1,63 @@
+---
+title: 'Loaders'
+---
+
+Loaders are asynchronous functions that load data for a story and its [decorators](./decorators.md). A story's loaders run before the story renders, and the loaded data is passed into the story via its render context.
+
+Loaders can be used to load any asset (e.g. lazy-loaded components), but they are are typically used to fetch remote API data to be used in a story.
+
+> NOTE: [Args](./args.md) are the recommended way to manage story data, and we're building up an ecosystem of tools and techniques around them. Loaders are an advanced feature ("escape hatch") and we only recommend using them if you have a specific need that can't be fulfilled by other means.
+
+## Fetching API data
+
+Stories are isolated component examples that render internal data that's defined as part of the story or alongside the story as [args](./args.md).
+
+Loaders are useful when you need to load story data externally, e.g. from a remote API. Consider the following example that fetches a todo item for display in a todo list:
+
+
+
+
+
+
+
+The loaded data is combined into a `loaded` field on the story context, which is the second argument to a story function. In this example we spread the story's args in first, so they take priority over the static data provided by the loader.
+
+## Global loaders
+
+We can also set a loader for **all stories** via the `loaders` export of your [`.storybook/preview.js`](../configure/overview.md#configure-story-rendering) file (this is the file where you configure all stories):
+
+
+
+
+
+
+
+In this example, we load a "current user" that is available as `loaded.currentUser` for all stories.
+
+## Loader inheritance
+
+Like parameters, loaders can be defined globally, at the component level and for a single story (as we’ve seen).
+
+All loaders, defined at all levels that apply to a story, run before the story is rendered.
+
+- All loaders run in parallel
+- All results are the `loaded` field in the story context
+- If there are keys that overlap, "later" loaders take precedence (from lowest to highest):
+ - Global loaders, in the order they are defined
+ - Component loaders, in the order they are defined
+ - Story loaders, in the order they are defined
+
+## Known limitations
+
+Loaders have the following known limitations:
+
+- They are not yet compatible with the storyshots addon ([#12703](https://github.com/storybookjs/storybook/issues/12703)).
+- They are not yet compatible with inline-rendered stories in Storybook Docs ([#12726](https://github.com/storybookjs/storybook/issues/12726)).
diff --git a/examples/html-kitchen-sink/stories/__snapshots__/loaders.stories.storyshot b/examples/html-kitchen-sink/stories/__snapshots__/loaders.stories.storyshot
new file mode 100644
index 000000000000..91af9532caf8
--- /dev/null
+++ b/examples/html-kitchen-sink/stories/__snapshots__/loaders.stories.storyshot
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Storyshots Core/Loaders Story 1`] = `
+