diff --git a/code/frameworks/nextjs/README.md b/code/frameworks/nextjs/README.md index 38f532ed312..45a1c080514 100644 --- a/code/frameworks/nextjs/README.md +++ b/code/frameworks/nextjs/README.md @@ -14,12 +14,12 @@ - [Remote Images](#remote-images) - [Optimization](#optimization) - [AVIF](#avif) - - [Next.js Navigation](#nextjs-navigation) - [Next.js Routing](#nextjs-routing) - [Overriding defaults](#overriding-defaults) - [Global Defaults](#global-defaults) - [Default Router](#default-router) - [Actions Integration Caveats](#actions-integration-caveats) + - [Next.js Navigation](#nextjs-navigation) - [Sass/Scss](#sassscss) - [Css/Sass/Scss Modules](#csssassscss-modules) - [Styled JSX](#styled-jsx) @@ -30,7 +30,7 @@ - [Typescript](#typescript) - [Notes for Yarn v2 and v3 users](#notes-for-yarn-v2-and-v3-users) - [FAQ](#faq) - - [Stories for pages](#stories-for-pages) + - [Stories for pages](#stories-for-pages-components-which-fetch-data) - [Statically imported images won't load](#statically-imported-images-wont-load) - [Module not found: Error: Can't resolve [package name]](#module-not-found-error-cant-resolve-package-name) - [Acknowledgements](#acknowledgements) @@ -71,29 +71,52 @@ Follow the prompts after running this command in your Next.js project's root directory: ```bash -npx storybook init +npx storybook@next init ``` -[More on getting started with Storybook](https://storybook.js.org/docs/react/get-started/introduction) +[More on getting started with Storybook](https://storybook.js.org/docs/react/get-started/install) ### In a project with Storybook -Update your `main.js` to look something like this: +This framework is designed to work with Storybook 7. If you’re not already using v7, upgrade with this command: + +```bash +npx storybook@next upgrade --prerelease +``` Install the framework: ```bash -yarn install @storybook/nextjs +yarn install -D @storybook/nextjs@next ``` +Update your `main.js` to change the framework property: + ```js // .storybook/main.js module.exports = { + // ... framework: { - name: '@storybook/nextjs', - options: {}; - } -} + // name: '@storybook/react-webpack5', // Remove this + name: '@storybook/nextjs', // Add this + options: {}, + }, +}; +``` + +If you were using Storybook plugins to integrate with Next.js, those are no longer necessary when using this framework and can be removed: + +```js +// .storybook/main.js +module.exports = { + // ... + addons: [ + // ... + // These can both be removed + // 'storybook-addon-next', + // 'storybook-addon-next-router', + ], +}; ``` ## Documentation @@ -109,14 +132,13 @@ For example: const path = require('path'); module.exports = { - // other config ommited for brevity + // ... framework: { name: '@storybook/nextjs', options: { nextConfigPath: path.resolve(__dirname, '../next.config.js'), }, }, - // ... }; ``` @@ -174,175 +196,15 @@ export default function Home() { This format is not supported by this framework yet. Feel free to [open up an issue](https://github.com/storybookjs/storybook/issues) if this is something you want to see. -### Next.js Navigation - -Please note that [next/navigation](https://beta.nextjs.org/docs/upgrade-guide#step-5-migrating-routing-hooks) can only be used in components/pages of the `app` directory of Next.js v13 or higher. - -#### Set `nextjs.appDirectory` to `true` - -If your story imports components that use `next/navigation`, you need to set the parameter `nextjs.appDirectory` to `true` in your Story: - -```js -// SomeComponentThatUsesTheRouter.stories.js -import SomeComponentThatUsesTheNavigation from './SomeComponentThatUsesTheNavigation'; - -export default { - component: SomeComponentThatUsesTheNavigation, -}; - -// if you have the actions addon -// you can click the links and see the route change events there -export const Example = { - parameters: { - nextjs: { - appDirectory: true, - }, - }, -}, -``` - -If your Next.js project uses the `app` directory for every page (in other words, it does not have a `pages` directory), you can set the parameter `nextjs.appDirectory` to `true` in the [preview.js](https://storybook.js.org/docs/react/configure/overview#configure-story-rendering) file to apply it to all stories. - -```js -// .storybook/preview.js - -export const parameters = { - nextjs: { - appDirectory: true, - }, -}; -``` - -The parameter `nextjs.appDirectory` defaults to `false` if not set. - -#### Overriding defaults - -Per-story overrides can be done by adding a `nextjs.navigation` property onto the story [parameters](https://storybook.js.org/docs/react/writing-stories/parameters). The framework will shallowly merge whatever you put here into the router. - -```js -// SomeComponentThatUsesTheNavigation.stories.js -import SomeComponentThatUsesTheNavigation from './SomeComponentThatUsesTheNavigation'; - -export default { - component: SomeComponentThatUsesTheNavigation, -}; - -// if you have the actions addon -// you can click the links and see the route change events there -export const Example = { - parameters: { - nextjs: { - appDirectory: true, - navigation: { - pathname: '/some-default-path', - query: { - foo: 'bar', - }, - }, - }, - }, -}; -``` - -#### Global Defaults - -Global defaults can be set in [preview.js](https://storybook.js.org/docs/react/configure/overview#configure-story-rendering) and will be shallowly merged with the default router. - -```js -// .storybook/preview.js - -export const parameters = { - nextjs: { - appDirectory: true, - navigation: { - pathname: '/some-default-path', - query: { - foo: 'bar', - }, - }, - }, -}; -``` - -#### Default Navigation Context - -The default values on the stubbed navigation context are as follows: - -```ts -const defaultNavigationContext = { - push(...args) { - action('nextNavigation.push')(...args); - }, - replace(...args) { - action('nextNavigation.replace')(...args); - }, - forward(...args) { - action('nextNavigation.forward')(...args); - }, - back(...args) { - action('nextNavigation.back')(...args); - }, - prefetch(...args) { - action('nextNavigation.prefetch')(...args); - }, - refresh: () => { - action('nextNavigation.refresh')(); - }, - pathname: '/', - query: {}, -}; -``` - -#### Actions Integration Caveats - -If you override a function, you lose the automatic action tab integration and have to build it out yourself. - -```js -// .storybook/preview.js - -export const parameters = { - nextjs: { - appDirectory: true, - navigation: { - push() { - // The default implementation that logs the action into the action tab is lost - }, - }, - }, -}; -``` - -Doing this yourself looks something like this (make sure you install the `@storybook/addon-actions` package): - -```js -// .storybook/preview.js -import { action } from '@storybook/addon-actions'; - -export const parameters = { - nextjs: { - appDirectory: true, - navigation: { - push(...args) { - // custom logic can go here - // this logs to the actions tab - action('nextNavigation.push')(...args); - // return whatever you want here - return Promise.resolve(true); - }, - }, - }, -}; -``` - ### Next.js Routing -[Next.js's router](https://nextjs.org/docs/routing/introduction) is automatically stubbed for you so that when the router is interacted with, all of its interactions are automatically logged to the [Storybook actions tab](https://storybook.js.org/docs/react/essentials/actions) if you have the actions addon. +[Next.js's router](https://nextjs.org/docs/routing/introduction) is automatically stubbed for you so that when the router is interacted with, all of its interactions are automatically logged to the Actions ctions panel if you have the [Storybook actions addon](https://storybook.js.org/docs/react/essentials/actions). -You should only use `next/router` in the `pages` directory of Next.js v13 or higher. In the `app` directory, it is necessary to use `next/navigation`. +> When using Next.js 13+, you should only use `next/router` in the `pages` directory. In the `app` directory, it is necessary to use `next/navigation`. #### Overriding defaults -Per-story overrides can be done by adding a `nextRouter` property onto the story [parameters](https://storybook.js.org/docs/react/writing-stories/parameters). The framework will shallowly merge whatever you put here into the router. +Per-story overrides can be done by adding a `nextjs.router` property onto the story [parameters](https://storybook.js.org/docs/react/writing-stories/parameters). The framework will shallowly merge whatever you put here into the router. ```js // SomeComponentThatUsesTheRouter.stories.js @@ -352,16 +214,16 @@ export default { component: SomeComponentThatUsesTheRouter, }; -// if you have the actions addon -// you can click the links and see the route change events there +// If you have the actions addon, +// you can interact with the links and see the route change events there export const Example = { parameters: { nextjs: { router: { path: '/profile/[id]', - asPath: '/profile/ryanclementshax', + asPath: '/profile/1', query: { - id: 'ryanclementshax', + id: '1', }, }, }, @@ -444,7 +306,7 @@ const defaultRouter = { #### Actions Integration Caveats -If you override a function, you lose the automatic action tab integration and have to build it out yourself. +If you override a function, you lose the automatic actions integration and have to build it out yourself. ```js // .storybook/preview.js @@ -453,7 +315,7 @@ export const parameters = { nextjs: { router: { push() { - // The default implementation that logs the action into the action tab is lost + // The default implementation that logs the action into the Actions panel is lost }, }, }, @@ -470,10 +332,165 @@ export const parameters = { nextjs: { router: { push(...args) { - // custom logic can go here - // this logs to the actions tab + // Custom logic can go here + // This logs to the Actions panel action('nextRouter.push')(...args); - // return whatever you want here + // Return whatever you want here + return Promise.resolve(true); + }, + }, + }, +}; +``` + +### Next.js Navigation + +> Please note that [next/navigation](https://beta.nextjs.org/docs/upgrade-guide#step-5-migrating-routing-hooks) can only be used in components/pages in the `app` directory of Next.js 13+. + +#### Set `nextjs.appDirectory` to `true` + +If your story imports components that use `next/navigation`, you need to set the parameter `nextjs.appDirectory` to `true` in your Story: + +```js +// SomeComponentThatUsesTheRouter.stories.js +import SomeComponentThatUsesTheNavigation from './SomeComponentThatUsesTheNavigation'; + +export default { + component: SomeComponentThatUsesTheNavigation, +}; + +export const Example = { + parameters: { + nextjs: { + appDirectory: true, + }, + }, +}, +``` + +If your Next.js project uses the `app` directory for every page (in other words, it does not have a `pages` directory), you can set the parameter `nextjs.appDirectory` to `true` in the [preview.js](https://storybook.js.org/docs/react/configure/overview#configure-story-rendering) file to apply it to all stories. + +```js +// .storybook/preview.js + +export const parameters = { + nextjs: { + appDirectory: true, + }, +}; +``` + +The parameter `nextjs.appDirectory` defaults to `false` if not set. + +#### Overriding defaults + +Per-story overrides can be done by adding a `nextjs.navigation` property onto the story [parameters](https://storybook.js.org/docs/react/writing-stories/parameters). The framework will shallowly merge whatever you put here into the router. + +```js +// SomeComponentThatUsesTheNavigation.stories.js +import SomeComponentThatUsesTheNavigation from './SomeComponentThatUsesTheNavigation'; + +export default { + component: SomeComponentThatUsesTheNavigation, +}; + +// If you have the actions addon, +// you can interact with the links and see the route change events there +export const Example = { + parameters: { + nextjs: { + appDirectory: true, + navigation: { + pathname: '/profile', + query: { + user: '1', + }, + }, + }, + }, +}; +``` + +#### Global Defaults + +Global defaults can be set in [preview.js](https://storybook.js.org/docs/react/configure/overview#configure-story-rendering) and will be shallowly merged with the default router. + +```js +// .storybook/preview.js + +export const parameters = { + nextjs: { + appDirectory: true, + navigation: { + pathname: '/some-default-path', + }, + }, +}; +``` + +#### Default Navigation Context + +The default values on the stubbed navigation context are as follows: + +```ts +const defaultNavigationContext = { + push(...args) { + action('nextNavigation.push')(...args); + }, + replace(...args) { + action('nextNavigation.replace')(...args); + }, + forward(...args) { + action('nextNavigation.forward')(...args); + }, + back(...args) { + action('nextNavigation.back')(...args); + }, + prefetch(...args) { + action('nextNavigation.prefetch')(...args); + }, + refresh: () => { + action('nextNavigation.refresh')(); + }, + pathname: '/', + query: {}, +}; +``` + +#### Actions Integration Caveats + +If you override a function, you lose the automatic action tab integration and have to build it out yourself. + +```js +// .storybook/preview.js + +export const parameters = { + nextjs: { + appDirectory: true, + navigation: { + push() { + // The default implementation that logs the action into the Actions panel is lost + }, + }, + }, +}; +``` + +Doing this yourself looks something like this (make sure you install the `@storybook/addon-actions` package): + +```js +// .storybook/preview.js +import { action } from '@storybook/addon-actions'; + +export const parameters = { + nextjs: { + appDirectory: true, + navigation: { + push(...args) { + // Custom logic can go here + // This logs to the Actions panel + action('nextNavigation.push')(...args); + // Return whatever you want here return Promise.resolve(true); }, }, @@ -496,7 +513,7 @@ This will automatically include any of your [custom sass configurations](https:/ const path = require('path'); module.exports = { - // any options here are included in sass compilation for your stories + // Any options here are included in Sass compilation for your stories sassOptions: { includePaths: [path.join(__dirname, 'styles')], }, @@ -508,7 +525,7 @@ module.exports = { [css modules](https://nextjs.org/docs/basic-features/built-in-css-support#adding-component-level-css) work as expected. ```js -// this import works just fine in Storybook now +// This import works just fine in Storybook now import styles from './Button.module.css'; // sass/scss is also supported // import styles from './Button.module.scss' @@ -603,10 +620,11 @@ export default function HomePage() { } ``` -```js -// preview.js +Also OK for global styles in `preview.js`! + +```js +// .storybook/preview.js -// Also ok in preview.js! import 'styles/globals.scss'; // ... @@ -659,14 +677,14 @@ Below is an example of how to add svgr support to Storybook with this framework. ```js // .storybook/main.js module.exports = { - // other config omitted for brevity + // ... webpackFinal: async (config) => { - // this modifies the existing image rule to exclude .svg files + // This modifies the existing image rule to exclude .svg files // since you want to handle those files with @svgr/webpack const imageRule = config.module.rules.find((rule) => rule.test.test('.svg')); imageRule.exclude = /\.svg$/; - // configure .svg files to be loaded with @svgr/webpack + // Configure .svg files to be loaded with @svgr/webpack config.module.rules.push({ test: /\.svg$/, use: ['@svgr/webpack'], @@ -703,9 +721,9 @@ This is because those versions of Yarn have different package resolution rules t ### FAQ -#### Stories for pages +#### Stories for pages/components which fetch data -Next.js page files can contain imports to modules meant to run in a node environment (for use in data fetching functions). If you import from a Next.js page file containing those node module imports in your stories, your Storybook's Webpack will crash because those modules will not run in a browser. To get around this, you can extract the component in your page file into a separate file and import that component in your stories. Or, if that's not feasible for some reason, you can [polyfill those modules](https://webpack.js.org/configuration/node/) in your Storybook's [`webpackFinal` configuration](https://storybook.js.org/docs/react/builders/webpack#extending-storybooks-webpack-config). +Next.js page files can contain imports to modules meant to run in a node environment (for use in data fetching functions). If you import from a Next.js page file containing those node module imports in your stories, your Storybook's Webpack will crash because those modules will not run in a browser. To get around this, you can extract the component in your page file into a separate file and import that pure component in your stories. Or, if that's not feasible for some reason, you can [polyfill those modules](https://webpack.js.org/configuration/node/) in your Storybook's [`webpackFinal` configuration](https://storybook.js.org/docs/react/builders/webpack#extending-storybooks-webpack-config). **Before** @@ -713,9 +731,10 @@ Next.js page files can contain imports to modules meant to run in a node environ // ./pages/my-page.jsx import fs from 'fs'; -export default MyPage = (props) => ( - // ... -); +// Using this component in your stories will break the Storybook build +export default function Page(props) { + return; // ... +} export const getStaticProps = async () => { // Logic that uses `fs` @@ -728,15 +747,57 @@ export const getStaticProps = async () => { // ./pages/my-page.jsx import fs from 'fs'; +// Use this pure component in your stories instead import MyPage from 'components/MyPage'; -export default MyPage; +export default function Page(props) { + return ; +} export const getStaticProps = async () => { // Logic that uses `fs` }; ``` +Starting with Next.js 13, you can also fetch data directly within server components in the `app` directory. This does not (currently) work within Storybook for similar reasons as above. It can be worked around similarly as well, by extracting a pure component to a separate file and importing that component in your stories. + +**Before** + +```jsx +// ./app/my-page/index.jsx +async function getData() { + const res = await fetch(...); + // ... +} + +// Using this component in your stories will break the Storybook build +export default async function Page() { + const data = await getData(); + + return // ... +} +``` + +**After** + +```jsx +// ./app/my-page/index.jsx + +// Use this component in your stories +import MyPage from './components/MyPage'; + +async function getData() { + const res = await fetch(...); + // ... +} + +export default async function Page() { + const data = await getData(); + + return ; +} +``` + #### Statically imported images won't load Make sure you are treating image imports the same way you treat them when using `next/image` in normal development.