mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-07 07:21:17 +08:00
Support next/navigation in Next.js v13
This commit is contained in:
parent
d4d0e81ab1
commit
240776b7b5
@ -14,6 +14,7 @@
|
||||
- [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)
|
||||
@ -38,7 +39,9 @@
|
||||
|
||||
👉 [Next.js's Image Component](#nextjss-image-component)
|
||||
|
||||
👉 [Next.js Routing](#nextjs-routing)
|
||||
👉 [Next.js Routing (next/router)](#nextjs-routing)
|
||||
|
||||
👉 [Next.js Navigation (next/navigation)](#nextjs-navigation)
|
||||
|
||||
👉 [Sass/Scss](#sassscss)
|
||||
|
||||
@ -58,7 +61,7 @@
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Next.js](https://nextjs.org/) >= 9.x
|
||||
- [Next.js](https://nextjs.org/) >= 12.x
|
||||
- [Storybook](https://storybook.js.org/) >= 7.x
|
||||
|
||||
## Getting Started
|
||||
@ -167,22 +170,137 @@ export default function Home() {
|
||||
}
|
||||
```
|
||||
|
||||
#### Optimization
|
||||
|
||||
All Next.js `Image`s are automatically [unoptimized](https://nextjs.org/docs/api-reference/next/image#unoptimized) for you.
|
||||
|
||||
If [placeholder="blur"](https://nextjs.org/docs/api-reference/next/image#placeholder) is used, the [blurDataURL](https://nextjs.org/docs/api-reference/next/image#blurdataurl) used is the [src](https://nextjs.org/docs/api-reference/next/image#src) of the image (thus effectively disabling the placeholder).
|
||||
|
||||
See [this issue](https://github.com/vercel/next.js/issues/18393) for more discussion on how Next.js `Image`s are handled for Storybook.
|
||||
|
||||
#### AVIF
|
||||
|
||||
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 consider, 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 `nextAppDirectory` to `app`
|
||||
|
||||
If your story imports components, which uses `next/navigation`, you need to set the parameter `nextAppDirectory` to `app` 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: {
|
||||
nextAppDirectory: true,
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
If your Next.js project doesn't have a `pages` directory, you can set the parameter `nextAppDirectory` to `true` in the [preview.js](https://storybook.js.org/docs/react/configure/overview#configure-story-rendering) file. Then the parameter is not needed in the stories.
|
||||
|
||||
```js
|
||||
// .storybook/preview.js
|
||||
|
||||
export const parameters = {
|
||||
nextAppDirectory: true,
|
||||
};
|
||||
```
|
||||
|
||||
The parameter `nextAppDirectory` defaults to `false` if not set.
|
||||
|
||||
#### Default Navigation Context
|
||||
|
||||
The default values on the stubbed navigation context are as follows (see [globals](https://storybook.js.org/docs/react/essentials/toolbars-and-globals#globals) for more details on how globals work)
|
||||
|
||||
```ts
|
||||
const defaultNavigationContext = {
|
||||
push(...args) {
|
||||
action('nextRouter.push')(...args);
|
||||
},
|
||||
replace(...args) {
|
||||
action('nextRouter.replace')(...args);
|
||||
},
|
||||
forward(...args) {
|
||||
action('nextRouter.forward')(...args);
|
||||
},
|
||||
back(...args) {
|
||||
action('nextRouter.back')(...args);
|
||||
},
|
||||
prefetch(...args) {
|
||||
action('nextRouter.prefetch')(...args);
|
||||
},
|
||||
refresh: () => {
|
||||
action('nextRouter.refresh')();
|
||||
},
|
||||
pathname: '/',
|
||||
query: {},
|
||||
};
|
||||
```
|
||||
|
||||
#### 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 = {
|
||||
nextAppDirectory: true,
|
||||
nextNavigation: {
|
||||
pathname: '/some-default-path',
|
||||
query: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
#### 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 = {
|
||||
nextAppDirectory: true,
|
||||
nextNavigation: {
|
||||
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 = {
|
||||
nextAppDirectory: true,
|
||||
nextNavigation: {
|
||||
push(...args) {
|
||||
// custom logic can go here
|
||||
// this logs to the actions tab
|
||||
action('nextRouter.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.
|
||||
|
||||
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`.
|
||||
|
||||
#### 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.
|
||||
@ -215,7 +333,7 @@ export const Example = {
|
||||
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/main.js
|
||||
// .storybook/preview.js
|
||||
|
||||
export const parameters = {
|
||||
nextRouter: {
|
||||
@ -232,44 +350,51 @@ The default values on the stubbed router are as follows (see [globals](https://s
|
||||
|
||||
```ts
|
||||
const defaultRouter = {
|
||||
locale: context?.globals?.locale,
|
||||
route: '/',
|
||||
pathname: '/',
|
||||
query: {},
|
||||
asPath: '/',
|
||||
push(...args: unknown[]) {
|
||||
push(...args) {
|
||||
action('nextRouter.push')(...args);
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
replace(...args: unknown[]) {
|
||||
replace(...args) {
|
||||
action('nextRouter.replace')(...args);
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
reload(...args: unknown[]) {
|
||||
reload(...args) {
|
||||
action('nextRouter.reload')(...args);
|
||||
},
|
||||
back(...args: unknown[]) {
|
||||
back(...args) {
|
||||
action('nextRouter.back')(...args);
|
||||
},
|
||||
prefetch(...args: unknown[]) {
|
||||
forward() {
|
||||
action('nextRouter.forward')();
|
||||
},
|
||||
prefetch(...args) {
|
||||
action('nextRouter.prefetch')(...args);
|
||||
return Promise.resolve();
|
||||
},
|
||||
beforePopState(...args: unknown[]) {
|
||||
beforePopState(...args) {
|
||||
action('nextRouter.beforePopState')(...args);
|
||||
},
|
||||
events: {
|
||||
on(...args: unknown[]) {
|
||||
on(...args) {
|
||||
action('nextRouter.events.on')(...args);
|
||||
},
|
||||
off(...args: unknown[]) {
|
||||
off(...args) {
|
||||
action('nextRouter.events.off')(...args);
|
||||
},
|
||||
emit(...args: unknown[]) {
|
||||
emit(...args) {
|
||||
action('nextRouter.events.emit')(...args);
|
||||
},
|
||||
},
|
||||
locale: globals?.locale,
|
||||
asPath: '/',
|
||||
basePath: '/',
|
||||
isFallback: false,
|
||||
isLocaleDomain: false,
|
||||
isReady: true,
|
||||
isPreview: false,
|
||||
route: '/',
|
||||
pathname: '/',
|
||||
query: {},
|
||||
};
|
||||
```
|
||||
|
||||
@ -278,7 +403,7 @@ const defaultRouter = {
|
||||
If you override a function, you lose the automatic action tab integration and have to build it out yourself.
|
||||
|
||||
```js
|
||||
// .storybook/main.js
|
||||
// .storybook/preview.js
|
||||
|
||||
export const parameters = {
|
||||
nextRouter: {
|
||||
@ -292,7 +417,7 @@ export const parameters = {
|
||||
Doing this yourself looks something like this (make sure you install the `@storybook/addon-actions` package):
|
||||
|
||||
```js
|
||||
// .storybook/main.js
|
||||
// .storybook/preview.js
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
export const parameters = {
|
||||
@ -404,14 +529,6 @@ You can use your own babel config too. This is an example of how you can customi
|
||||
}
|
||||
```
|
||||
|
||||
If you use a monorepo, you may need to add the babel config yourself to your storybook project. Just add a babel config to your storybook project with the following contents to get started.
|
||||
|
||||
```json
|
||||
{
|
||||
"presets": ["next/babel"]
|
||||
}
|
||||
```
|
||||
|
||||
### Postcss
|
||||
|
||||
Next.js lets you [customize postcss config](https://nextjs.org/docs/advanced-features/customizing-postcss-config#default-behavior). Thus this framework will automatically handle your postcss config for you.
|
||||
|
@ -8,6 +8,8 @@ export function configureNextImport(baseConfig: WebpackConfig) {
|
||||
|
||||
const isNext12 = semver.satisfies(nextJSVersion, '~12');
|
||||
const isNext13 = semver.satisfies(nextJSVersion, '~13');
|
||||
const isNextVersionSmallerThan13 = semver.lt(nextJSVersion, '13.0.0');
|
||||
const isNextVersionSmallerThan12 = semver.lt(nextJSVersion, '12.0.0');
|
||||
|
||||
baseConfig.plugins = baseConfig.plugins ?? [];
|
||||
|
||||
@ -26,4 +28,20 @@ export function configureNextImport(baseConfig: WebpackConfig) {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isNextVersionSmallerThan13) {
|
||||
baseConfig.plugins.push(
|
||||
new IgnorePlugin({
|
||||
resourceRegExp: /next\/dist\/shared\/lib\/hooks-client-context$/,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isNextVersionSmallerThan12) {
|
||||
baseConfig.plugins.push(
|
||||
new IgnorePlugin({
|
||||
resourceRegExp: /next\/dist\/shared\/lib\/app-router-context$/,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
44
code/frameworks/nextjs/src/routing/app-router-provider.tsx
Normal file
44
code/frameworks/nextjs/src/routing/app-router-provider.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { AppRouterContext } from 'next/dist/shared/lib/app-router-context';
|
||||
import { PathnameContext, SearchParamsContext } from 'next/dist/shared/lib/hooks-client-context';
|
||||
import type { RouteParams } from './types';
|
||||
|
||||
type AppRouterProviderProps = {
|
||||
action: (name: string) => (...args: any[]) => void;
|
||||
routeParams: RouteParams;
|
||||
};
|
||||
|
||||
const AppRouterProvider: React.FC<AppRouterProviderProps> = ({ children, action, routeParams }) => {
|
||||
const { pathname, query, ...restRouteParams } = routeParams;
|
||||
return (
|
||||
<AppRouterContext.Provider
|
||||
value={{
|
||||
push(...args) {
|
||||
action('nextRouter.push')(...args);
|
||||
},
|
||||
replace(...args) {
|
||||
action('nextRouter.replace')(...args);
|
||||
},
|
||||
forward(...args) {
|
||||
action('nextRouter.forward')(...args);
|
||||
},
|
||||
back(...args) {
|
||||
action('nextRouter.back')(...args);
|
||||
},
|
||||
prefetch(...args) {
|
||||
action('nextRouter.prefetch')(...args);
|
||||
},
|
||||
refresh: () => {
|
||||
action('nextRouter.refresh')();
|
||||
},
|
||||
...restRouteParams,
|
||||
}}
|
||||
>
|
||||
<SearchParamsContext.Provider value={new URLSearchParams(query)}>
|
||||
<PathnameContext.Provider value={pathname}>{children}</PathnameContext.Provider>
|
||||
</SearchParamsContext.Provider>
|
||||
</AppRouterContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppRouterProvider;
|
@ -2,8 +2,17 @@ import * as React from 'react';
|
||||
// this will be aliased by webpack at runtime (this is just for typing)
|
||||
import type { action as originalAction } from '@storybook/addon-actions';
|
||||
import type { Addon_StoryContext } from '@storybook/types';
|
||||
import { RouterContext } from 'next/dist/shared/lib/router-context';
|
||||
import Router from 'next/router';
|
||||
|
||||
import PageRouterProvider from './page-router-provider';
|
||||
import type { RouteParams, NextAppDirectory } from './types';
|
||||
|
||||
/**
|
||||
* Dynamic import necessary because otherwise
|
||||
* older versions of Next.js will throw an error
|
||||
* because some imports in './app-router-provider' only exists
|
||||
* in Next.js > v13
|
||||
*/
|
||||
const AppRouterProvider = React.lazy(() => import('./app-router-provider'));
|
||||
|
||||
let action: typeof originalAction;
|
||||
|
||||
@ -13,61 +22,41 @@ try {
|
||||
action = () => () => {};
|
||||
}
|
||||
|
||||
const defaultRouter = {
|
||||
route: '/',
|
||||
const defaultRouterParams: RouteParams = {
|
||||
pathname: '/',
|
||||
query: {},
|
||||
asPath: '/',
|
||||
push(...args: unknown[]): Promise<boolean> {
|
||||
action('nextRouter.push')(...args);
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
replace(...args: unknown[]): Promise<boolean> {
|
||||
action('nextRouter.replace')(...args);
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
reload(...args: unknown[]): void {
|
||||
action('nextRouter.reload')(...args);
|
||||
},
|
||||
back(...args: unknown[]): void {
|
||||
action('nextRouter.back')(...args);
|
||||
},
|
||||
prefetch(...args: unknown[]): Promise<void> {
|
||||
action('nextRouter.prefetch')(...args);
|
||||
return Promise.resolve();
|
||||
},
|
||||
beforePopState(...args: unknown[]): void {
|
||||
action('nextRouter.beforePopState')(...args);
|
||||
},
|
||||
events: {
|
||||
on(...args: unknown[]): void {
|
||||
action('nextRouter.events.on')(...args);
|
||||
},
|
||||
off(...args: unknown[]): void {
|
||||
action('nextRouter.events.off')(...args);
|
||||
},
|
||||
emit(...args: unknown[]): void {
|
||||
action('nextRouter.events.emit')(...args);
|
||||
},
|
||||
},
|
||||
isFallback: false,
|
||||
};
|
||||
|
||||
export const RouterDecorator = (
|
||||
Story: React.FC,
|
||||
{ globals, parameters }: Addon_StoryContext
|
||||
): React.ReactNode => {
|
||||
const nextRouterParams = parameters.nextRouter ?? {};
|
||||
const nextAppDirectory = (parameters.nextAppDirectory as NextAppDirectory | undefined) ?? false;
|
||||
|
||||
Router.router = {
|
||||
...defaultRouter,
|
||||
locale: globals?.locale,
|
||||
...nextRouterParams,
|
||||
} as NonNullable<typeof Router.router>;
|
||||
if (nextAppDirectory) {
|
||||
return (
|
||||
<AppRouterProvider
|
||||
action={action}
|
||||
routeParams={{
|
||||
...defaultRouterParams,
|
||||
...parameters.nextNavigation,
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</AppRouterProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RouterContext.Provider value={Router.router as any}>
|
||||
<PageRouterProvider
|
||||
action={action}
|
||||
globals={globals}
|
||||
routeParams={{
|
||||
...defaultRouterParams,
|
||||
...parameters.nextRouter,
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</RouterContext.Provider>
|
||||
</PageRouterProvider>
|
||||
);
|
||||
};
|
||||
|
70
code/frameworks/nextjs/src/routing/page-router-provider.tsx
Normal file
70
code/frameworks/nextjs/src/routing/page-router-provider.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import type { Globals } from '@storybook/csf';
|
||||
import { RouterContext } from 'next/dist/shared/lib/router-context';
|
||||
import React from 'react';
|
||||
import type { RouteParams } from './types';
|
||||
|
||||
type PageRouterProviderProps = {
|
||||
action: (name: string) => (...args: any[]) => void;
|
||||
routeParams: RouteParams;
|
||||
globals: Globals;
|
||||
};
|
||||
|
||||
const PageRouterProvider: React.FC<PageRouterProviderProps> = ({
|
||||
children,
|
||||
action,
|
||||
routeParams,
|
||||
globals,
|
||||
}) => (
|
||||
<RouterContext.Provider
|
||||
value={{
|
||||
push(...args) {
|
||||
action('nextRouter.push')(...args);
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
replace(...args) {
|
||||
action('nextRouter.replace')(...args);
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
reload(...args) {
|
||||
action('nextRouter.reload')(...args);
|
||||
},
|
||||
back(...args) {
|
||||
action('nextRouter.back')(...args);
|
||||
},
|
||||
forward() {
|
||||
action('nextRouter.forward')();
|
||||
},
|
||||
prefetch(...args) {
|
||||
action('nextRouter.prefetch')(...args);
|
||||
return Promise.resolve();
|
||||
},
|
||||
beforePopState(...args) {
|
||||
action('nextRouter.beforePopState')(...args);
|
||||
},
|
||||
events: {
|
||||
on(...args) {
|
||||
action('nextRouter.events.on')(...args);
|
||||
},
|
||||
off(...args) {
|
||||
action('nextRouter.events.off')(...args);
|
||||
},
|
||||
emit(...args) {
|
||||
action('nextRouter.events.emit')(...args);
|
||||
},
|
||||
},
|
||||
locale: globals?.locale,
|
||||
route: '/',
|
||||
asPath: '/',
|
||||
basePath: '/',
|
||||
isFallback: false,
|
||||
isLocaleDomain: false,
|
||||
isReady: true,
|
||||
isPreview: false,
|
||||
...routeParams,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RouterContext.Provider>
|
||||
);
|
||||
|
||||
export default PageRouterProvider;
|
7
code/frameworks/nextjs/src/routing/types.tsx
Normal file
7
code/frameworks/nextjs/src/routing/types.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export type RouteParams = {
|
||||
pathname: string;
|
||||
query: Record<string, string>;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type NextAppDirectory = boolean;
|
@ -72,3 +72,9 @@ export default {
|
||||
};
|
||||
|
||||
export const Default = {};
|
||||
|
||||
export const InAppDir = {
|
||||
parameters: {
|
||||
nextAppDirectory: true,
|
||||
},
|
||||
};
|
||||
|
@ -0,0 +1,75 @@
|
||||
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
||||
import React from 'react';
|
||||
|
||||
function Component() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const searchParamsList = Array.from(searchParams.entries());
|
||||
|
||||
const routerActions = [
|
||||
{
|
||||
cb: () => router.back(),
|
||||
name: 'Go back',
|
||||
},
|
||||
{
|
||||
cb: () => router.forward(),
|
||||
name: 'Go forward',
|
||||
},
|
||||
{
|
||||
cb: () => router.prefetch('/prefetched-html'),
|
||||
name: 'Prefetch',
|
||||
},
|
||||
{
|
||||
cb: () => router.push('/push-html', { forceOptimisticNavigation: true }),
|
||||
name: 'Push HTML',
|
||||
},
|
||||
{
|
||||
cb: () => router.refresh(),
|
||||
name: 'Refresh',
|
||||
},
|
||||
{
|
||||
cb: () => router.replace('/replaced-html', { forceOptimisticNavigation: true }),
|
||||
name: 'Replace',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>pathname: {pathname}</div>
|
||||
<div>
|
||||
searchparams:{' '}
|
||||
<ul>
|
||||
{searchParamsList.map(([key, value]) => (
|
||||
<li key={key}>
|
||||
{key}: {value}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{routerActions.map(({ cb, name }) => (
|
||||
<div key={name} style={{ marginBottom: '1em' }}>
|
||||
<button type="button" onClick={cb}>
|
||||
{name}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
component: Component,
|
||||
parameters: {
|
||||
nextAppDirectory: true,
|
||||
nextNavigation: {
|
||||
pathname: '/hello',
|
||||
query: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Default = {};
|
@ -0,0 +1,67 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
function Component() {
|
||||
const router = useRouter();
|
||||
const searchParams = router.query;
|
||||
|
||||
const routerActions = [
|
||||
{
|
||||
cb: () => router.back(),
|
||||
name: 'Go back',
|
||||
},
|
||||
{
|
||||
cb: () => router.forward(),
|
||||
name: 'Go forward',
|
||||
},
|
||||
{
|
||||
cb: () => router.prefetch('/prefetched-html'),
|
||||
name: 'Prefetch',
|
||||
},
|
||||
{
|
||||
cb: () => router.push('/push-html', { forceOptimisticNavigation: true }),
|
||||
name: 'Push HTML',
|
||||
},
|
||||
{
|
||||
cb: () => router.replace('/replaced-html', { forceOptimisticNavigation: true }),
|
||||
name: 'Replace',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>pathname: {router.pathname}</div>
|
||||
<div>
|
||||
searchparams:{' '}
|
||||
<ul>
|
||||
{Object.entries(searchParams).map(([key, value]) => (
|
||||
<li key={key}>
|
||||
{key}: {value}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{routerActions.map(({ cb, name }) => (
|
||||
<div key={name} style={{ marginBottom: '1em' }}>
|
||||
<button type="button" onClick={cb}>
|
||||
{name}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
component: Component,
|
||||
parameters: {
|
||||
nextRouter: {
|
||||
pathname: '/hello',
|
||||
query: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Default = {};
|
@ -75,3 +75,9 @@ export default {
|
||||
} as Meta<typeof Component>;
|
||||
|
||||
export const Default: StoryObj<typeof Component> = {};
|
||||
|
||||
export const InAppDir: StoryObj<typeof Component> = {
|
||||
parameters: {
|
||||
nextAppDirectory: true,
|
||||
},
|
||||
};
|
||||
|
@ -0,0 +1,76 @@
|
||||
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
||||
import React from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
function Component() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const searchParamsList = Array.from(searchParams.entries());
|
||||
|
||||
const routerActions = [
|
||||
{
|
||||
cb: () => router.back(),
|
||||
name: 'Go back',
|
||||
},
|
||||
{
|
||||
cb: () => router.forward(),
|
||||
name: 'Go forward',
|
||||
},
|
||||
{
|
||||
cb: () => router.prefetch('/prefetched-html'),
|
||||
name: 'Prefetch',
|
||||
},
|
||||
{
|
||||
cb: () => router.push('/push-html', { forceOptimisticNavigation: true }),
|
||||
name: 'Push HTML',
|
||||
},
|
||||
{
|
||||
cb: () => router.refresh(),
|
||||
name: 'Refresh',
|
||||
},
|
||||
{
|
||||
cb: () => router.replace('/replaced-html', { forceOptimisticNavigation: true }),
|
||||
name: 'Replace',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>pathname: {pathname}</div>
|
||||
<div>
|
||||
searchparams:{' '}
|
||||
<ul>
|
||||
{searchParamsList.map(([key, value]) => (
|
||||
<li key={key}>
|
||||
{key}: {value}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{routerActions.map(({ cb, name }) => (
|
||||
<div key={name} style={{ marginBottom: '1em' }}>
|
||||
<button type="button" onClick={cb}>
|
||||
{name}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
component: Component,
|
||||
parameters: {
|
||||
nextAppDirectory: true,
|
||||
nextNavigation: {
|
||||
pathname: '/hello',
|
||||
query: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Meta<typeof Component>;
|
||||
|
||||
export const Default: StoryObj<typeof Component> = {};
|
Loading…
x
Reference in New Issue
Block a user