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
number numeric field
boolean switch

Functions, objects, arrays, null/undefined are skipped (passed through untouched). For a dropdown, declare optional CSF argTypes on a story (also used for ranges):

export default {
  title: 'Badge',
  component: Badge,
  args: { tone: 'neutral' },
  argTypes: { tone: { control: 'select', options: ['neutral', 'success', 'danger'] } },
};
// 'range' is supported too and renders as a clamped number field:
// argTypes: { size: { control: 'range', min: 16, max: 256, step: 8 } }

A Reset button restores the story's declared args. Edits never leak between stories.

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 + CSF argTypes select/range, on-device, live.
  • 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