DEV Community

Raynal Ramadhan Gobel
Raynal Ramadhan Gobel

Posted on

Working with `next/navigation` in Storybook

Background

I was working on integrating Storybook process into legacy system using next. The thing is, some of the components are dependent on the useRouter() hook from next/navigation. I'm planning to document how to work with those and fix getting errors like:

Error: invariant expected app router to be mounted
Enter fullscreen mode Exit fullscreen mode

Approach

TL;DR Use context provider with mock value. Like:

<AppRouterContext.Provider value={mockRouter}>
  <Component {...args} />
</AppRouterContext.Provider>
Enter fullscreen mode Exit fullscreen mode

Setup

Import dependency like this on your Storybook. Example, I have this page component I want to render on storybook.

// src/app/login/page.tsx
import { useRouter } from 'next/navigation';

const REGISTER_ROUTE = '/register';

export default function LoginPage() {
  const router = useRouter(); // The offending line

  return (
    <div>
      <button onClick={() => router.push(REGISTER_ROUTE)}>
        Register
      </button>
    </div>
  )

}

Enter fullscreen mode Exit fullscreen mode

With Storybook story like:

// src/stories/LoginPage.stories.tsx
import type { Meta, StoryObj } from 'storybook/react';

import Component from '@app/login';

const meta = {
  title: 'Login Page',
  component: Component,
  tags: ['autodocs'],
} satisfies Meta<typeof Component>;

export default meta;

type Story = StoryObj<typeof meta>;

export const DefaultView: Story = {};
Enter fullscreen mode Exit fullscreen mode

This won't run and will fail.

Solution

Update the storybook and wrap component in next AppRouterContext.

// src/stories/LoginPage.stories.tsx
import type { Meta, StoryObj } from 'storybook/react';
+ import {
+   AppRouterContext,
+   AppRouterInstance,
+ } from 'next/dist/shared/lib/app-router-context';

import Component from '@app/login';

const meta = {
  title: 'Login Page',
  component: Component,
  tags: ['autodocs'],
} satisfies Meta<typeof Component>;

+ // I tried empty functions, but I think mock function would
+ // be better? See: `vi.fn()`
+ const mockRouter: AppRouterInstance = {
+   back: () => {},
+   forward: () => {},
+   prefetch: () => {},
+   push: () => {},
+   refresh: () => {},
+   replace: () => {},
+ }

export default meta;

type Story = StoryObj<typeof meta>;

- export const DefaultView: Story = {};
+ export const DefaultView: Story = {
+   render: (args) => )
+     <AppRouterContext.Provider value={mockRouter}>
+       <Component {...args} />
+     </AppRouterContext.Provider>
+   ),
+ };

Enter fullscreen mode Exit fullscreen mode

Top comments (0)