Back to Posthog

How to build a scene?

frontend/src/stories/How to build a scene.stories.mdx

1.43.17.0 KB
Original Source

import { Meta } from '@storybook/addon-docs'

<Meta title=" How to build a scene?" />

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?

Option A: I'm buliding a global scene that does not depend on an id in the URL

1. Create the component, logic and styles

Create a component like: frontend/src/scenes/dashboard/Dashboards.tsx

ts
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

ts
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.

ts
.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

bash
pnpm typegen:write && pnpm typescript:check

2. Add a URL function

in frontend/src/scenes/urls.ts

ts
export const urls = {
  dashboards: () => `/dashboard`,
}

3. Add a scene to the enum

in frontend/src/scenes/sceneTypes.ts

ts
export enum Scene {
  Dashboards = 'Dashboards',
}

4. Add a scene configuration and a route to scene mapping

in frontend/src/scenes/scenes.ts

ts
export const sceneConfigurations: Partial<Record<Scene, SceneConfig>> = {
  [Scene.Dashboards]: {
    projectBased: true,
    name: 'Dashboards',
  },
}

export const routes: Record<string, Scene> = {
  [urls.dashboards()]: Scene.Dashboards,
}

5. Add a scene import

in frontend/src/scenes/appScenes.ts

ts
export const appScenes: Record<Scene, () => any> = {
  [Scene.Dashboards]: () => import('./dashboard/Dashboards'),
}

Option B: My scene depends on an id in the URL (/dashboard/:id)

1. Create the component, logic and styles

Create a component like: frontend/src/scenes/dashboard/Dashboard.tsx

ts
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

ts
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.

ts
.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

bash
pnpm typegen:write && pnpm typescript:check

2. Add a URL function

in frontend/src/scenes/urls.ts

ts
export const urls = {
    dashboard: (id: string | number) => `/dashboard{id ? `/${id}` : ''}`,
}

3. Add a scene to the enum

in frontend/src/scenes/sceneTypes.ts

ts
export enum Scene {
  Dashboard = 'Dashboard',
}

4. Add a scene configuration and a route to scene mapping

in frontend/src/scenes/scenes.ts

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,
}

5. Add a scene import

in frontend/src/scenes/appScenes.ts

ts
export const appScenes: Record<Scene, () => any> = {
  [Scene.Dashboard]: () => import('./dashboard/Dashboard'),
}

6. Create a story for your scene

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.

tsx
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 />
}

7. Add some components

Read next: