Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- [Internal CSF tools use the unified mutation API](#internal-csf-tools-use-the-unified-mutation-api)
- [Internal WebSocket heartbeat controls removed](#internal-websocket-heartbeat-controls-removed)
- [Internal toolset telemetry now returns with the outcome](#internal-toolset-telemetry-now-returns-with-the-outcome)
- [React: Require v18 and up](#react-require-v18-and-up)

- [From version 10.5.x to 10.6.0](#from-version-105x-to-1060)
- [Vue 3: `vue-docgen-api` is deprecated](#vue-3-vue-docgen-api-is-deprecated)
Expand Down Expand Up @@ -649,6 +650,7 @@ If you're using framework-specific Vite plugins, ensure they are compatible with

For more information on upgrading Vite, see the [Vite Migration Guide](https://vite.dev/guide/migration).


### Next.js: Require v15 and up

Storybook has dropped support for Next.js versions below 15. The minimum supported version is now Next.js 15.
Expand Down Expand Up @@ -750,6 +752,16 @@ If you implement toolsets using Storybook's internal open-service APIs, return u

Custom SDK callers must remove the `telemetry` callback from `ToolsCallOptions`. The `toolsCommandDimensions` and `wrapMethodTelemetry` helpers are no longer exported from `storybook/internal/tools`. The CLI and MCP adapters handle reporting for their own calls.

### React: Require v18 and up

Storybook now requires React 18 or newer. The `react` and `react-dom` peer dependency ranges of all React-based framework packages are now `^18.0.0 || ^19.0.0`, so projects on React 16 or 17 must upgrade React before upgrading Storybook.

Storybook renders through React's new root API (`react-dom/client`), which React 18 introduced and React 19 requires. The `legacyRootApi` framework option of `@storybook/react-vite` and `@storybook/react-webpack5` has been removed along with the `@storybook/react-dom-shim/react-16` compatibility export, so there is no longer a way to opt out. If you had set `legacyRootApi: true` to ease a React 18 migration, follow [React's upgrade guide](https://react.dev/blog/2022/03/08/react-18-upgrade-guide) for your application code instead.

`storybook upgrade` blocks the upgrade when it detects an unsupported `react` or `react-dom` version and links to this section. Upgrade React to 18 or 19 and run the upgrade again.

Remove `framework.options.legacyRootApi` from `.storybook/main.*`, whether its value is `true` or `false`. `storybook upgrade` blocks the upgrade while the option is still present and links to this section. This is a manual migration: `storybook upgrade` does not remove the option or migrate application code to the new root API. Projects that enabled the legacy root must verify their stories with the new root API before upgrading; automatically deleting the option cannot establish that their components support the changed rendering behavior.

## From version 10.5.x to 10.6.0

### Vue 3: `vue-docgen-api` is deprecated
Expand Down
8 changes: 4 additions & 4 deletions code/addons/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,16 @@
"@mdx-js/react": "^3.0.0",
"@storybook/icons": "^2.0.2",
"@storybook/react-dom-shim": "workspace:*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"ts-dedent": "^2.0.0",
"unplugin": "^2.3.5"
},
"devDependencies": {
"@mdx-js/mdx": "^3.0.0",
"@rollup/pluginutils": "^5.0.2",
"@types/color-convert": "^2.0.0",
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"@types/react": "^18.0.0 || ^19.0.0",
"color-convert": "^2.0.1",
"es-toolkit": "^1.43.0",
"github-slugger": "^2.0.0",
Expand All @@ -117,7 +117,7 @@
"vite": "^7.0.4"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"@types/react": "^18.0.0 || ^19.0.0",
"storybook": "workspace:^"
},
"peerDependenciesMeta": {
Expand Down
4 changes: 2 additions & 2 deletions code/addons/links/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@
"typescript": "^6.0.3"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"@types/react": "^18.0.0 || ^19.0.0",
"react": "^18.0.0 || ^19.0.0",
"storybook": "workspace:^"
},
"peerDependenciesMeta": {
Expand Down
2 changes: 1 addition & 1 deletion code/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@
"zod": "^3.25.76"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"@types/react": "^18.0.0 || ^19.0.0",
"prettier": "^2 || ^3",
"vite-plus": "^0.1.15 || ^0.2.0"
},
Expand Down
6 changes: 1 addition & 5 deletions code/e2e-sandbox/addon-docs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,7 @@ test.describe('addon-docs', () => {

// Arrange - Setup expectations
let expectedReactVersionRange = /^19/;
if (templateName.includes('react-webpack/17') || templateName.includes('react-vite/17')) {
expectedReactVersionRange = /^17/;
} else if (templateName.includes('react16')) {
expectedReactVersionRange = /^16/;
} else if (templateName.includes('preact-vite/prerelease')) {
if (templateName.includes('preact-vite/prerelease')) {
expectedReactVersionRange = /^18/;
} else if (
templateName.includes('internal/react18-webpack-babel') ||
Expand Down
4 changes: 2 additions & 2 deletions code/frameworks/ember/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@
"babel-plugin-ember-modules-api-polyfill": "^3.5.0",
"babel-plugin-htmlbars-inline-precompile": "^5.3.1",
"ember-source": "~3.28.1 || >=4.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"storybook": "workspace:^"
},
"publishConfig": {
Expand Down
8 changes: 4 additions & 4 deletions code/frameworks/nextjs-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,11 @@
"typescript": "^6.0.3"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"@types/react": "^18.0.0 || ^19.0.0",
"@types/react-dom": "^18.0.0 || ^19.0.0",
"next": "^15.0.0 || ^16.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"storybook": "workspace:^",
"vite": "^6.3.0 || ^7.0.0 || ^8.0.0"
},
Expand Down
8 changes: 4 additions & 4 deletions code/frameworks/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,11 @@
"webpack": "^5.65.0"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"@types/react": "^18.0.0 || ^19.0.0",
"@types/react-dom": "^18.0.0 || ^19.0.0",
"next": "^15.0.0 || ^16.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"storybook": "workspace:^",
"webpack": "^5.0.0"
},
Expand Down
4 changes: 2 additions & 2 deletions code/frameworks/react-native-web-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@
"typescript": "^6.0.3"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"react-native": ">=0.74.5",
"react-native-web": "^0.19.12 || ^0.20.0 || ^0.21.0",
"storybook": "workspace:^",
Expand Down
4 changes: 2 additions & 2 deletions code/frameworks/react-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@
"vite": "^7.0.4"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"storybook": "workspace:^",
"typescript": ">= 4.9.x",
"vite": "^6.3.0 || ^7.0.0 || ^8.0.0"
Expand Down
10 changes: 0 additions & 10 deletions code/frameworks/react-vite/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,6 @@ type BuilderName = CompatibleString<'@storybook/builder-vite'>;
export type FrameworkOptions = {
builder?: BuilderOptions;
strictMode?: boolean;
/**
* Use React's legacy root API to mount components
*
* React has introduced a new root API with React 18.x to enable a whole set of new features (e.g.
* concurrent features) If this flag is true, the legacy Root API is used to mount components to
* make it easier to migrate step by step to React 18.
*
* @default false
*/
legacyRootApi?: boolean;
};

type StorybookConfigFramework = {
Expand Down
4 changes: 2 additions & 2 deletions code/frameworks/react-webpack5/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@
"@types/node": "^22.19.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"storybook": "workspace:^",
"typescript": ">= 4.9.x"
},
Expand Down
4 changes: 2 additions & 2 deletions code/frameworks/tanstack-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@
"@tanstack/react-start": "^1.167.16",
"@tanstack/router-core": "^1.168.9",
"@tanstack/start-client-core": "^1.167.9",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"storybook": "workspace:^",
"vite": "^6.3.0 || ^7.0.0 || ^8.0.0"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,4 +233,67 @@ describe('dependenciesVersions blocker', () => {

expect(logged.title).toBe('react-scripts version < 5.0.0 support removed');
});

test('blocks on react 17 with a message linking the migration guide', async () => {
const packageManager = createPackageManager({ react: '17.0.0', 'react-dom': '17.0.0' });

const result = await blocker.check(createCheckOptions(packageManager));

expect(result).toEqual({
packageName: 'react',
installedVersion: '17.0.0',
minimumVersion: '18.0.0',
});

if (!result) {
throw new Error('Expected the blocker to block on react 17');
}

const logged = blocker.log(result);

expect(logged.title).toBe('React 18 or newer required');
expect(logged.message).toContain('Support for React < 18 has been removed');
expect(logged.link).toBe(
'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#react-require-v18-and-up'
);
});

test('blocks on react-dom 17 when react itself is current', async () => {
const packageManager = createPackageManager({ react: '18.2.0', 'react-dom': '17.0.2' });

const result = await blocker.check(createCheckOptions(packageManager));

expect(result).toEqual({
packageName: 'react-dom',
installedVersion: '17.0.2',
minimumVersion: '18.0.0',
});
});

test('passes on react 18', async () => {
const packageManager = createPackageManager({ react: '18.2.0', 'react-dom': '18.2.0' });

const result = await blocker.check(createCheckOptions(packageManager));

expect(result).toBe(false);
});

test('passes on react 19', async () => {
const packageManager = createPackageManager({ react: '19.0.0', 'react-dom': '19.0.0' });

const result = await blocker.check(createCheckOptions(packageManager));

expect(result).toBe(false);
});

test('passes on react 0.0.0 experimental builds, matching the shim treatment', async () => {
const packageManager = createPackageManager({
react: '0.0.0-experimental-4cb07d14-20250411',
'react-dom': '0.0.0-experimental-4cb07d14-20250411',
});

const result = await blocker.check(createCheckOptions(packageManager));

expect(result).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const minimalVersionsMap = {
'@angular/core': '21.0.0',
next: '15.0.0',
preact: '10.0.0',
react: '18.0.0',
Comment thread
valentinpalkovic marked this conversation as resolved.
'react-dom': '18.0.0',
svelte: '5.0.0',
vue: '3.0.0',
vite: '5.0.0',
Expand All @@ -23,6 +25,16 @@ export const blocker = createBlocker({
packageManager,
});

// React experimental/canary builds (0.0.0*) ship react-dom/client and are treated as
// React 18+ by the react-dom-shim, so their version string must not block the upgrade.
if (
outdated &&
(outdated.packageName === 'react' || outdated.packageName === 'react-dom') &&
outdated.installedVersion?.startsWith('0.0.0')
) {
return false;
}

if (outdated !== false) {
return outdated;
}
Expand Down Expand Up @@ -82,6 +94,16 @@ export const blocker = createBlocker({
`,
link: 'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#vitest-addon-requires-vitest-40-or-higher',
};
case 'react':
case 'react-dom':
return {
title: 'React 18 or newer required',
message: dedent`
Support for React < 18 has been removed.
Please see the migration guide for more information:
`,
link: 'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#react-require-v18-and-up',
};
default:
return {
title: `${data.packageName} version < ${data.minimumVersion} support removed`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, expect, test } from 'vitest';

import type { StorybookConfig } from 'storybook/internal/types';

import { blocker } from './block-react-legacy-root-api.ts';

const check = (framework: StorybookConfig['framework']) =>
blocker.check({
packageManager: {} as any,
mainConfig: { stories: [], framework } as StorybookConfig,
mainConfigPath: '.storybook/main.ts',
configDir: '.storybook',
});

describe('blocks', () => {
test('when legacyRootApi is true', async () => {
await expect(
check({ name: '@storybook/react-vite', options: { legacyRootApi: true } })
).resolves.toBe(true);
});

// The option no longer exists, so an explicit `false` must be removed as well.
test('when legacyRootApi is false', async () => {
await expect(
check({ name: '@storybook/react-vite', options: { legacyRootApi: false } })
).resolves.toBe(true);
});

test('when legacyRootApi is set on react-webpack5', async () => {
await expect(
check({ name: '@storybook/react-webpack5', options: { legacyRootApi: true } })
).resolves.toBe(true);
});
});

describe('does not block', () => {
test('when framework has no options', async () => {
await expect(check({ name: '@storybook/react-vite' })).resolves.toBe(false);
});

test('when options omit legacyRootApi', async () => {
await expect(check({ name: '@storybook/react-vite', options: { builder: {} } })).resolves.toBe(
false
);
});

test('when framework is a bare string', async () => {
await expect(check('@storybook/react-vite')).resolves.toBe(false);
});

test('when framework is undefined', async () => {
await expect(check(undefined)).resolves.toBe(false);
});
});

describe('log', () => {
test('names the option, both values, and links React upgrade guidance', () => {
const { title, message, link } = blocker.log(true);

expect(title).toBe('React legacy root API removed');
expect(message).toContain('legacyRootApi');
expect(message).toContain('`true` or `false`');
expect(message).toContain('https://react.dev/blog/2022/03/08/react-18-upgrade-guide');
expect(link).toBe(
'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#react-require-v18-and-up'
);
});
});
Loading
Loading