Update @storybook/nextjs README

- Add instructions for working around Next 13 components with data fetching
- Updates for accuracy
- Stylistic updates
This commit is contained in:
Kyle Gach 2022-12-08 13:50:54 -07:00
parent d672182dc8
commit 7b287ec114

View File

@ -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 youre 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 <MyPage {...props} />;
}
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 <MyPage {...data} />;
}
```
#### 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.