Repository navigation
TypeScript/Redux: action handlers (.match / addCase) emit only imports — "What listens to X?" returns the dispatchers instead (PE-80047) #61
Description
Found while validating the fork on lawnstarter/sales-platform for PE-80047 (https://lawnstarter.atlassian.net/browse/PE-80047). Graded shape: Q3 — "What listens to event X?".
Summary
For Redux Toolkit events, the graph is inverted relative to the question. The code that dispatches an action gets a typed calls edge (it invokes the action creator). The code that handles it — action.match(...) in a middleware, builder.addCase(action, ...) in a slice — gets only a plain imports edge, indistinguishable from a test file or a type-only importer.
So the only relation-typed answer explain can give for "what listens to X" points at the emitters, and the real listeners are buried in undifferentiated imports.
Minimal reproduction
Four files, graphify update .:
// src/actions.ts
import { createAction } from '@reduxjs/toolkit';
export const thingHappened = createAction<{ id: string }>('tracking/thingHappened');// src/listenerMiddleware.ts — LISTENER
import { thingHappened } from './actions';
export const middleware = () => (next: (a: unknown) => unknown) => (action: unknown) => {
if (thingHappened.match(action)) {
console.log('handled');
}
return next(action);
};// src/listenerSlice.ts — LISTENER
import { createSlice } from '@reduxjs/toolkit';
import { thingHappened } from './actions';
export const slice = createSlice({
name: 'thing',
initialState: { flag: false },
reducers: {},
extraReducers: (builder) => {
builder.addCase(thingHappened, (state) => { state.flag = true; });
},
});// src/dispatcher.ts — DISPATCHER, not a listener
import { thingHappened } from './actions';
export function fire(dispatch: (a: unknown) => void) {
dispatch(thingHappened({ id: 'x' }));
}Inbound edges to thingHappened:
contains <- actions.ts | src/actions.ts
imports <- dispatcher.ts | src/dispatcher.ts
calls <- fire() | src/dispatcher.ts <-- the EMITTER gets the typed edge
imports <- listenerMiddleware.ts | src/listenerMiddleware.ts <-- LISTENER, untyped
imports <- listenerSlice.ts | src/listenerSlice.ts <-- LISTENER, untyped
Real-world impact measured on sales-platform
propertyIncompleteAtClose (apps/sales-tool/src/store/modules/tracking/actions.ts:38) has exactly two listeners by git grep + per-hit adjudication:
apps/sales-tool/src/store/middleware/analyticsMiddleware.ts:828—if (propertyIncompleteAtClose.match(action)) { ... }apps/sales-tool/src/store/modules/property-details/slice.ts:38—.addCase(propertyIncompleteAtClose, (state) => { ... })
graphify explain "propertyIncompleteAtClose" returns 13 connections. Both listeners are in there — as imports, alongside three test files that are also imports. The only three calls edges are the three dispatchers:
<-- middleware/analyticsMiddleware.ts [imports] ...analyticsMiddleware.ts:L37 <-- listener
<-- property-details/slice.ts [imports] ...property-details/slice.ts:L4 <-- listener
<-- PaymentInformationSection() [calls] ...PaymentInformationSection/index.tsx:L41
<-- ActionButton() [calls] ...ConfirmationSection/components/ActionButton.tsx:L28
<-- PostPaymentContinue() [calls] ...PaymentInformationSection/components/PostPaymentContinue.tsx:L30
An agent that trusts the typed edges answers "the three CTA components listen to it" — which is exactly backwards.
Environment
- fork
v8@5e9d95c11837aeabbbb5d90cb45544f49dc58ca4,graphify 0.9.40 - cold cache, fresh clone of
lawnstarter/sales-platform@701c8eb; 1266 files, 6722 nodes, 15324 edges
Suggested direction
Recognize the two RTK handler-registration shapes and emit a distinct relation (e.g. handles / listens_to) from the enclosing middleware or slice node to the action node:
<actionCreator>.match(x)used as a conditionbuilder.addCase(<actionCreator>, ...)/addMatcher(<actionCreator>.match, ...)
Emitting dispatch(<actionCreator>(...)) as dispatches rather than plain calls would make the pair queryable in both directions.
Activity
filipechagas commentedon Aug 11, 2026
Triage: lead issue for the Redux Toolkit awareness work unit (with #65). One PR against the JS/TS extractor: endpoint node minting inside
injectEndpoints/createApicallbacks (#65), plus one recognizer covering all three handler-registration shapes —<action>.match(x),addCase(<action>, …),addMatcher(<api>.endpoints.<name>.matchFulfilled, …)— emitting a listener-direction edge. #65's homonym-warning sub-ask needs no separate code: minting the endpoint node activates the existing ambiguity tiers.Sequencing notes:
.match(...)half of this issue.#65 stays open; both are closed by the one PR.