Skip to content
33 changes: 33 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [`parameters.componentSubtitle` removed](#parameterscomponentsubtitle-removed)
- [Raised browser support floors](#raised-browser-support-floors)
- [Docs Code panel enabled by default](#docs-code-panel-enabled-by-default)
- [`argTypes` removed from loaders, `beforeEach`, `play` and `afterEach`](#argtypes-removed-from-loaders-beforeeach-play-and-aftereach)
- [Node.js 22.12 or higher](#nodejs-2212-or-higher)
- [TypeScript 5.9 or 6.x](#typescript-59-or-6x)
- [Yarn PnP support removed](#yarn-pnp-support-removed)
Expand Down Expand Up @@ -653,6 +654,38 @@ You can also set this parameter at the component or story level. An explicit `tr

No automigration is needed. Existing boolean settings retain their meaning, and projects with no setting receive the new default.

### `argTypes` removed from loaders, `beforeEach`, `play` and `afterEach`

The story context passed to loaders, `beforeEach`, `play`, `afterEach` and `step` callbacks no longer contains `argTypes`. Reading it throws an error that links here.

With server-side docgen (`features.experimentalDocgenServer`), the preview no longer infers arg types from components or args. `context.argTypes` in these hooks only ever contained the arg types you declared by hand, so it looked complete but was not.

```ts
// Before
export const Primary: Story = {
play: async ({ argTypes, args }) => {
for (const name of Object.keys(argTypes)) {
// ...
}
},
};

// After
export const Primary: Story = {
play: async ({ args }) => {
for (const name of Object.keys(args)) {
// ...
}
},
};
```

- Use `args` for the values passed to the story. Iterate `Object.keys(args)` instead of `Object.keys(argTypes)`.
- To see the resolved arg types, including those inferred from your component, use the Controls panel or the `ArgTypes` doc block.
- In portable stories, `composeStory(Story, meta).argTypes` still exposes the story's declared arg types outside of the lifecycle hooks.

Decorators and `render` functions keep receiving `argTypes`, because renderers rely on them while rendering. Their context type is the new `StoryContextForRender`; the `StoryContext` type no longer declares `argTypes`. Custom decorator or render helpers that annotate their context parameter as `StoryContext` and read `argTypes` should switch to `StoryContextForRender`.

### Node.js 22.12 or higher

Storybook 11 targets Node.js 22.12 or higher. Before upgrading, update Node.js in your local development environment, CI jobs, and deployment environments that build Storybook. Update any Node.js version pins, such as `.nvmrc`, `.node-version`, or your CI configuration.
Expand Down
26 changes: 13 additions & 13 deletions code/core/src/actions/addArgsHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';

import type { StoryContext } from 'storybook/internal/types';
import type { StoryContextForEnhancers } from 'storybook/internal/types';

import { addActionsFromArgTypes, inferActionsFromArgTypesRegex } from './addArgsHelpers.ts';

Expand All @@ -14,7 +14,7 @@ describe('actions parameter enhancers', () => {
initialArgs: {},
argTypes,
parameters,
} as unknown as StoryContext);
} as unknown as StoryContextForEnhancers);
expect(args).toEqual({
onClick: expect.any(Function),
onFocus: expect.any(Function),
Expand All @@ -26,7 +26,7 @@ describe('actions parameter enhancers', () => {
initialArgs: { onClick: 'pre-existing value' },
argTypes,
parameters,
} as unknown as StoryContext);
} as unknown as StoryContextForEnhancers);
expect(args).toEqual({ onFocus: expect.any(Function) });
});

Expand All @@ -35,7 +35,7 @@ describe('actions parameter enhancers', () => {
initialArgs: { onClick: null },
argTypes,
parameters,
} as unknown as StoryContext);
} as unknown as StoryContextForEnhancers);
expect(args).toEqual({ onFocus: expect.any(Function) });
});

Expand All @@ -44,7 +44,7 @@ describe('actions parameter enhancers', () => {
initialArgs: {},
argTypes,
parameters,
} as unknown as StoryContext);
} as unknown as StoryContextForEnhancers);
expect(args).toEqual({ onClick: expect.any(Function), onFocus: expect.any(Function) });
});

Expand All @@ -53,7 +53,7 @@ describe('actions parameter enhancers', () => {
initialArgs: { onClick: undefined },
argTypes,
parameters,
} as unknown as StoryContext);
} as unknown as StoryContextForEnhancers);
expect(args).toEqual({ onClick: undefined, onFocus: expect.any(Function) });
});

Expand All @@ -65,7 +65,7 @@ describe('actions parameter enhancers', () => {
...parameters,
actions: { ...parameters.actions, disable: true },
},
} as unknown as StoryContext);
} as unknown as StoryContextForEnhancers);
expect(args).toEqual({});
});
});
Expand All @@ -81,7 +81,7 @@ describe('actions parameter enhancers', () => {
initialArgs: {},
argTypes,
parameters: {},
} as unknown as StoryContext)
} as unknown as StoryContextForEnhancers)
).toEqual({
onClick: expect.any(Function),
onBlur: expect.any(Function),
Expand All @@ -94,7 +94,7 @@ describe('actions parameter enhancers', () => {
argTypes: { onClick: { action: 'clicked!' } },
initialArgs: { onClick: 'pre-existing value' },
parameters: {},
} as unknown as StoryContext)
} as unknown as StoryContextForEnhancers)
).toEqual({});
});

Expand All @@ -104,7 +104,7 @@ describe('actions parameter enhancers', () => {
argTypes: { onClick: { action: 'clicked!' } },
initialArgs: { onClick: null },
parameters: {},
} as unknown as StoryContext)
} as unknown as StoryContextForEnhancers)
).toEqual({});
});

Expand All @@ -114,7 +114,7 @@ describe('actions parameter enhancers', () => {
argTypes: { onClick: { action: 'clicked!' } },
initialArgs: {},
parameters: {},
} as unknown as StoryContext)
} as unknown as StoryContextForEnhancers)
).toEqual({ onClick: expect.any(Function) });
});

Expand All @@ -124,7 +124,7 @@ describe('actions parameter enhancers', () => {
argTypes: { onClick: { action: 'clicked!' } },
initialArgs: { onClick: undefined },
parameters: {},
} as unknown as StoryContext)
} as unknown as StoryContextForEnhancers)
).toEqual({ onClick: undefined });
});

Expand All @@ -134,7 +134,7 @@ describe('actions parameter enhancers', () => {
initialArgs: {},
argTypes,
parameters: { actions: { disable: true } },
} as unknown as StoryContext)
} as unknown as StoryContextForEnhancers)
).toEqual({});
});
});
Expand Down
31 changes: 26 additions & 5 deletions code/core/src/csf/story.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,12 +270,20 @@ export type AfterEach<TRenderer extends Renderer = Renderer, TArgs = Args> = (
export interface Canvas extends BoundFunctions<typeof queries> {}

export interface StoryContext<TRenderer extends Renderer = Renderer, TArgs = Args>
extends StoryContextForEnhancers<TRenderer, TArgs>, Required<StoryContextUpdate<TArgs>> {
extends
Omit<StoryContextForEnhancers<TRenderer, TArgs>, 'argTypes'>,
Required<StoryContextUpdate<TArgs>> {
Comment thread
ndelangen marked this conversation as resolved.
loaded: Record<string, any>;
abortSignal: AbortSignal;
canvasElement: TRenderer['canvasElement'];
hooks: unknown;
originalStoryFn: ArgsStoryFn<TRenderer>;
// Declared as a method rather than `originalStoryFn: ArgsStoryFn<TRenderer>`: as a property, the
// context parameter would make StoryContext invariant in TRenderer, so a generic
// PlayFunction<Renderer> could no longer be assigned to a framework-specific story.
originalStoryFn(
args: Args,
context: StoryContextForRender<TRenderer>
): (TRenderer & { T: Args })['storyResult'];
viewMode: ViewMode;
step: StepFunction<TRenderer, TArgs>;
context: this;
Expand All @@ -285,6 +293,19 @@ export interface StoryContext<TRenderer extends Renderer = Renderer, TArgs = Arg
reporting: ReportingAPI;
}

/**
* The story context as decorators and render functions receive it.
*
* Unlike the context passed to loaders, `beforeEach`, `play` and `afterEach`, it carries the
* story's `argTypes`, because renderers read them while rendering.
*/
export interface StoryContextForRender<
TRenderer extends Renderer = Renderer,
TArgs = Args,
> extends StoryContext<TRenderer, TArgs> {
argTypes: StrictArgTypes<TArgs>;
}

/** @deprecated Use {@link StoryContext} instead. */
export interface StoryContextForLoaders<
TRenderer extends Renderer = Renderer,
Expand Down Expand Up @@ -319,13 +340,13 @@ export type PartialStoryFn<TRenderer extends Renderer = Renderer, TArgs = Args>

// This is a passArgsFirst: false user story function
export type LegacyStoryFn<TRenderer extends Renderer = Renderer, TArgs = Args> = (
context: StoryContext<TRenderer, TArgs>
context: StoryContextForRender<TRenderer, TArgs>
) => TRenderer['storyResult'];

// This is a passArgsFirst: true user story function
export type ArgsStoryFn<TRenderer extends Renderer = Renderer, TArgs = Args> = (
args: TArgs,
context: StoryContext<TRenderer, TArgs>
context: StoryContextForRender<TRenderer, TArgs>
) => (TRenderer & { T: TArgs })['storyResult'];

// This is either type of user story function
Expand All @@ -335,7 +356,7 @@ export type StoryFn<TRenderer extends Renderer = Renderer, TArgs = Args> =

export type DecoratorFunction<TRenderer extends Renderer = Renderer, TArgs = Args> = (
fn: PartialStoryFn<TRenderer, TArgs>,
c: StoryContext<TRenderer, TArgs>
c: StoryContextForRender<TRenderer, TArgs>
) => TRenderer['storyResult'];

export type DecoratorApplicator<TRenderer extends Renderer = Renderer, TArgs = Args> = (
Expand Down
6 changes: 3 additions & 3 deletions code/core/src/measure/withMeasure.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @vitest-environment happy-dom
import { beforeEach, describe, expect, it, vi } from 'vitest';

import type { StoryContext } from 'storybook/internal/types';
import type { StoryContextForRender } from 'storybook/internal/types';

vi.mock('storybook/preview-api', () => ({
useEffect: (cb: () => void) => {
Expand All @@ -21,11 +21,11 @@ vi.mock('./box-model/visualizer.ts', () => ({
import * as canvas from './box-model/canvas.ts';
import { withMeasure } from './withMeasure.ts';

const runDecorator = (context: Partial<StoryContext>) =>
const runDecorator = (context: Partial<StoryContextForRender>) =>
withMeasure(() => 'story', {
viewMode: 'story',
...context,
} as StoryContext);
} as StoryContextForRender);

describe('withMeasure', () => {
beforeEach(() => {
Expand Down
6 changes: 3 additions & 3 deletions code/core/src/outline/withOutline.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @vitest-environment happy-dom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import type { StoryContext } from 'storybook/internal/types';
import type { StoryContextForRender } from 'storybook/internal/types';

vi.mock('storybook/preview-api', () => ({
useEffect: (cb: () => void) => {
Expand All @@ -14,12 +14,12 @@ import { withOutline } from './withOutline.ts';

const OUTLINE_STYLE_ID = 'addon-outline';

const runDecorator = (context: Partial<StoryContext>) =>
const runDecorator = (context: Partial<StoryContextForRender>) =>
withOutline(() => 'story', {
viewMode: 'story',
id: 'test--story',
...context,
} as StoryContext);
} as StoryContextForRender);

describe('withOutline', () => {
beforeEach(() => {
Expand Down
19 changes: 3 additions & 16 deletions code/core/src/preview-api/modules/preview-web/PreviewWeb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
UPDATE_GLOBALS,
UPDATE_STORY_ARGS,
} from 'storybook/internal/core-events';
import { ArgTypesRemovedFromStoryContextError } from 'storybook/internal/preview-errors';
import type { ModuleImportFn, ProjectAnnotations, Renderer } from 'storybook/internal/types';

import { global } from '@storybook/global';
Expand Down Expand Up @@ -505,13 +506,11 @@ describe('PreviewWeb', () => {
throwPlayFunctionExceptions: false,
},
initialArgs: { foo: 'a', one: 1 },
argTypes: {
foo: { name: 'foo', type: { name: 'string' } },
one: { name: 'one', type: { name: 'string' }, mapping: { 1: 'mapped-1' } },
},
args: { foo: 'a', one: 'mapped-1' },
})
);
const loaderContext = componentOneExports.default.loaders[0].mock.calls[0][0];
expect(() => loaderContext.argTypes).toThrow(ArgTypesRemovedFromStoryContextError);
});

it('passes loaded context to renderToCanvas', async () => {
Expand Down Expand Up @@ -2169,10 +2168,6 @@ describe('PreviewWeb', () => {
fileName: './src/ComponentOne.stories.js',
}),
initialArgs: { foo: 'b', one: 1 },
argTypes: {
foo: { name: 'foo', type: { name: 'string' } },
one: { name: 'one', type: { name: 'string' }, mapping: { 1: 'mapped-1' } },
},
args: { foo: 'b', one: 'mapped-1' },
})
);
Expand Down Expand Up @@ -2749,10 +2744,6 @@ describe('PreviewWeb', () => {
fileName: './src/ComponentOne.stories.js',
}),
initialArgs: { foo: 'a', one: 1 },
argTypes: {
foo: { name: 'foo', type: { name: 'string' } },
one: { name: 'one', type: { name: 'string' }, mapping: { 1: 'mapped-1' } },
},
args: { foo: 'a', one: 'mapped-1' },
})
);
Expand Down Expand Up @@ -3094,10 +3085,6 @@ describe('PreviewWeb', () => {
throwPlayFunctionExceptions: false,
},
initialArgs: { foo: 'edited', one: 1 },
argTypes: {
foo: { name: 'foo', type: { name: 'string' } },
one: { name: 'one', type: { name: 'string' }, mapping: { 1: 'mapped-1' } },
},
args: { foo: 'edited', one: 'mapped-1' },
})
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';

import { Channel } from 'storybook/internal/channels';
import { STORY_FINISHED } from 'storybook/internal/core-events';
import { ArgTypesRemovedFromStoryContextError } from 'storybook/internal/preview-errors';
import type {
PreparedStory,
Renderer,
Expand Down Expand Up @@ -166,6 +167,53 @@ describe('StoryRender', () => {
expect(mountSpy).toHaveBeenCalledOnce();
});

it('hides argTypes from lifecycle hooks but passes them to the renderer', async () => {
const argTypes = { label: { name: 'label' } };
const hookContexts: StoryContext[] = [];
const story = buildStory({
applyLoaders: vi.fn(async (context) => {
hookContexts.push(context);
return {};
}),
applyBeforeEach: vi.fn(async (context) => {
hookContexts.push(context);
return [];
}),
playFunction: vi.fn(async (context) => {
hookContexts.push(context);
await context.step('step', async (stepContext: StoryContext) => {
hookContexts.push(stepContext);
});
}),
applyAfterEach: vi.fn(async (context) => {
hookContexts.push(context);
}),
runStep: vi.fn((label, play, context) => play(context)),
});
const renderToScreen = vi.fn();
const render = new StoryRender(
new Channel({}),
buildStore({
getStoryContext: () => ({ argTypes, reporting: new ReporterAPI() }) as any,
}),
renderToScreen,
{} as any,
entry.id,
'story',
{ autoplay: true },
story
);

await render.renderToElement({} as any);

expect(hookContexts).toHaveLength(5);
for (const context of hookContexts) {
expect(() => context.argTypes).toThrow(ArgTypesRemovedFromStoryContextError);
expect('argTypes' in context).toBe(false);
}
expect(renderToScreen.mock.calls[0][0].storyContext.argTypes).toBe(argTypes);
});

it('does not call mount twice if mount called in play function', async () => {
const story = buildStory({
usesMount: true,
Expand Down
Loading
Loading