Skip to content

2bTwist/component-previewer

Repository files navigation

component-previewer

Render a single React Native component in isolation, in your app's real provider shell, with chosen props/state, without booting the rest of the app. Uses Storybook-compatible CSF (*.stories.tsx) with zero setup (no .rnstorybook/, no codegen manifest, no Metro wrapper), and runs on both surfaces: the iOS/Android simulator (true native) and a browser (via react-native-web).

The differentiator: your real AppShell wraps every story (single source of truth, can't drift), and CSF decorators run inside it. Edit a story's args live on-device with the built-in controls panel.

Install

pnpm add -D component-previewer        # or npm i -D / yarn add -D

Peer dependencies: react, react-native.

Use it in your own app (the contract)

  1. Expose your real provider shell as one component (theme, query client, i18n, safe-area, …):
    // AppProviders.tsx
    export function AppProviders({ children }) { /* your real providers */ }
  2. Write CSF stories (*.stories.tsx), the exact Storybook format, no import from this tool needed:
    // Button.stories.tsx
    import { Button } from './Button';
    export default { title: 'Button', component: Button, args: { label: 'Press me' } };
    export const Primary = { args: { label: 'Buy now' } };
    export const Disabled = { args: { disabled: true } };
    // optional per-story providers run INSIDE your real shell:
    // export const Boxed = { args: {...}, decorators: [(Story) => <View>…<Story/></View>] };
  3. Wire the boot flag at your app root (~10 lines). Discovery is zero-codegen:
    import { Previewer, fromRequireContext } from 'component-previewer';
    
    const ctx = require.context('./', true, /\.stories\.tsx$/);            // Metro / native + Expo web
    const PREVIEW = process.env.EXPO_PUBLIC_PREVIEW === '1';
    const Shell = ({ children }) => <AppProviders>{children}</AppProviders>;
    
    export default function App() {
      return PREVIEW
        ? <Previewer stories={fromRequireContext(ctx)} shell={Shell} />
        : <YourApp />;
    }
    On a pure-Vite host, use fromGlob(import.meta.glob('./**/*.stories.tsx', { eager: true })) instead of fromRequireContext.

Live props controls

Open any story and tap Controls to edit its args on-device and re-render live (the Storybook-controls equivalent). Control kinds are inferred from the arg values with zero config:

value type control
string text field
string like #rrggbb color swatch + hex field
number numeric field
boolean switch

Functions, plain objects, arrays, null/undefined are skipped by inference (passed through untouched). Declare optional CSF argTypes to force a control — a dropdown, a range, an explicit color, or an editable object/array:

export default {
  title: 'Badge',
  component: Badge,
  args: { tone: 'neutral', accent: '#3366ff', data: { rows: 3 } },
  argTypes: {
    tone: { control: 'select', options: ['neutral', 'success', 'danger'] },
    size: { control: 'range', min: 16, max: 256, step: 8 }, // clamped number field
    accent: { control: 'color' },                            // swatch + hex (also auto-inferred for #hex values)
    data: { control: 'object' },                             // JSON editor (objects + arrays), commits on valid JSON
  },
};

A Reset button restores the story's declared args; Copy logs the current args as JSON (pass onCopyArgs to wire a real clipboard). Edits never leak between stories.

Args persistence

Edited args persist per story id across list ↔ stage navigation and Fast Refresh, with zero dependencies (an in-memory session store). To also survive a cold reload, pass a persistence adapter — the package never imports a storage lib itself:

import { Previewer, type ArgsPersistence } from 'component-previewer';

// any synchronous get/set/remove around AsyncStorage, MMKV, a file, etc.
const persistence: ArgsPersistence = { get, set, remove };

<Previewer stories={} shell={Shell} persistence={persistence} />

Globals — parameterize the one shell (theme, locale, …)

Args are per-component props. Globals are toolbar-level params that vary the shell across every story — the canonical case is light/dark. Declare them on <Previewer globalTypes> and read them in your shell:

const Shell: ShellComponent = ({ children, globals }) => (
  <AppProviders mode={globals?.theme === 'light' ? 'light' : 'dark'}>{children}</AppProviders>
);

<Previewer
  stories={}
  shell={Shell}
  globalTypes={{ theme: { options: ['dark', 'light'], default: 'dark' } }}
/>

A Globals section appears at the top of the Controls panel; switching theme re-renders the current story through the same shell. This is what removes the two classic sources of duplication:

  • No forked light/dark stories. One story, toggled — not export const Light + export const Dark with a hand-written dark decorator.
  • No separate "lab" component with its own switchers. Variant axes (a card's state, a chart's data) belong in args/argTypes and drive the Controls panel; theme/locale belong in globalTypes. There's nothing left for a bespoke lab harness to do.

Single source of truth (no shell drift)

The differentiator is that your real AppProviders is the shell, so the preview can't drift from the app. Globals exist specifically so you never have to fork that shell into a lighter "preview-only" copy to vary the theme — vary it with a global instead. The package deliberately can't auto-detect a forked shell (it can't see your app root), so the contract is the guard: import your real providers as shell; parameterize with globalTypes; never re-author.

Run the example

The repo ships an Expo example app.

Native (simulator / device):

cd example
EXPO_PUBLIC_PREVIEW=1 npx expo start   # press i, or scan with Expo Go

Boots into a searchable picker of discovered stories; selecting one renders it natively inside the real shell. This is the surface that shows Skia/glass/native modules (the example's BlurCard story renders a real native blur).

Browser (no simulator):

EXPO_PUBLIC_PREVIEW=1 npx expo start --web

Same picker via react-native-web. Native modules can't render here (documented cap).

Without the flag, the app boots its normal screen instead. Deep-link straight into one story with EXPO_PUBLIC_PREVIEW_STORY=button--primary.

API

  • fromRequireContext(ctx) / fromGlob(glob)StoryEntry[] — zero-codegen discovery.
  • <Previewer stories={…} shell={…} initialStoryId? /> — the picker + stage UI; initialStoryId (or EXPO_PUBLIC_PREVIEW_STORY) deep-links straight into one story.
  • inferControls(args, argTypes?)Control[] — pure; the control descriptors that drive the on-device panel.
  • composeStory(entry, Shell?, args?)Shell (outer) > decorators (inner) > story; pure. Pass args to render an override (the live-edit path).
  • parseCsfModule(id, module)StoryEntry[] — parse one CSF module to entries.

Exported types: StoryEntry, Meta, Story, Args, ArgType, ArgTypes, ControlKind, Control, Decorator, ShellComponent, RequireContext, GlobModules, CsfModule.

Develop the library

This is a pnpm workspace: the example/ app consumes the library source via Metro (bob's metro-config + the source export condition), so editing src/ hot-reloads in the example, no rebuild or repack.

pnpm install     # once — links the workspace
pnpm lint        # eslint (typescript-eslint)
pnpm typecheck   # tsc --noEmit
pnpm test        # vitest (CSF parsing, discovery, shell compose, control inference)
pnpm build       # react-native-builder-bob → lib/module (ESM) + lib/typescript (.d.ts)

Build/packaging: react-native-builder-bob (ESM module + typescript targets), exports map with a source condition, MIT licensed. See CONTRIBUTING.md.

Status

  • Core: CSF parsing, zero-codegen discovery (require.context / import.meta.glob), registry, real-shell compose. Pure and unit-tested.
  • Native backend: boot-flag entry swap + in-app picker + real-shell render + deep-link, verified on the iOS simulator (including a native expo-blur story).
  • Web backend: renders the same CSF stories via react-native-web (native-module cap documented).
  • Live props controls: value-inferred text/number/boolean/color + CSF argTypes select/range/color/object, on-device, live. Edits persist per story (session + optional cold-reload adapter); Copy/Reset.
  • Roadmap: TS-type-driven prop controls (auto-infer from prop types), VS Code extension, more CSF decorator edge cases.

License

MIT

About

Render a single React Native component instantly in its app's real provider shell, without booting the app.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors