Skip to content

Getting started with React ​

Installation ​

shell
pnpm i -D poveste @poveste/plugin-react
# OR
npm i -D poveste @poveste/plugin-react
# OR
yarn add -D poveste @poveste/plugin-react

You also need react@^19.0.0 and react-dom@^19.0.0, and a JSX transform in your Vite config — @vitejs/plugin-react, which a React project built with Vite already has. React 18 is not supported.

Just installed and got an older version?

For about a day after a release, pnpm installs the previous version, with no error and no warning. Current pnpm prints nothing to say a newer one exists. That's pnpm's release-age cooldown, not a broken publish. A plugin that's new in that release is worse off: nothing comes before its 0.0.1 placeholder, which has no code, so pnpm installs that beside the previous poveste, and the only sign is a deprecated warning.

pnpm also saves the version it installed to package.json as ^x.y.z, so waiting a day doesn't fix an install you've already run. Run the install line again with one of these:

  • the npm line above;
  • --config.minimum-release-age=0 added to the pnpm line. Use the kebab-case spelling: pnpm 12.4 ignores --config.minimumReleaseAge=0 without a word;
  • exact versions: poveste@x.y.z and the plugin at the same version. pnpm installs them, saves them without the ^, and lists them under minimumReleaseAgeExclude in a pnpm-workspace.yaml it creates. @latest doesn't get you past the cooldown.

Poveste needs Node >=22.22.2, and npm will not tell you

Run node -v before you install. On an older Node, npm i poveste still succeeds: npm installs the newest earlier Poveste that accepts your Node, and on a recent Node the only warning it prints names a dependency, not Poveste. These docs then describe a version you do not have, and the difference looks like a bug rather than its cause. With engine-strict=true in your .npmrc, npm refuses with EBADENGINE instead. pnpm installs the current version, and Poveste then refuses to start, naming the Node it needs; Yarn 1 refuses to install.

On Node 24, use 24.15 or later: Poveste's DOM dependency (jsdom) accepts ^22.22.2 || ^24.15.0 || >=26.

Configuration ​

Keep @vitejs/plugin-react in your Vite config — Poveste reads it, so story files compile the way your components do, and editing a component refreshes it in place:

ts
// vite.config.ts
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react()],
})

Then add the plugin:

ts
// poveste.config.ts
import { HstReact } from '@poveste/plugin-react'
import { defineConfig } from 'poveste'

export default defineConfig({
  plugins: [HstReact()],
})

The plugin adds **/*.story.tsx and **/*.story.jsx to the files Poveste looks for, so there is no storyMatch to set.

React stories can sit beside Vue ones in the same book, but not beside Solid ones: both are .story.tsx files, and one plugin takes them all. See More than one framework in a book.

In a Vue project ​

A project made from Vite's Vue template compiles JSX for Vue: its tsconfig.app.json extends @vue/tsconfig, which sets "jsxImportSource": "vue", and Vite follows it. Your React components and stories then compile to Vue's vnodes, so the book builds and every React story shows "This component threw while rendering". The browser console says why:

  • under poveste dev, Objects are not valid as a React child (found: object with keys {__v_isVNode, __v_skip, type, props, …});
  • in a built book, Minified React error #31 with the same keys.

If the project has no Vue components written in TSX, set React's JSX in tsconfig.app.json:

json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "react"
  }
}

If it has some, leave the tsconfig as it is, because that change makes each of them render [object Object]. Start every React file with this line instead, the stories and the components they import alike:

tsx
/** @jsxImportSource react */

A pragma on the story file alone is not enough: the component it imports still compiles for Vue. Each way, used where it says, also satisfies vue-tsc, which reports type errors in the React files until one of them is in place.

Write a story ​

A story is a .story.tsx file whose default export describes it. Each variant has a render that returns JSX. Badge stands in for a component of your own; to follow along as written, this one is enough:

tsx
// src/Badge.tsx
import type { ReactNode } from 'react'

export function Badge({ tone, children }: { tone: 'info' | 'warn', children: ReactNode }) {
  return <span className={`badge badge-${tone}`}>{children}</span>
}
tsx
// src/Badge.story.tsx
import { defineStory } from '@poveste/plugin-react'
import { Badge } from './Badge'

export default defineStory({
  title: 'Badge',
  variants: [
    { title: 'Info', render: () => <Badge tone="info">Information</Badge> },
    { title: 'Warning', render: () => <Badge tone="warn">Careful</Badge> },
  ],
})

A story with no variants and a render of its own has one implicit variant. id, group, icon, layout and source work as they do for the other frameworks.

Command Line Interface ​

Poveste provides the following commands:

  • poveste dev: starts a development server with hot-reload
  • poveste build: builds the app for production
  • poveste preview: starts an HTTP server that serves the built app

You can add these to your package.json like this:

json
{
  "scripts": {
    "story:dev": "poveste dev",
    "story:build": "poveste build",
    "story:preview": "poveste preview"
  }
}

And then run them with npm run story:dev or npm run story:build.

You can specify additional CLI options like --port. For a full list of CLI options, run npx poveste --help in your project.

State ​

Give a story or a variant an initState, and render receives that state and a way to change it. Counter again stands in for your own component:

tsx
// src/Counter.tsx
export function Counter({ count, onIncrement }: { count: number, onIncrement: () => void }) {
  return <button type="button" onClick={onIncrement}>{`Count: ${count}`}</button>
}
tsx
// src/Counter.story.tsx
import { defineStory } from '@poveste/plugin-react'
import { Counter } from './Counter'

export default defineStory<{ count: number }>({
  title: 'Counter',
  initState: () => ({ count: 0 }),
  variants: [
    {
      title: 'From zero',
      render: ({ state, setState }) => (
        <Counter count={state.count} onIncrement={() => setState(s => ({ count: s.count + 1 }))} />
      ),
    },
    {
      title: 'From ten',
      initState: () => ({ count: 10 }),
      render: ({ state, setState }) => (
        <Counter count={state.count} onIncrement={() => setState(s => ({ count: s.count + 1 }))} />
      ),
    },
  ],
})

render is a component body: it runs again on every change, with state a fresh snapshot each time, so hooks work inside it as they would in any component. setState merges what you pass into the state, as a class component's setState does — the keys that change, or a function of the current state.

The state panel shows the state and lets you edit it, and an edit renders the story again with the new snapshot. A variant's own initState takes precedence over the story's.

Setup and wrappers ​

Export setupReact from your setup file to run code before every story mounts. addWrapper puts a component around each one — a theme provider, a router, a context:

tsx
// src/poveste.setup.tsx
import { defineSetupReact } from '@poveste/plugin-react'

export const setupReact = defineSetupReact(({ addWrapper }) => {
  addWrapper(({ children }) => <div className="story-frame">{children}</div>)
})
ts
// poveste.config.ts
export default defineConfig({
  plugins: [HstReact()],
  setupFile: '/src/poveste.setup.tsx',
})

The hook runs before the story mounts, so a wrapper is there for the first render. The first wrapper added is the outermost. A variant can set its own setupApp, called after the global hook with the same argument: { story, variant, addWrapper }.

A story is not rendered inside StrictMode. Add it as a wrapper if you want its checks.

What is not here yet ​

  • Controls. A variant with initState gets the state panel; one without says there is nothing to show. Controls built from your components — explicit ones and props detected automatically — come in later releases.
  • Generated source. The source panel shows the story file. React elements keep no source to generate per-variant code from, which is what the source panel reads in Vue.

Released under the MIT License. Updates on Bluesky and X.