Refactor a large React component file into a directory of smaller files. Use when the user asks to break up, split, or refactor a big component file.
Break a monolithic React component file into a well-organized directory with no functionality changes.
The target component is: $ARGUMENTS
If no component is specified, ask the user which file to refactor.
Before touching any code, verify that the component's key user flows are covered by E2E tests. Search for Playwright test files (typically in packages/app/tests/e2e/) that exercise the component — look for references to its data-testid attributes, page object methods, or user-visible behaviour (e.g. clicking Run, Save, switching chart types, SQL mode).
playwrightThis step is critical — E2E tests are the strongest guarantee that the refactor doesn't break real user flows. Unit tests alone cannot catch issues like missing DOM elements that only appear when the full app is composed.
Read the entire target file. Identify:
__tests__/ directory that imports the targetCreate a plan listing each new file, what goes in it, and which imports change. Present the plan to the user and wait for approval before writing code.
The new directory is created inside packages/app/src/components/, replacing the original file. It keeps the same name (without extension), so external imports resolve unchanged.
packages/app/src/components/ComponentName/
index.ts — barrel re-export of the default export
ComponentName.tsx — main component (default export)
SubComponent.tsx — one file per sub-component (named export)
utils.ts — utility functions, schemas, constants
__tests__/ — moved test files with updated imports
index.ts is the single public API of the directory. All exports that consumers depend on must be re-exported through index.ts. Sub-components or utilities that are only used within the directory should not be re-exported../ relative paths. Files outside the directory must never import directly from a sub-file (e.g. ComponentName/ChartActionBar) — they import from the directory barrel (ComponentName) only.../Foo, ./Foo) in the extracted files must be converted to @/-prefixed absolute imports (e.g. @/components/Foo). This is critical — files moved one level deeper will break if relative paths aren't updated.@/-prefixed imports stay unchanged.Move __tests__/ComponentName.test.tsx into ComponentName/__tests__/ComponentName.test.tsx.
Update in the test file:
from '../ComponentName' → from '..'../SiblingComponent → ../../SiblingComponent (one extra level up)Remove the original monolithic .tsx file only after all new files are written.
After the initial split, clean up duplication and improve structure within the new directory:
function FooComponent and then aliased as export const Foo = FooComponent, rename the function to Foo and export it directly.validateAndNormalize) that returns { errors, config } so each caller only contains its unique logic.renderComponent helpers in test files — hoist a single shared factory to file scope. Describe blocks with special defaults can wrap it in a thin helper (e.g. renderAlertComponent) that passes overrides.Scan component files for logic that can move into utils.ts:
useMemo callbacks — if the memo body is a pure transformation (no hooks, no JSX), extract it as a named function in utils.ts and call it from the memo.['table', 'time', 'number', 'pie'].includes(tab)) become named exports (e.g. TABS_WITH_GENERATED_SQL).DisplayType → tab string) become standalone functions. These don't need useMemo since they're cheap.Good extraction candidates:
buildSampleEventsConfig, buildChartConfigForExplanations)displayTypeToActiveTab)computeDbTimeChartConfig)Leave in the component:
setValue, onSubmit, etc.All components should use named type aliases for their props, declared directly above the component function:
type FooProps = {
bar: string;
onBaz: () => void;
};
export function Foo({ bar, onBaz }: FooProps) {
Do not use inline object types in the function signature.
If a file exceeds ~500 lines after the initial split, look for natural extraction points:
ChartEditorControls.tsxChartPreviewPanel.tsxWhen splitting JSX, the extracted component receives form state via props (control, setValue, etc.). Watches that are only used in the extracted component (e.g. alertChannelType) can use useWatch inside it rather than being passed as props.
Before moving JSX into a new component, map the render tree's branching structure. Every piece of JSX must be classified as either:
&&, if/else)Extract from the bottom up: pull out the conditional/branch-specific parts first, leaving shared sections in the parent. Never bundle shared JSX into a component that only renders for one branch.
Verify after extraction: for each value of every conditional, confirm the new code renders the same set of components as the original. A toolbar, action bar, or footer that appeared for all chart types must still appear for all chart types after the split.
This is a dedicated step — do not skip it or fold it into another step.
utils.tsWrite unit tests in __tests__/utils.test.ts covering:
Write unit tests for each extracted component in __tests__/<ComponentName>.test.tsx covering:
activeTab, isRawSqlInput, isSaving produce the correct UI state (disabled buttons, hidden sections, etc.)Use a FormWrapper test helper when the component requires react-hook-form's control/handleSubmit — create a small wrapper that calls useForm with sensible defaults and passes the form methods to the component under test via a render-prop pattern. Mock heavy child components (chart renderers, SQL editors, etc.) with simple stubs that render a data-testid.
Run in order — fix any failures before proceeding to the next step:
make ci-lint # TypeScript compilation + lint
cd packages/app && yarn ci:unit # Unit tests (or the appropriate package)
make e2e # E2E tests — re-run the same tests from step 1 to catch regressions
If lint fails with import-sort or formatting errors, run npx eslint --fix <file> on the affected files and re-check. If there are unused-import warnings, remove the unused imports manually.
If an E2E test fails, investigate whether the failure is related to the refactoring (e.g. a broken import path at runtime) or a pre-existing flake. Fix refactoring-related failures before presenting the result. The E2E tests identified in step 1 are the most important — if those pass, the refactor is safe.
index.ts barrel makes the directory a drop-in replacement@/ prefixed./utils.tsutils.ts functionsmake ci-lint passes