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) - [Remote Images](#remote-images)
- [Optimization](#optimization) - [Optimization](#optimization)
- [AVIF](#avif) - [AVIF](#avif)
- [Next.js Navigation](#nextjs-navigation)
- [Next.js Routing](#nextjs-routing) - [Next.js Routing](#nextjs-routing)
- [Overriding defaults](#overriding-defaults) - [Overriding defaults](#overriding-defaults)
- [Global Defaults](#global-defaults) - [Global Defaults](#global-defaults)
- [Default Router](#default-router) - [Default Router](#default-router)
- [Actions Integration Caveats](#actions-integration-caveats) - [Actions Integration Caveats](#actions-integration-caveats)
- [Next.js Navigation](#nextjs-navigation)
- [Sass/Scss](#sassscss) - [Sass/Scss](#sassscss)
- [Css/Sass/Scss Modules](#csssassscss-modules) - [Css/Sass/Scss Modules](#csssassscss-modules)
- [Styled JSX](#styled-jsx) - [Styled JSX](#styled-jsx)
@ -30,7 +30,7 @@
- [Typescript](#typescript) - [Typescript](#typescript)
- [Notes for Yarn v2 and v3 users](#notes-for-yarn-v2-and-v3-users) - [Notes for Yarn v2 and v3 users](#notes-for-yarn-v2-and-v3-users)
- [FAQ](#faq) - [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) - [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) - [Module not found: Error: Can't resolve [package name]](#module-not-found-error-cant-resolve-package-name)
- [Acknowledgements](#acknowledgements) - [Acknowledgements](#acknowledgements)
@ -71,29 +71,52 @@
Follow the prompts after running this command in your Next.js project's root directory: Follow the prompts after running this command in your Next.js project's root directory:
```bash ```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 ### 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: Install the framework:
```bash ```bash
yarn install @storybook/nextjs yarn install -D @storybook/nextjs@next
``` ```
Update your `main.js` to change the framework property:
```js ```js
// .storybook/main.js // .storybook/main.js
module.exports = { module.exports = {
// ...
framework: { framework: {
name: '@storybook/nextjs', // name: '@storybook/react-webpack5', // Remove this
options: {}; 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 ## Documentation
@ -109,14 +132,13 @@ For example:
const path = require('path'); const path = require('path');
module.exports = { module.exports = {
// other config ommited for brevity // ...
framework: { framework: {
name: '@storybook/nextjs', name: '@storybook/nextjs',
options: { options: {
nextConfigPath: path.resolve(__dirname, '../next.config.js'), 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. 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 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 #### 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 ```js
// SomeComponentThatUsesTheRouter.stories.js // SomeComponentThatUsesTheRouter.stories.js
@ -352,16 +214,16 @@ export default {
component: SomeComponentThatUsesTheRouter, component: SomeComponentThatUsesTheRouter,
}; };
// if you have the actions addon // If you have the actions addon,
// you can click the links and see the route change events there // you can interact with the links and see the route change events there
export const Example = { export const Example = {
parameters: { parameters: {
nextjs: { nextjs: {
router: { router: {
path: '/profile/[id]', path: '/profile/[id]',
asPath: '/profile/ryanclementshax', asPath: '/profile/1',
query: { query: {
id: 'ryanclementshax', id: '1',
}, },
}, },
}, },
@ -444,7 +306,7 @@ const defaultRouter = {
#### Actions Integration Caveats #### 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 ```js
// .storybook/preview.js // .storybook/preview.js
@ -453,7 +315,7 @@ export const parameters = {
nextjs: { nextjs: {
router: { router: {
push() { 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: { nextjs: {
router: { router: {
push(...args) { push(...args) {
// custom logic can go here // Custom logic can go here
// this logs to the actions tab // This logs to the Actions panel
action('nextRouter.push')(...args); 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); return Promise.resolve(true);
}, },
}, },
@ -496,7 +513,7 @@ This will automatically include any of your [custom sass configurations](https:/
const path = require('path'); const path = require('path');
module.exports = { 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: { sassOptions: {
includePaths: [path.join(__dirname, 'styles')], 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. [css modules](https://nextjs.org/docs/basic-features/built-in-css-support#adding-component-level-css) work as expected.
```js ```js
// this import works just fine in Storybook now // This import works just fine in Storybook now
import styles from './Button.module.css'; import styles from './Button.module.css';
// sass/scss is also supported // sass/scss is also supported
// import styles from './Button.module.scss' // import styles from './Button.module.scss'
@ -603,10 +620,11 @@ export default function HomePage() {
} }
``` ```
```js Also OK for global styles in `preview.js`!
// preview.js
```js
// .storybook/preview.js
// Also ok in preview.js!
import 'styles/globals.scss'; import 'styles/globals.scss';
// ... // ...
@ -659,14 +677,14 @@ Below is an example of how to add svgr support to Storybook with this framework.
```js ```js
// .storybook/main.js // .storybook/main.js
module.exports = { module.exports = {
// other config omitted for brevity // ...
webpackFinal: async (config) => { 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 // since you want to handle those files with @svgr/webpack
const imageRule = config.module.rules.find((rule) => rule.test.test('.svg')); const imageRule = config.module.rules.find((rule) => rule.test.test('.svg'));
imageRule.exclude = /\.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({ config.module.rules.push({
test: /\.svg$/, test: /\.svg$/,
use: ['@svgr/webpack'], use: ['@svgr/webpack'],
@ -703,9 +721,9 @@ This is because those versions of Yarn have different package resolution rules t
### FAQ ### 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** **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 // ./pages/my-page.jsx
import fs from 'fs'; 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 () => { export const getStaticProps = async () => {
// Logic that uses `fs` // Logic that uses `fs`
@ -728,15 +747,57 @@ export const getStaticProps = async () => {
// ./pages/my-page.jsx // ./pages/my-page.jsx
import fs from 'fs'; import fs from 'fs';
// Use this pure component in your stories instead
import MyPage from 'components/MyPage'; import MyPage from 'components/MyPage';
export default MyPage; export default function Page(props) {
return <MyPage {...props} />;
}
export const getStaticProps = async () => { export const getStaticProps = async () => {
// Logic that uses `fs` // 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 #### 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. Make sure you are treating image imports the same way you treat them when using `next/image` in normal development.