# Integrating Storybook and Inertia.js - Part 1
Table of Contents
If you are using Inertia for your project with a front end framework like Vue or React, creating stories for them in Storybook can cause issues if they rely on Inertia’s APIs like usePage or the router.
Fortunately there is a simple way to make them work together. In this post we’ll be using Vue.js components but this technique should apply to other frameworks as well.
You can find the code for this solution here.
Injecting Inertia page props
One of the main issues is when components need the Inertia page props. Let’s say we have the following component:
src/components/HelloWorld.vue
<template> <div>Hello World: {{ message }}</div></template>
<script setup lang="ts">import { usePage, router } from '@inertiajs/vue3'import { computed } from 'vue'
const page = usePage()const message = computed(() => page.props.message)</script>
If we were to use this component in a Story:
src/components/HelloWorld.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3'import HelloWorld from './HelloWorld.vue'
const meta = { component: HelloWorld } satisfies Meta<typeof HelloWorld>
export default metatype Story = StoryObj<typeof meta>
export const Main: Story = {}
We would get the following error as page.props
would be undefined.
Error: Cannot read properties of undefined (reading 'message')
To make this work, we need to find a way for Storybook to set the Inertia page props. If we take a look at the Inertia source code:
//...const page = ref<Page<any>>(null)// ...const App: InertiaApp = defineComponent({ //... setup({ initialPage, initialComponent, resolveComponent, titleCallback, onHeadUpdate, }) { component.value = initialComponent ? markRaw(initialComponent) : null page.value = initialPage
//... if (!isServer) { router.init({ initialPage, resolveComponent, swapComponent: async (args: VuePageHandlerArgs) => { component.value = markRaw(args.component) page.value = args.page key.value = args.preserveState ? key.value : Date.now() }, }) } //... },})
export function usePage<SharedProps extends PageProps>(): Page<SharedProps> { return reactive({ props: computed(() => page.value?.props), //... })}
We can see that the page
variable is only set intially on setup or when the router swaps the component. The approach we will use is to
set the initial page props for each story.
Story wrapper
We’ll begin by creating a Storybook wrapper component:
.storybook/InertiaWrapper.vue
<template> <div id="inertia" /> <slot v-if="isReady" name="story" /></template>
<script setup lang="ts">import { createApp, h, onMounted, ref } from 'vue'import { createInertiaApp } from '@inertiajs/vue3'
export interface Props { inertia: Record<string, unknown>}
const { inertia } = defineProps<Props>()
const isReady = ref(false)
onMounted(() => createInertiaApp({ id: 'inertia', page: inertia, resolve: () => ({ render: () => h('div') }), setup: ({ App, props, el }) => { createApp({ render: () => h(App, props) }).mount(el) isReady.value = true }, }),)</script>
There are several points to note:
- The wrapper components accepts an object that will contain the inertia props needed for the story to work and those are passed into the
page
parameter ofcreateInertiaApp
. - Inside the
setup
callback we create a dummy Vue app, which then renders and mounts Inertia’sApp
component, which as shown in the previous section executes the line: createInertiaApp
is called when after mounting and we set theisReady
ref to true after we have finished the Inertia setup to prevent the story (and hence our HelloWorld component) from being rendered too early.- We are also passing a dummy function for the
resolve
parameter as we won’t be using the router.
Adding a decorator
.storybook/preview.ts
import { h } from 'vue'import InertiaWrapper from './InertiaWrapper.vue'
const DEFAULT_INERTIA_PROPS = { storage: { url: 'http://example.com', },}
export const withInertiaWrapper = (storyFn, context) => { const inertia = { props: { ...DEFAULT_INERTIA_PROPS, ...(context.globals.inertia || {}) }, url: '', } const story = storyFn()
return () => { return h( InertiaWrapper, { inertia }, { story: () => h(story, { ...context.args }), }, ) }}
export const decorators = [withInertiaWrapper]
The DEFAULT_INERTIA_PROPS
contains inertia props that all components can use and is then merged by the inertia
property inside context.globals
. The InertiaStoryWrapper
then wraps the story with the merged props passed in.
Updating the story
We can now update our story for our HelloWorld component
src/components/HelloWorld.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3'import HelloWorld from './HelloWorld.vue'
const meta = { component: HelloWorld, globals: { inertia: { message: 'This is Inertia.js in Storybook', }, },} satisfies Meta<typeof HelloWorld>
export default metatype Story = StoryObj<typeof meta>
export const Main: Story = {}
Setting the inertia
property in the globals context allows our decorator to pass it on to the wrapper and onto Inertia.js.
You can now see the message displayed in the Story.