Targets TypeScript route modules importing from @remix-run/*. See remix-v2-data-flow for canonical patterns.
app/routes/ exporting loader, action, shouldRevalidate, or headers; components that consume useLoaderData, useActionData, useNavigation, useFetcher, useRevalidator, . markup, accessibility, useFetcher UI patterns) → covered by remix-v2-forms-review. Route module conventions, file naming, nested routing, error boundary placement → covered by remix-v2-routing-review.@remix-run/node (or @remix-run/cloudflare / @remix-run/deno) for server utilities; @remix-run/react for hooks and components.| Issue Type | Reference |
|---|---|
| ------------ | ----------- |
| Mutations in loader, missing validation, leaked server fields, throwing primitives, missing param checks | references/loaders.md |
Unvalidated FormData, json instead of redirect on success, missing error case, leaked actionData | references/actions.md |
useTransition v1 holdover, missing pending state, blanket shouldRevalidate: false, misused useRevalidator | references/revalidation.md |
defer for already-fast data, missing , no errorElement on , awaiting what should stream | references/defer-await.md |
loader, not useEffectactionrequest.formData() results are validated (zod/valibot/invariant) before useinternal_* fieldsuseLoaderData() uses the type annotation form (not as Foo)throw a Response (or json/redirect), never a plain Error or stringredirect(...) (PRG); validation failures return json({ errors }, { status: 400 })return nullparams.foo is checked with invariant / zod before useuseNavigation() / fetcher.state — no useTransitionformMethod comparisons use UPPERCASE ("POST", not "post")shouldRevalidate returns defaultShouldRevalidate by default; opt-outs are narrow and justifieddefer() is used only when at least one promise streams (no await before passing it) is wrapped in and has an errorElementuseRevalidator().revalidate() is reserved for focus/polling/SSE — not called immediately after a post or fetcher.submit (Remix already revalidates).These are correct Remix v2 usage and must not be reported as issues:
useEffect for client-only data — Loaders run server-side; localStorage, window dimensions, IntersectionObserver, and browser-only APIs belong in useEffect.loader returning null — A loader may legitimately return null (e.g. optional resource not present); flag only if it should be a 404 throw.useLoaderData() as type annotation — The is a generic parameter feeding SerializeFrom, not a as-style type assertion. Do not flag it as "unsafe cast."new Response(body, init) returns — v2 routes may return any Response; json() is an ergonomic wrapper, not a requirement. Non-JSON bodies (binary, text, streams) correctly skip json().return redirect(...) from an action — Both return redirect(...) and throw redirect(...) are legal in actions; throwing is required only from non-action helpers when you want to exit the calling function.loader declared without the request arg — Loaders may destructure only what they need ({ params }, { context }, or () with no args); the unused arg is not a bug.loader revalidated after an unrelated action — This is default Remix behavior, not a smell. Flag only if shouldRevalidate exists and is wrong.json({ errors }, { status: 400 }) — This is the canonical validation-error pattern (keeps the form route rendered with field errors). Not the same as the "no redirect on success" anti-pattern.useRevalidator for focus / polling / cross-tab sync — These are the documented use cases; only flag manual revalidate() calls that immediately follow a post or fetcher.submit Remix would already revalidate.SerializeFrom-induced type changes — Date typed as string, Map typed as {} after deserialization is correct wire-format behavior, not a typing bug.Only flag these issues when the specific context applies:
| Issue | Flag ONLY IF |
|---|---|
| ------- | -------------- |
Missing loader (using useEffect instead) | Data is available server-side and is NOT a browser-only API read |
loader returns a raw ORM object | The object contains fields a reviewer would not paste into a screenshot (passwords, tokens, internal flags) |
Action returns json on success | The action is invoked via causing a URL change — NOT via useFetcher |
| Missing pending UI | No nav.state / fetcher.state reference exists elsewhere in the file driving the same surface |
shouldRevalidate returns false | The body has no condition or never references formAction / currentParams / nextParams |
Manual useRevalidator().revalidate() | The call follows a Remix-managed mutation ( post, fetcher.submit) — not focus / polling / websocket |
defer() used | Every promise in the defer({...}) payload was already awaited before the call |
Run these in order. Do not draft user-facing findings until every gate passes for the batch you are about to report.
export async function loader|action are not reportable.useEffect is not loading client-only data; confirm a bare Response return is not intentionally non-JSON; confirm a loader returning null is not a legitimate optional read.as (assertion) — not useLoaderData() (annotation) and not useActionData() (annotation). The generic form is the documented safe path and must not be flagged.useTransition, transition.submission, fetcher.type, formMethod === "post" or formMethod==='post' (lowercase, any whitespace/quote variation), and LoaderArgs / ActionArgs. If present, the finding is a v1-holdover migration issue, not a missing-feature issue — label it accordingly.loader body, return shape, params, throws, or sensitive-field leaks → references/loaders.mdaction body, FormData validation, success/error branches, or PRG redirect → references/actions.mduseNavigation / useTransition migrations, shouldRevalidate, or useRevalidator use → references/revalidation.mddefer(), , , or streaming decisions → references/defer-await.mdloader, or is it stuck in a useEffect that defeats SSR and revalidation?password, token, internal_* fields) leak to the browser?request.formData() with a schema before touching the database?redirect(...) so refresh / back behaves correctly (PRG)?useLoaderData() (annotation) — not useLoaderData() as Foo (assertion)?useTransition, transition.submission, fetcher.type, lowercase formMethod, LoaderArgs / ActionArgs)?shouldRevalidate return a literal false, or does it reach for defaultShouldRevalidate and opt out narrowly?defer() used only when at least one promise is passed unresolved, and is every wrapped in with an errorElement?Complete Hard gates (especially gate 5), then report only issues that still pass the review-verification-protocol pre-report checks.
共 2 个版本