diff --git a/code/frameworks/nextjs/README.md b/code/frameworks/nextjs/README.md index 48fb459db3ab..5875aa6b92be 100644 --- a/code/frameworks/nextjs/README.md +++ b/code/frameworks/nextjs/README.md @@ -40,6 +40,7 @@ - [Runtime Config](#runtime-config) - [Custom Webpack Config](#custom-webpack-config) - [Typescript](#typescript) + - [Experimental React Server Components (RSC)](#experimental-react-server-components-rsc) - [Notes for Yarn v2 and v3 users](#notes-for-yarn-v2-and-v3-users) - [FAQ](#faq) - [Stories for pages/components which fetch data](#stories-for-pagescomponents-which-fetch-data) @@ -908,6 +909,39 @@ Storybook handles most [Typescript](https://www.typescriptlang.org/) configurati } ``` +### Experimental React Server Components (RSC) + +If your app uses [React Server Components (RSC)](https://nextjs.org/docs/app/building-your-application/rendering/server-components), Storybook can render them in stories in the browser. + +To enable this set the `experimentalNextRSC` feature flag in your `.storybook/main.js` config: + +```js +// main.js +export default { + features: { + experimentalNextRSC: true, + }, +}; +``` + +Setting this flag automatically wraps your story in a [Suspense](https://react.dev/reference/react/Suspense) wrapper, which is able to render asynchronous components in NextJS's version of React. + +If this wrapper causes problems in any of your existing stories, you can selectively disable it using the `nextjs.rsc` [parameter](https://storybook.js.org/docs/writing-stories/parameters) at the global/component/story level: + +```js +// MyServerComponent.stories.js +export default { + component: MyServerComponent, + parameters: { nextjs: { rsc: false } }, +}; +``` + +Note that wrapping your server components in Suspense does not help if your server components access server-side resources like the file system or Node-specific libraries. To deal work around this, you'll need to mock out your data access layer using [Webpack aliases](https://webpack.js.org/configuration/resolve/#resolvealias) or an addon like [storybook-addon-module-mock](https://storybook.js.org/addons/storybook-addon-module-mock). + +If your server components access data via the network, we recommend using the [MSW Storybook Addon](https://storybook.js.org/addons/msw-storybook-addon) to mock network requests. + +In the future we will provide better mocking support in Storybook and support for [Server Actions](https://nextjs.org/docs/app/api-reference/functions/server-actions). + ### Notes for Yarn v2 and v3 users If you're using [Yarn](https://yarnpkg.com/) v2 or v3, you may run into issues where Storybook can't resolve `style-loader` or `css-loader`. For example, you might get errors like: diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json index c673b0de812e..46fd2faac332 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -42,6 +42,7 @@ "import": "./dist/font/webpack/loader/storybook-nextjs-font-loader.mjs" }, "./dist/preview.mjs": "./dist/preview.mjs", + "./dist/previewRSC.mjs": "./dist/previewRSC.mjs", "./next-image-loader-stub.js": { "types": "./dist/next-image-loader-stub.d.ts", "require": "./dist/next-image-loader-stub.js", @@ -152,6 +153,7 @@ "./src/index.ts", "./src/preset.ts", "./src/preview.tsx", + "./src/previewRSC.tsx", "./src/next-image-loader-stub.ts", "./src/images/decorator.tsx", "./src/images/next-legacy-image.tsx", diff --git a/code/frameworks/nextjs/src/preset.ts b/code/frameworks/nextjs/src/preset.ts index c12ed31877dd..504425df0297 100644 --- a/code/frameworks/nextjs/src/preset.ts +++ b/code/frameworks/nextjs/src/preset.ts @@ -65,10 +65,17 @@ export const core: PresetProperty<'core'> = async (config, options) => { }; }; -export const previewAnnotations: PresetProperty<'previewAnnotations'> = (entry = []) => [ - ...entry, - join(dirname(require.resolve('@storybook/nextjs/package.json')), 'dist/preview.mjs'), -]; +export const previewAnnotations: PresetProperty<'previewAnnotations'> = ( + entry = [], + { features } +) => { + const nextDir = dirname(require.resolve('@storybook/nextjs/package.json')); + const result = [...entry, join(nextDir, 'dist/preview.mjs')]; + if (features?.experimentalNextRSC) { + result.unshift(join(nextDir, 'dist/previewRSC.mjs')); + } + return result; +}; // Not even sb init - automigrate - running dev // You're using a version of Nextjs prior to v10, which is unsupported by this framework. diff --git a/code/frameworks/nextjs/src/previewRSC.tsx b/code/frameworks/nextjs/src/previewRSC.tsx new file mode 100644 index 000000000000..d605a96db980 --- /dev/null +++ b/code/frameworks/nextjs/src/previewRSC.tsx @@ -0,0 +1,10 @@ +import type { Addon_DecoratorFunction } from '@storybook/types'; +import { ServerComponentDecorator } from './rsc/decorator'; + +export const decorators: Addon_DecoratorFunction[] = [ServerComponentDecorator]; + +export const parameters = { + nextjs: { + rsc: true, + }, +}; diff --git a/code/frameworks/nextjs/src/rsc/decorator.tsx b/code/frameworks/nextjs/src/rsc/decorator.tsx new file mode 100644 index 000000000000..73b7e7b4d817 --- /dev/null +++ b/code/frameworks/nextjs/src/rsc/decorator.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import type { StoryContext } from '@storybook/react'; + +export const ServerComponentDecorator = ( + Story: React.FC, + { parameters }: StoryContext +): React.ReactNode => + parameters?.nextjs?.rsc ? ( + + + + ) : ( + + ); diff --git a/code/frameworks/nextjs/template/stories/RSC.jsx b/code/frameworks/nextjs/template/stories/RSC.jsx new file mode 100644 index 000000000000..17a98b954919 --- /dev/null +++ b/code/frameworks/nextjs/template/stories/RSC.jsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const RSC = async ({ label }) => <>RSC {label}; + +export const Nested = async ({ children }) => <>Nested {children}; diff --git a/code/frameworks/nextjs/template/stories/RSC.stories.jsx b/code/frameworks/nextjs/template/stories/RSC.stories.jsx new file mode 100644 index 000000000000..e14456b50e58 --- /dev/null +++ b/code/frameworks/nextjs/template/stories/RSC.stories.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { RSC, Nested } from './RSC'; + +export default { + component: RSC, + args: { label: 'label' }, +}; + +export const Default = {}; + +export const DisableRSC = { + tags: ['test-skip'], + parameters: { + chromatic: { disable: true }, + nextjs: { rsc: false }, + }, +}; + +export const Error = { + tags: ['test-skip'], + parameters: { + chromatic: { disable: true }, + }, + render: () => { + throw new Error('RSC Error'); + }, +}; + +export const NestedRSC = { + render: (args) => ( + + + + ), +}; diff --git a/code/lib/cli/src/sandbox-templates.ts b/code/lib/cli/src/sandbox-templates.ts index 24fb3dc9846f..6aeed1d585a3 100644 --- a/code/lib/cli/src/sandbox-templates.ts +++ b/code/lib/cli/src/sandbox-templates.ts @@ -116,6 +116,11 @@ const baseTemplates = { renderer: '@storybook/react', builder: '@storybook/builder-webpack5', }, + modifications: { + mainConfig: { + features: { experimentalNextRSC: true }, + }, + }, skipTasks: ['e2e-tests-dev', 'bench'], inDevelopment: true, }, @@ -128,6 +133,11 @@ const baseTemplates = { renderer: '@storybook/react', builder: '@storybook/builder-webpack5', }, + modifications: { + mainConfig: { + features: { experimentalNextRSC: true }, + }, + }, skipTasks: ['e2e-tests-dev', 'bench'], }, 'nextjs/default-ts': { @@ -139,6 +149,11 @@ const baseTemplates = { renderer: '@storybook/react', builder: '@storybook/builder-webpack5', }, + modifications: { + mainConfig: { + features: { experimentalNextRSC: true }, + }, + }, skipTasks: ['e2e-tests-dev', 'bench'], }, 'nextjs/prerelease': { @@ -150,6 +165,11 @@ const baseTemplates = { renderer: '@storybook/react', builder: '@storybook/builder-webpack5', }, + modifications: { + mainConfig: { + features: { experimentalNextRSC: true }, + }, + }, skipTasks: ['e2e-tests-dev', 'bench'], }, 'react-vite/default-js': { diff --git a/code/lib/types/src/modules/core-common.ts b/code/lib/types/src/modules/core-common.ts index aaf96ddb2207..4eb9119b74c4 100644 --- a/code/lib/types/src/modules/core-common.ts +++ b/code/lib/types/src/modules/core-common.ts @@ -395,6 +395,11 @@ export interface StorybookConfigRaw { * This will make sure that your story renders the same no matter if docgen is enabled or not. */ disallowImplicitActionsInRenderV8?: boolean; + + /** + * Enable asynchronous component rendering in NextJS framework + */ + experimentalNextRSC?: boolean; }; build?: TestBuildConfig; diff --git a/scripts/tasks/test-runner-build.ts b/scripts/tasks/test-runner-build.ts index b57d4c803233..f902777fc63c 100644 --- a/scripts/tasks/test-runner-build.ts +++ b/scripts/tasks/test-runner-build.ts @@ -17,6 +17,7 @@ export const testRunnerBuild: Task & { port: number } = { '--junit', '--maxWorkers=2', '--failOnConsole', + '--skipTags="test-skip"', ]; // index-json mode is only supported in ssv7