react-doctor/server-dedup-props
Pass the source array once and derive the projection on the client — passing both doubles RSC serialization bytes
- Category: Server
- Severity: warn
- Source:
oxlint-plugin-react-doctor - Framework: global
- Enabled when: always
Validation prompt
Use this to decide whether a fired diagnostic is real or a false positive.
Rule walks the attributes on a single JSXOpeningElement: it maps identifier-valued props by their identifier name, then reports any other prop whose value is a .toSorted(), .toReversed(), .filter(), .map(), or .slice() call chained off that same root identifier. Both projections then serialize across the RSC wire. Skip when the projection differs enough that client-side re-derivation would duplicate non-trivial server logic.
Fix prompt
Use this once validation confirms the diagnostic is real.
Send the source array once and derive the projection on the client with useMemo, or pre-derive on the server and drop the source prop entirely. Doubling the RSC payload to save a one-line client transform is rarely worth it. See https://react.dev/reference/rsc/use-client#serializable-types
Related rules
More Server rules from the rules reference:
react-doctor/server-fetch-without-revalidate: Pass `{ next: { revalidate: <seconds> } }` (or `cache: "no-store"` / `next: { tags: [...] }`) so stale cached data doesn't silently persistreact-doctor/server-hoist-static-io: Hoist the read to module scope: `const FONT_DATA = await fetch(new URL('./fonts/Inter.ttf', import.meta.url)).then(r => r.arrayBuffer())` runs once at module loadreact-doctor/server-no-mutable-module-state: Move per-request data into the action body, headers/cookies, or a request-scope (React.cache, AsyncLocalStorage). Module-scope `let`/`var` is shared across requests.react-doctor/server-sequential-independent-await: Wrap independent awaits in `Promise.all([...])` so they race instead of waterfalling — second call doesn't depend on the firstreact-doctor/server-after-nonblocking: `import { after } from 'next/server'` then wrap: `after(() => analytics.track(...))` — response isn't blocked