frontend/src/stories/How to build a scene.stories.mdx
import { Meta } from '@storybook/addon-docs'
<Meta title=" How to build a scene?" />If you want to add a new scene in the PostHog App frontend, here are 7 easy steps for fun and profit.
But first, you must answer one question: Does your scene depend on an id in the URL, like /dashboard/:id?
id in the URLCreate a component like: frontend/src/scenes/dashboard/Dashboards.tsx
import { dashboardsLogic } from './dashboardsLogic'
import { SceneExport } from 'scenes/sceneTypes'
import { useValues } from 'kea'
export function Dashboards (): JSX.Element {
const {
counter
} = useValues(dashboardsLogic)
return (
// TODO: consolidate on a recommended naming convention
<div className='dashboard-scene'>
Dashboard Scene {counter}!
</div>
)
}
export const scene: SceneExport = {
component: Dashboards,
logic: dashboardsLogic,
}
Create the logic: frontend/src/scenes/dashboard/dashboardsLogic.tsx
import { kea, path, reducers } from 'kea'
export const dashboardsLogic = kea([
path(['scenes', 'dashboard', 'dashboardsLogic']),
reducers({
counter: [1, {}],
}),
])
Create the styles frontend/src/scenes/dashboard/Dashboards.scss.
.dashboards-scene {
// put all your styles inside this scope, as everything is global
}
Run kea type generation and check, which will created dashboardsLogicType.ts and update imports in dashboardsLogic.tsx
pnpm typegen:write && pnpm typescript:check
in frontend/src/scenes/urls.ts
export const urls = {
dashboards: () => `/dashboard`,
}
in frontend/src/scenes/sceneTypes.ts
export enum Scene {
Dashboards = 'Dashboards',
}
in frontend/src/scenes/scenes.ts
export const sceneConfigurations: Partial<Record<Scene, SceneConfig>> = {
[Scene.Dashboards]: {
projectBased: true,
name: 'Dashboards',
},
}
export const routes: Record<string, Scene> = {
[urls.dashboards()]: Scene.Dashboards,
}
in frontend/src/scenes/appScenes.ts
export const appScenes: Record<Scene, () => any> = {
[Scene.Dashboards]: () => import('./dashboard/Dashboards'),
}
id in the URL (/dashboard/:id)Create a component like: frontend/src/scenes/dashboard/Dashboard.tsx
import { dashboardLogic } from './dashboardLogic'
import { SceneExport } from 'scenes/sceneTypes'
import { useValues } from 'kea'
export const scene: SceneExport = {
component: Dashboard,
logic: dashboardLogic,
// paramsToProps - Convert url _string_ params to logic props.
// This mounts the right logic with turbo mode before the component renders.
// This wraps the scene's logic in <BindLogic />
paramsToProps: ({ params: {id} }:{ params: { id?: string }}) => ({ id: id ? parseInt(id) : 'new' }),
}
export function Dashboard ({ id }: { id?: string } = {}): JSX.Element {
// dashboardLogic is automatically bound to the props above with BindLogic
const {
counter
} = useValues(dashboardLogic)
return (
// TODO: consolidate on a recommended naming convention
<div className='dashboard-scene'>
Dashboard Scene {id} {counter}!
</div>
)
}
Create the logic: frontend/src/scenes/dashboard/dashboardLogic.tsx
import { kea, key, path, props, reducers } from 'kea'
export interface DashboardLogicProps {
id: number | 'new'
}
export const dashboardLogic = kea([
props({} as DashboardLogicProps),
key(({ id }) => id),
path((id) => ['scenes', 'dashboard', 'dashboardLogic', id]),
reducers({
counter: [1, {}],
}),
])
Create the styles frontend/src/scenes/dashboard/Dashboard.scss.
.dashboard-scene {
// put all your styles inside this scope, as everything is global
}
Run kea type generation and check, which will created dashboardLogicType.ts and update imports in dashboardLogic.tsx
pnpm typegen:write && pnpm typescript:check
in frontend/src/scenes/urls.ts
export const urls = {
dashboard: (id: string | number) => `/dashboard{id ? `/${id}` : ''}`,
}
in frontend/src/scenes/sceneTypes.ts
export enum Scene {
Dashboard = 'Dashboard',
}
in frontend/src/scenes/scenes.ts
export const sceneConfigurations: Partial<Record<Scene, SceneConfig>> = {
[Scene.Dashboard]: {
projectBased: true,
name: 'Dashboard',
},
}
export const routes: Record<string, Scene> = {
// this `:id` gets used in "params" in "paramsToProps" and passed to the <Dashboard /> component
[urls.dashboard(':id')]: Scene.Dashboard,
}
in frontend/src/scenes/appScenes.ts
export const appScenes: Record<Scene, () => any> = {
[Scene.Dashboard]: () => import('./dashboard/Dashboard'),
}
In the same folder as your component, create a file like frontend/src/scenes/dashboard/Dashboard.stories.tsx.
If you need a lot of .json files for mocked data, create a __mocks__ subdirectory for those files, just like you would with jest.
import { Meta, StoryObj } from '@storybook/react'
import { router } from 'kea-router'
import { useEffect } from 'react'
import { useOnMountEffect } from 'lib/hooks/useOnMountEffect'
import { App } from 'scenes/App'
import { urls } from 'scenes/urls'
import { mswDecorator, useStorybookMocks } from '~/mocks/browser'
export default {
title: 'Scenes-App/Dashboard',
decorators: [
// mocks used by all stories in this file
mswDecorator({
get: {
'/api/projects/1/dashboards/': require('./__mocks__/dashboards.json'),
'/api/projects/1/dashboards/1/': require('./__mocks__/dashboard1.json'),
'/api/projects/1/dashboards/1/collaborators/': [],
},
}),
],
// NB! These `parameters` only apply for Scene stories.
parameters: { layout: 'fullscreen', options: { showPanel: false }, viewMode: 'story' }, // scene mode
} as Meta
export function NewDashboard(): JSX.Element {
// mocks used only in this story
useStorybookMocks({
get: { '/api/projects/dashboard/2/': require('./__mocks__/dashboard2.json') },
})
useOnMountEffect(() => {
// change the URL
router.actions.push(urls.dashboard(2))
// call various other actions to set the initial state
newDashboardLogic.actions.showNewDashboardModal()
})
return <App />
}
Read next: