For AI agentsA Markdown index of these docs is at /llms.txt. Every page is also Markdown: add .md to its address.

Agent Rules / Chimeras

Why does a React prop stop arriving after a rename?

Chimera CH-04 · React (higher-order component, barrel)

Rules it tests: AR-02, AR-36

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.

tsx// 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} />; }
tsx// 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
ts// before/src/components/index.ts export * from "./PriceCard"; export * from "./SignupButton";
tsx// before/src/pages/Pricing.tsx import { PriceCard, SignupButton } from "../components"; // line 1

2. Where the call path hides

  • The prop has no source in its own file. PriceCard.tsx declares onTrack? at line 6 and calls it at line 12, and nothing there passes it. The value arrives from withTracking.tsx at render time.
  • The prop is optional on purpose. The test renders PriceCardBase directly with its own onTrack, so the local type says onTrack?. 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), so PriceCardBase and PriceCard are both public. The page imports PriceCard, a name that appears in no export line of the barrel.
  • The wrapper and the base have different names. A search for who renders PriceCardBase finds the test. The component production renders is PriceCard.

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:

tsx// 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.

diff--- 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);
diff--- 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";
diff--- 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 inside PriceCard.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 exports PriceCard, and a language-server rename follows the explicit export { PriceCard } line.
  • The payload is checked. track({ action: "select_plan", label: plan }) without value is a type error against the union in events.ts, and renaming label there fails at both component call sites at once.
  • Tests inject the sink. TrackingProvider takes the function that receives events, so PriceCard.test.tsx renders 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) w1

Treat 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

For agentsthis page as Markdown/agent-readable/ch-04.mdllms.txt