Agent Rules / Chimeras
Why does a React prop stop arriving after a rename?
A higher-order component injects a handler prop, the wrapped components are re-exported through export *, and pages import the wrapped version from the barrel. The component file shows a prop that nothing in that file supplies, and the page shows no prop at all. Use a hook inside each component, re-export by name, and give the payload a type.
The code below is from a small example project we wrote for this page. The break below is read from the files, by line; the tsc result in section 3 and the LLMGRAPH output in section 7 come from real runs on this code.
Note on before/: that folder holds the code with the section 3 edit already applied (the onEvent rename in withTracking.tsx), written that way on purpose. It is not the pre-edit state; the pre-edit text is quoted in section 1.
1. The setup
The pattern is a long-standing one and its strengths are real: cross-cutting behavior like analytics is written once, components stay free of tracking code, and a page just renders <PriceCard />. The app has two components, PriceCard and SignupButton, and a pricing page.
// before/src/components/withTracking.tsx (before the edit)
export function withTracking<P extends object>(
Component: ComponentType<P & { onTrack?: TrackFn }>,
) {
function Tracked(props: P) {
const onTrack: TrackFn = (payload) => track({ component: name, ...payload });
return <Component {...(props as P)} onTrack={onTrack} />;
}// before/src/components/PriceCard.tsx
export function PriceCardBase({ plan, priceCents, onTrack }: Props) { // line 9
...
onClick={() => onTrack?.({ action: "select_plan", label: plan, value: priceCents })} // line 12
...
export const PriceCard = withTracking(PriceCardBase); // line 19// before/src/components/index.ts
export * from "./PriceCard";
export * from "./SignupButton";// before/src/pages/Pricing.tsx
import { PriceCard, SignupButton } from "../components"; // line 12. Where the call path hides
- The prop has no source in its own file.
PriceCard.tsxdeclaresonTrack?at line 6 and calls it at line 12, and nothing there passes it. The value arrives fromwithTracking.tsxat render time. - The prop is optional on purpose. The test renders
PriceCardBasedirectly with its ownonTrack, so the local type saysonTrack?. The optional call does nothing when the prop is absent, and React gives no warning for a prop a component never received. - The barrel forwards more than the page uses.
export *re-exports every named export of each file, as the ECMAScript spec defines for star exports (see AR-02), soPriceCardBaseandPriceCardare both public. The page importsPriceCard, a name that appears in noexportline of the barrel. - The wrapper and the base have different names. A search for who renders
PriceCardBasefinds the test. The component production renders isPriceCard.
3. The edit that breaks it, and which call is left wrong
The tracking team renames the injected prop to onEvent, to match a new analytics vocabulary. The change is in one file:
// before/src/components/withTracking.tsx (edit applied)
Component: ComponentType<P & { onEvent?: TrackFn }>, // line 7 (was onTrack)
...
return <Component {...(props as P)} onEvent={onTrack} />; // line 13 (was onTrack={onTrack})The calls left wrong are PriceCard.tsx:12 and SignupButton.tsx:10, which still call onTrack?.(...). PriceCard.tsx:6 and SignupButton.tsx:5 still declare onTrack?: TrackFn. Both sides are optional props and line 13 spreads a value cast with as P, so the edit is not a type error. Checked with tsc 7.0.2 under --strict: the rename compiles silently, with no error at PriceCard.tsx:12, SignupButton.tsx:10 or the spread at withTracking.tsx:13. (The check used a minimal React type stub; the only errors left were the missing test-package modules, and restoring the old name gave the same two.) At runtime the click handler finds onTrack undefined, the optional call is skipped, and plan selections and sign-up clicks stop reaching /api/events.
The unit test, PriceCard.test.tsx:7, renders PriceCardBase with its own onTrack mock and passes. The page at Pricing.tsx:1 imports through the barrel and never mentions the prop, so a search for onTrack finds the two component files, the test and the HOC, and none of the three places that render the wrapped components (Pricing.tsx:6, :7, :8).
A variant with the same shape: rename the payload key label in the HOC's track() call. The payload type is Record<string, unknown> (track.ts:1), so the callers at PriceCard.tsx:12 and SignupButton.tsx:10 keep compiling while the event shape changes.
4. The fix
Components get tracking from a hook, useTracking("PriceCard"), which returns a typed track function. The barrel lists each export by name, the props are exported types, and the payload is a union keyed on action.
--- before/src/components/PriceCard.tsx+++ after/src/components/PriceCard.tsx-import { withTracking, type TrackFn } from "./withTracking";+import { useTracking } from "../tracking/useTracking"; -type Props = {+export type PriceCardProps = { plan: string; priceCents: number;- onTrack?: TrackFn; }; -export function PriceCardBase({ plan, priceCents, onTrack }: Props) {+export function PriceCard({ plan, priceCents }: PriceCardProps) {+ const { track } = useTracking("PriceCard"); return (- <button onClick={() => onTrack?.({ action: "select_plan", label: plan, value: priceCents })}>+ <button onClick={() => track({ action: "select_plan", label: plan, value: priceCents })}>...-export const PriceCard = withTracking(PriceCardBase);--- before/src/components/index.ts+++ after/src/components/index.ts-export * from "./PriceCard";-export * from "./SignupButton";+export { PriceCard, type PriceCardProps } from "./PriceCard";+export { SignupButton, type SignupButtonProps } from "./SignupButton";--- before/src/analytics/track.ts+++ after/src/tracking/events.ts-export type TrackPayload = Record<string, unknown>;+export type TrackPayload =+ | { action: "select_plan"; label: string; value: number }+ | { action: "signup_click"; label: string };5. Why the fix works
- The dependency is in the component's own file.
useTracking("PriceCard")is an import and a call insidePriceCard.tsx. A reader and a coding agent see where tracking comes from without opening another file. - A hook shares logic without a wrapper. The React docs describe custom hooks as the way to extract and share logic between components, with names that start with
use, and note that each call gets its own state. The component keeps its own name and its own props. - The page and the component agree on one name. There is no
PriceCardBase. The barrel exportsPriceCard, and a language-server rename follows the explicitexport { PriceCard }line. - The payload is checked.
track({ action: "select_plan", label: plan })withoutvalueis a type error against the union inevents.ts, and renaminglabelthere fails at both component call sites at once. - Tests inject the sink.
TrackingProvidertakes the function that receives events, soPriceCard.test.tsxrenders the real component and checks the real payload with no string-keyed mocking.
6. Rules that apply
- AR-02: re-export through an explicit list, not
export *chains. - AR-36: in React, prefer hooks and explicit props over higher-order components that inject props.
7. What LLMGRAPH shows
Before the edit, LLMGRAPH flags both call sites that use the injected prop as open, and says the prop is passed as a value in 3 places. In after/ the hook version has fewer blind spots, 5 down to 1.
who_calls onTrack (before)
llmgraph · who_calls withTracking.Tracked.onTrack (src/components/withTracking.tsx:11): 2 open
⚠ maybe_more src/components/PriceCard.tsx:12 PriceCardBase (open: type unknown) w1
⚠ maybe_more src/components/SignupButton.tsx:10 SignupButtonBase (open: type unknown) w2
⚠ passed as a value in 3 places: calls through it are not visible w3
⚠ plus 1 dynamic call nearby that could reach anything w4
what_is_unknown (before)
llmgraph · what_is_unknown (whole repo): 5 blind spots
⚠ dynamic call 3: src/analytics/track.ts (2), src/components/withTracking.tsx (1) w5
⚠ type unknown 2: src/components/PriceCard.tsx (1), src/components/SignupButton.tsx (1) w6
who_calls onTrack (after)
llmgraph · no function, method, class or component named "onTrack" (variables and enum members are not tracked)
what_is_unknown (after)
llmgraph · what_is_unknown (whole repo): 1 blind spot
⚠ dynamic call 1: src/tracking/TrackingContext.tsx (1) w1Treat the two open callers as the files to read when you touch the injection in withTracking.tsx, and expect the remaining blind spot in TrackingContext.tsx to be the one place left to check by hand.
8. References
React (fetched 2026-10-30):
- Reusing logic with custom hooks: https://react.dev/learn/reusing-logic-with-custom-hooks
- Component reference (function components and hooks are the recommended form; the page does not cover higher-order components): https://react.dev/reference/react/Component
ECMAScript, star exports and ResolveExport (cited in AR-02): https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html
Changelog
- 2026-10-30: LLMGRAPH capture added (Lead-checked)
- 2026-10-30: tool checks resolved (tsc 7.0.2, pyright 1.1.414)
Last verified