Skip to main content

Next.js

Next.js is a popular React framework that provides a lot of features out of the box. Ladle works well with Next.js, but there are some caveats.

This component relies on a build-time transformation that Next.js provides. However, Ladle has its own build process. To make next/image work, we need to replace it with a simple <img /> element.

You'll need to customize Vite's config. Place it in the root directory of your project.

vite.config.ts
import path from "path";
import { defineConfig } from "vite";

export default defineConfig({
resolve: {
alias: {
"next/image": path.resolve(__dirname, "./.ladle/UnoptimizedImage.tsx"),
"next/link": path.resolve(__dirname, "./.ladle/UnoptimizedLink.tsx"),
},
},
});
.ladle/UnoptimizedLink.tsx
const UnoptimizedLink = (props: any) => {
return <a {...props} />;
};
export default UnoptimizedLink;
.ladle/UnoptimizedImage.tsx
import React from 'react';

interface UnoptimizedImageProps
extends React.ImgHTMLAttributes<HTMLImageElement> {
fill?: boolean;
}

const UnoptimizedImage: React.FC<UnoptimizedImageProps> = ({
fill,
...props
}) => {
const style: React.CSSProperties = fill
? {
position: 'absolute',
inset: '0',
width: '100%',
height: '100%',
}
: {};

return <img {...props} style={style} />;
};

export default UnoptimizedImage;

This solution is inspired by blog post.

next/navigation​

In the Stories file, using useRouter() of next/navigation may cause the following error. Uncaught Error: invariant expected app router to be mounted.

You could solve it by setting Providers.

.ladle/components.tsx
import { GlobalProvider } from "@ladle/react";
import { AppRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime";

export const Provider: GlobalProvider = ({ children }) => {
return (
<AppRouterContext.Provider
value={{
back: () => {
// Do nothing
},
forward: () => {
// Do nothing
},
prefetch: () => {
// Do nothing
},
push: () => {
// Do nothing
},
refresh: () => {
// Do nothing
},
replace: () => {
// Do nothing
},
}}
>
{children}
</AppRouterContext.Provider>
);
};

Or if you want to set it in each file, you could use Decorators.

./Hello.stories.tsx
import type { StoryDefault, Story } from "@ladle/react";
import { AppRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime";
import { useRouter } from "next/navigation";

export default {
decorators: [
(Component) => {
return (
<AppRouterContext.Provider
value={{
back: () => {
// Do nothing
},
forward: () => {
// Do nothing
},
prefetch: () => {
// Do nothing
},
push: () => {
// Do nothing
},
refresh: () => {
// Do nothing
},
replace: () => {
// Do nothing
},
}}
>
<Component />
</AppRouterContext.Provider>
);
},
],
} satisfies StoryDefault;

export const Hello: Story = () => {
const router = useRouter();
return (
<>
<h1>Hello Next.js App Router</h1>
<button onClick={() => router.push("/example")}>Route</button>
</>
);
};

If you want to wrap all stories with AppRouterContext, you can add it to the global provider instead.

Using environment variables​

To make use of your current .env files, you simply need to let Vite know and ensure they are passed to the browser. As Next.js requires you to add NEXT_PUBLIC_ as a prefix to your frontend environment variables, we also need to inform Vite about this.

To achieve this, you will need to customize Vite's configuration further.

vite.config.ts
import { defineConfig, loadEnv } from "vite";

export default defineConfig(({ mode }) => ({
// resolve: {...},
define: {
"process.env": loadEnv(mode, process.cwd(), "NEXT_PUBLIC_"),
},
}));