Component Generation
Scaffold new UI components with the right structure, tests, and Storybook stories from a template.
Why
Every component in a design system should have the same structure: props interface, forwardRef, a Storybook story with all variants, and a unit test. Hand-creating these 4 files for every new component wastes 15 min and introduces inconsistency — some components get tests, some don't, some use forwardRef, some forget. A generator (plop, hygen, or a custom script) produces all 4 files from a template in 5 seconds, enforcing the same structure every time. This is how design systems scale to 100+ components without becoming a maintenance nightmare: the structure is uniform, so onboarding to a new component is zero-effort.
When
When you have ≥10 components or a design system. Takes 1 h to set up plop. Skip for one-off components.
How
1. Install plop and create a plopfile with a component generator - Prompts: component name, directory (atoms/molecules/organisms), hasVariants - Templates: Component.tsx, Component.test.tsx, Component.stories.tsx, index.ts 2. Template the Component.tsx with the standard structure - React.forwardRef, typed props interface, className merge with cn() - Export as named + default; include a displayName for DevTools 3. Template the Storybook story with at least: default, with className, disabled - Use args composition so variants inherit base args - Add a docsOnly story for documentation without rendering 4. Template the test with a render + snapshot + a behavior test - render(<Component />) should not throw - Test the key interaction (click, input) — not just that it renders 5. Wire into package.json: "gen:component": "plop component" - Run it, answer 3 prompts, get 4 files that all pass lint+test on first try
Example
// plopfile.ts
export default function (plop) {
plop.setGenerator("component", {
prompts: [
{ type: "input", name: "name", message: "Component name (PascalCase)" },
{ type: "list", name: "level", choices: ["atoms", "molecules", "organisms"] },
],
actions: [
{ type: "add", path: "src/components/{{level}}/{{name}}/{{name}}.tsx", templateFile: "templates/Component.tsx.hbs" },
{ type: "add", path: "src/components/{{level}}/{{name}}/{{name}}.test.tsx", templateFile: "templates/Component.test.tsx.hbs" },
{ type: "add", path: "src/components/{{level}}/{{name}}/{{name}}.stories.tsx", templateFile: "templates/Component.stories.tsx.hbs" },
],
});
}
// templates/Component.tsx.hbs
import { forwardRef } from "react";
export interface {{name}}Props { className?: string; }
export const {{name}} = forwardRef<HTMLDivElement, {{name}}Props>(function {{name}}({ className }, ref) {
return <div ref={ref} className={className} />;
});
{{name}}.displayName = "{{name}}";Common Problems
- ⚠Templates that drift from the actual project conventions, so generated code needs manual fixing every time.
- ⚠Generating only the .tsx file without tests or stories, so the component exists but is undocumented and untested.
- ⚠Not handling compound components (Select + Option) where one generator should produce multiple related files.
- ⚠Forgetting to update the barrel export (index.ts) after generating, so the component isn't importable.
Best Practices
- ✓Keep the templates in the repo and review changes to them like any other code — they generate code, so they're high-leverage.
- ✓Add a post-generation step that runs lint+test on the new files and fails if they don't pass — catches template drift.
- ✓Support both simple and compound components in one generator with a 'compound' prompt that adds sub-files.
- ✓Generate the Storybook story with autodocs enabled so the component is documented from day one.