Straitjacket

The React rules

Why Straitjacket enforces one component per file and keeps effects out of component files — and how the AST analysis works.

Four rules are React-specific and only ever fire on .tsx/.jsx, so they're inert in non-React repos. They're AST-based, built on OXC — the same parser and semantic analyzer used by the Rust JavaScript tooling ecosystem. This page covers one-component and effect-in-component; the two forwarding rules get their own page, Prop-drilling and drill depth.

Why AST, not regex

React structure isn't reliably detectable with pattern matching — you need to know what's a component, what's a hook call, which scope a binding lives in. OXC parses the file and builds a semantic model (scopes, symbols, references), and the rules ask questions of that model. That's what lets prop-drilling distinguish a forwarded-but-unused prop from one the component actually reads.

one-component

Flags more than one React component declared in a single .tsx/.jsx file.

One component per file keeps a codebase navigable: the file name tells you what's in it, imports stay honest, and a component can't quietly grow three siblings that should have been their own files. LLMs love to inline a little helper component right next to the one you asked for — this catches that.

effect-in-component

Flags a useEffect defined in a component's body.

The rule isn't "never use effects" — it's that effects belong in a named custom hook (useThing), not tangled inline into a component. The line is precise: an effect inside a use* hook is fine, even in the same file as a component. So you don't have to spray your codebase with one-hook-per-file — a component and its hooks can share a file; only the useEffect-directly-in-the-component adjacency is flagged. Anonymous components (memo(() => …), forwardRef) count as components.

The reasoning: effects are where the tricky, stateful, easy-to-get-wrong logic lives. Pulling each one into a named hook gives it a name, a home, and a testable boundary — and stops component bodies from accreting side effects that are hard to spot in review. LLMs reach for useEffect reflexively, often where derived state or an event handler would do; forcing the effect into a named hook makes each one a deliberate, visible choice. Whether the hook is reusable is beside the point — the value is that it's named and out of the component.

It's AST-based (OXC): straitjacket finds every useEffect call and checks whether its nearest enclosing function is a component (flag) or a hook (fine).

Turning them off

Both are on by default. Disable individually with --skip one-component or --skip effect-in-component. They never fire outside .tsx/.jsx, so there's nothing to disable in a non-React repo.

On this page