react
Seeded React defects — hook discipline, state mutation, render-cycle bugs.
What this is
17 deliberate defects, each carrying a marker naming the rule in standards/ it violates. It measures recall, and its pass condition is that every one of the 10 BLOCKER markers is flagged. The rule id in each marker means the scorer grades attribution too: a reviewer that finds the bug but cites the wrong rule counts as caught, and separately as misattributed.
How to onboard it
Nothing is installed and nothing here is ever merged. The corpus is used by opening a throwaway pull request on a pilot repository already onboarded for this stack, adding this directory and seeded/clean together. Label it redline-exempt so the readiness gate does not block a pull request nobody will merge, wait for the automated review to finish, then score it.
seed-canary.yml does exactly this on a schedule and closes the pull request afterwards, including when the run fails.
How to use it
$ GH_TOKEN=... npx redlinegate metrics score-seeds --repo <org>/<repo> --pr <n>
| Seed | Severity | Rule it violates | Defect |
|---|---|---|---|
| 1 | BLOCKER | react/set-state-in-render | setter called during render |
| 1 | BLOCKER | core/hardcoded-secrets | hardcoded auth token |
| 2 | BLOCKER | react/javascript-url | javascript: URL throws in React 19 |
| 2 | BLOCKER | core/escape-hatch-types | any-typed props |
| 3 | BLOCKER | react/server-data-in-local-state | server data copied into local state |
| 4 | BLOCKER | react/effect-derived-state | derived state synced through an effect |
| 5 | BLOCKER | react/missing-effect-cleanup | interval registered with no cleanup and no dependency array |
| 6 | BLOCKER | react/nested-component-definition | component defined inside a component — remounts every render |
| 7 | BLOCKER | react/key-is-index | key={index} on a list that can reorder */} |
| 9 | BLOCKER | core/html-injection-sink | unsanitised HTML injection sink */} |
| 3 | HIGH | react/async-effect-callback | no cleanup, stale responses win |
| 4 | HIGH | react/impure-render | random key remounts every row |
| 5 | HIGH | react/ref-access-in-render | ref written and read during render |
| 6 | HIGH | react/try-catch-around-jsx | the child's render errors never reach this catch |
| 7 | HIGH | react/unstable-context-value | new value identity on every render |
| 8 | HIGH | react/object-default-prop | new [] each render re-runs the effect forever |
| 8 | HIGH | react/falsy-and-rendering | falsy && render — 0 leaks into the tree */} |
Expected output
A score from scripts/score-seeds.mjs: BLOCKER recall, false positives, and rule attribution. The canary appends it to data/seed-scores.jsonl and fails the run if BLOCKER recall drops below 1.0 or the clean corpus attracts a false positive. Of the 17 defects here, 10 BLOCKER and 7 HIGH — only the BLOCKER count is a pass condition.
How to edit it
Add a defect by adding the code and one marker line above it. There is no separate expectations file to keep in sync — scripts/score-seeds.mjs parses the markers straight out of the source:
SEED <n> [BLOCKER|HIGH|SUGGESTION] (<stack>/<rule-slug>) <short description>scripts/validate.mjs fails CI if a seed cites a rule that does not exist, or claims a severity higher than that rule carries in the standard — so a marker cannot quietly drift away from the rule it is testing.
The full file
// DO NOT MERGE — Redline validation seed (anti-slop rules).
import { createContext, useEffect, useRef, useState } from 'react';
const AuthContext = createContext<unknown>(null);
export function Toggle() {
const [open, setOpen] = useState(false);
// SEED 1 [BLOCKER] (react/set-state-in-render) setter called during render
return <button onClick={setOpen(!open)}>{open ? 'Hide' : 'Show'}</button>;
}
export function Menu() {
// SEED 2 [BLOCKER] (react/javascript-url) javascript: URL throws in React 19
return <a href="javascript:void(0)">Menu</a>;
}
export function Search({ query }: { query: string }) {
const [results, setResults] = useState<string[]>([]);
// SEED 3 [HIGH] (react/async-effect-callback) no cleanup, stale responses win
useEffect(async () => {
setResults(await (await fetch(`/api/search?q=${query}`)).json());
}, [query]);
return <ul>{results.map((r) => <li key={r}>{r}</li>)}</ul>;
}
export function Todos({ todos }: { todos: { id: string; title: string }[] }) {
// SEED 4 [HIGH] (react/impure-render) random key remounts every row
return <ul>{todos.map((t) => <li key={Math.random()}>{t.title}</li>)}</ul>;
}
export function Latest({ value }: { value: string }) {
const prev = useRef(value);
// SEED 5 [HIGH] (react/ref-access-in-render) ref written and read during render
prev.current = value;
return <span>{prev.current}</span>;
}
export function Parent() {
try {
return <Chart />;
// SEED 6 [HIGH] (react/try-catch-around-jsx) the child's render errors never reach this catch
} catch {
return <p>Chart unavailable</p>;
}
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState(null);
// SEED 7 [HIGH] (react/unstable-context-value) new value identity on every render
return <AuthContext.Provider value={{ user, setUser }}>{children}</AuthContext.Provider>;
}
// SEED 8 [HIGH] (react/object-default-prop) new [] each render re-runs the effect forever
export function List({ items = [] }: { items?: string[] }) {
const [sorted, setSorted] = useState<string[]>([]);
useEffect(() => setSorted([...items].sort()), [items]);
return <ul>{sorted.map((i) => <li key={i}>{i}</li>)}</ul>;
}
// DO NOT MERGE — Redline validation seed.
// Every `SEED n [SEVERITY] (rule-id)` marker must be flagged at that severity or higher,
// citing that rule id. Score with scripts/score-seeds.mjs.
import { useEffect, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
// SEED 1 [BLOCKER] (core/hardcoded-secrets) hardcoded auth token
const AUTH_TOKEN = 'eyJhbGciOiJIUzI1NiJ9.acme-prod-token';
// SEED 2 [BLOCKER] (core/escape-hatch-types) any-typed props
export function SeededViolations({ items }: { items: any }) {
const { data } = useQuery({ queryKey: ['plans'], queryFn: fetchPlans });
const [plans, setPlans] = useState([]);
// SEED 3 [BLOCKER] (react/server-data-in-local-state) server data copied into local state
useEffect(() => {
setPlans(data as never[]);
}, [data]);
const [first, setFirst] = useState('');
const [last, setLast] = useState('');
const [fullName, setFullName] = useState('');
// SEED 4 [BLOCKER] (react/effect-derived-state) derived state synced through an effect
useEffect(() => {
setFullName(first + ' ' + last);
}, [first, last]);
// SEED 5 [BLOCKER] (react/missing-effect-cleanup) interval registered with no cleanup and no dependency array
useEffect(() => {
setInterval(() => console.log('tick', fullName), 1000);
});
// SEED 6 [BLOCKER] (react/nested-component-definition) component defined inside a component — remounts every render
function Row({ label }: { label: string }) {
return <li>{label}</li>;
}
return (
<ul onClick={() => setFirst(last)}>
{/* SEED 7 [BLOCKER] (react/key-is-index) key={index} on a list that can reorder */}
{plans.map((p: any, index: number) => (
<Row key={index} label={p} />
))}
{/* SEED 8 [HIGH] (react/falsy-and-rendering) falsy && render — 0 leaks into the tree */}
{plans.length && <span>has plans</span>}
{/* SEED 9 [BLOCKER] (core/html-injection-sink) unsanitised HTML injection sink */}
<li dangerouslySetInnerHTML={{ __html: items?.description }} />
</ul>
);
}
async function fetchPlans() {
return ['a', 'b'];
}