# Why does a React prop stop arriving after a rename?

> Chimera CH-04 (React (higher-order component, barrel)): 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.

```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.

```text
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)

## Rules it tests

- [AR-02: Do barrel files hide calls from coding agents?](https://beta.llmgraph.in/agent-readable/ar-02.md)
- [AR-36: Do higher-order components hide props from coding agents?](https://beta.llmgraph.in/agent-readable/ar-36.md)

Last verified 2026-10-30
