React Flow Tracer
What actually happens when a user taps this button?
React Flow Tracer answers that question by statically analyzing your React and
React Native code. Put your cursor on a JSX event handler, run one command, and
get an interactive tree of everything that code path can reach — function calls
across files, network requests, navigation, state updates and other side
effects — with every node clickable straight to its source.
Static analysis only. This extension reads your code; it never runs it.
Results describe what the code can do, not what it will do at runtime.
See Limitations.
The problem
You open an unfamiliar screen and find this:
<Button title="Checkout" onPress={checkout} />
Answering "what does pressing this do?" means a manual crawl: jump to
checkout, discover it comes from useCheckout(), open that hook, follow
createOrder, open the service, find the HTTP call. Four files later you have
the answer — and you repeat the whole thing for the next button.
Go-to-definition answers one hop. Call hierarchy shows callers, not
behaviour. Neither tells you "this button posts to /orders, dispatches a
Redux action and navigates to Success."
The solution
One command produces the whole picture:
USER ACTION: Checkout (Button)
onPress
└── checkout()
└── createOrder()
└── POST /orders
The sidebar adds a summary and analysis warnings:
Summary
API Calls 1
State Changes 1
Navigation 1
Side Effects 2
Warnings
Static analysis detected a potential network request: POST /orders.
Every row opens the exact file, line and column it came from.
Reverse tracing — "what can trigger this?"
Forward tracing answers "what does this button do?". Reverse tracing answers
the opposite question, which is usually the harder one when you are about to
change shared code:
Which user actions can reach this function or side effect?
Put the cursor on a function, a call, or a detected side effect and run
React Flow Tracer: Find User Actions:
Target
POST /orders
User Actions
CheckoutScreen · Checkout Button
onPress
checkout()
createOrder()
POST /orders
QuickOrderScreen · Place Order
onClick
placeOrder()
createOrder()
POST /orders
Summary
User Actions 2
Paths 2
Internal Nodes 3
Useful before touching a shared service: you can see every entry point that
reaches it before you change its behaviour.
What you can start from
A function declaration, a call to one of your functions, or any detected
side-effect node — API (fetch, axios, service clients), navigation, Redux
dispatch, TanStack Query, storage, and analytics.
How it works
Reverse tracing is not a text or name search, and it is not a second
graph. It:
- resolves the cursor to a declaration or call site using the TypeScript type
checker;
- walks the import graph upwards from that file to find which modules can
reach it (semantic module resolution, not a workspace scan);
- runs the existing forward trace engine from each user action found in
those modules;
- keeps the traces whose nodes contain the target, and reports the path from
the forward trace's own edges.
Because every reported path is produced by the forward engine, forward and
reverse results cannot disagree.
Reverse tracing limitations
- It finds statically resolvable user-action paths. It does not claim to
find all callers: a caller reached only through dynamic dispatch, a
runtime-generated handler, or an unresolvable alias is not reported.
- An action whose handler cannot be resolved is skipped rather than guessed.
- The same depth, node, timeout and cancellation limits as forward tracing
apply, and a truncated search says so explicitly.
- The first reverse trace in a project loads that project's files (roughly
half a second on a small project); subsequent reverse traces in the same
project reuse it and are typically an order of magnitude faster.
When nothing is found, the result distinguishes the two cases rather than
showing a bare "no results":
- "No user action was found that can be statically traced to this target."
- "No React/React Native user action root was found in the reachable callers."
— callers exist, but none of them is a UI event handler.
Installation
From the Marketplace — search for React Flow Tracer in the Extensions
view, or:
code --install-extension zaidAlfaqeeh.react-flow-tracer
From a VSIX
npx @vscode/vsce package
code --install-extension react-flow-tracer-0.3.3.vsix
From source
git clone https://github.com/zaid-alfaqeeh/react-action-trace.git
cd react-action-trace
npm install
npm run compile
Then press F5 in VS Code to launch an Extension Development Host.
Usage
- Open a
.tsx, .jsx, .ts or .js file.
- Put the cursor on a JSX event handler — the prop name, the handler, or the
element itself all work.
- Trigger a trace in any of these ways:
- Command Palette →
React Flow Tracer: Trace Current Action
- Right-click in the editor →
Trace React Action
- Click the Trace Action CodeLens above the handler
- Read the result in the React Flow Tracer sidebar.
- Run
React Flow Tracer: Open Graph for the visual view.
To go the other way, put the cursor on a function or side effect and run
React Flow Tracer: Find User Actions (also on the editor context menu).
Both results appear in the same sidebar and both can be opened in the graph.
Commands
| Command |
What it does |
React Flow Tracer: Trace Current Action |
Forward: what this UI action does |
React Flow Tracer: Find User Actions |
Reverse: which UI actions reach this |
React Flow Tracer: Open Graph |
Graph view of the current result |
React Flow Tracer: Copy Trace as Text |
Copy the current result |
React Flow Tracer: Re-run Trace |
Re-run the last forward trace |
React Flow Tracer: Clear Trace |
Clear the sidebar |
React Flow Tracer: Show Diagnostics Output |
Open the output channel |
Screenshots
Screenshot and demo capture is pending for this release. No screenshot
images are included; none are referenced here to avoid broken links.
Supported frameworks
"Support" below means event detection and call tracing are verified by
tests. It does not mean every API of that framework is modelled — see
Limitations.
| Framework |
Support |
| React (web) |
Events on DOM elements and components; handler resolution |
| React Native |
Events on the core components listed below; handler resolution |
| TypeScript / TSX |
Resolution via the TypeScript type checker |
| JavaScript / JSX |
Works without a tsconfig.json; resolution is weaker |
| Redux / Redux Toolkit |
dispatch(...) and action creators detected |
| TanStack Query (v4 & v5) |
Cache calls and mutate() → mutationFn detected |
| React Navigation |
navigate / push / replace / goBack detected |
| Next.js / React Router |
router.push / replace detected |
| Monorepos (pnpm, npm, yarn, Turborepo, Nx) |
Nearest-tsconfig project discovery |
Not modelled as distinct behaviours (they appear as ordinary function
calls, not as typed side effects): React Native platform APIs such as
Linking, Vibration, Alert, Share, Clipboard, Permissions and native
modules; React Context providers; GraphQL clients; WebSockets.
No framework is a hard dependency. Everything is detected from your source.
Supported events
React web — onClick, onDoubleClick, onChange, onInput, onSubmit,
onReset, onFocus, onBlur, onKeyDown, onKeyUp, onMouseDown,
onMouseEnter, onMouseLeave, onDrop, onScroll, onSelect and more, on
button, input, form, select, textarea, a and other elements.
React Native — onPress, onPressIn, onPressOut, onLongPress,
onChangeText, onSubmitEditing, onEndEditing, onValueChange,
onRefresh, onEndReached, onScroll, onSelectionChange and more, on
Pressable, TouchableOpacity, TouchableHighlight,
TouchableWithoutFeedback, Button, TextInput, ScrollView, FlatList,
SectionList, Switch, Modal and others.
Any prop matching the onXxx convention is also traceable, so custom
components work without configuration.
Handler shapes
<Button onPress={handleCheckout} /> // direct reference
<Button onPress={() => handleCheckout()} /> // inline wrapper
<Button onPress={() => handleCheckout(order.id)} /> // arguments shown
<Button onPress={async () => { await save(); }} /> // inline body
const handle = useCallback(() => checkout(), []); // unwrapped
const { checkout } = useCheckout(); // resolved through the hook
Supported analyzers
| Analyzer |
Detects |
Example output |
| API |
fetch, axios.*, configurable service clients |
POST /orders |
| Navigation |
navigation.navigate/push/replace, router.push |
navigate -> Checkout |
| State |
useState / useReducer setters, verified against their declaration |
setEmail(value) |
| Redux |
dispatch(...) with action creators or objects |
dispatch(orderCreated()) |
| Query |
invalidateQueries, setQueryData, mutate |
queryClient.invalidateQueries() |
| Storage |
localStorage, AsyncStorage, SecureStore, MMKV |
AsyncStorage.setItem("user.name") |
| Analytics |
Configurable providers and functions |
analytics.track: order_created |
URLs, routes and event names are shown only when they are statically
resolvable. Dynamic values are labelled honestly rather than guessed:
POST <dynamic URL>
Configuration
| Setting |
Default |
Description |
reactFlowTracer.enableCodeLens |
true |
Show the "Trace Action" CodeLens |
reactFlowTracer.maxDepth |
20 |
Maximum traversal depth |
reactFlowTracer.maxNodes |
300 |
Maximum nodes before truncation |
reactFlowTracer.maxFileSize |
1048576 |
Skip files larger than this (bytes) |
reactFlowTracer.timeoutMs |
10000 |
Per-trace time budget |
reactFlowTracer.enableReactNative |
true |
Detect React Native events |
reactFlowTracer.enableReactWeb |
true |
Detect React DOM events |
reactFlowTracer.enableApiDetection |
true |
Detect network calls |
reactFlowTracer.enableNavigationDetection |
true |
Detect navigation |
reactFlowTracer.enableStateDetection |
true |
Detect state and storage |
reactFlowTracer.apiClientNames |
["api", "client", "http", ...] |
Identifiers treated as HTTP clients |
reactFlowTracer.analyticsObjectNames |
["analytics", "mixpanel", ...] |
Analytics provider objects |
reactFlowTracer.analyticsFunctionNames |
["track", "logEvent", ...] |
Analytics function names |
reactFlowTracer.excludeGlobs |
["**/node_modules/**", ...] |
Paths excluded from analysis |
Teams with a custom API wrapper only need to add its name to
apiClientNames for myHttp.post("/orders") to be recognised.
Architecture
src/
├── extension.ts Activation, command and provider registration
├── commands/ Command handlers (trace, graph, reveal)
├── analyzer/
│ ├── ActionDetector.ts Finds JSX event handlers at a position
│ ├── TraceEngine.ts Traversal, limits, cycle protection
│ ├── SymbolResolver.ts Type-checker-backed cross-file resolution
│ ├── ProjectManager.ts tsconfig discovery, project cache
│ ├── TraceService.ts Facade used by the VS Code layer
│ ├── eventCatalog.ts Event/component data tables
│ └── analyzers/ Pluggable behaviour analyzers
├── models/ TraceNode, TraceEdge, ActionTrace (UI-independent)
├── providers/ TreeView and CodeLens providers
├── webview/ Graph panel and its assets
├── utils/ Config, logging, AST helpers
└── tests/ Unit and VS Code integration tests
Design principles
Real analysis. The TypeScript compiler API (through ts-morph) does the
resolution. Regex is never the primary mechanism.
Model/UI separation. models/ has no VS Code or ts-morph imports, so
the engine is testable in plain Node and reusable in other front-ends.
Pluggable analyzers. Adding a behaviour means implementing one interface
and registering it — the engine does not change:
interface BehaviorAnalyzer {
readonly id: string;
canAnalyze(node: ts.Node, context: AnalysisContext): boolean;
analyze(node: ts.Node, context: AnalysisContext): AnalysisResult;
}
Lazy and cached. Projects are created per tsconfig and reused; files
load on demand through import resolution rather than scanning the repo; a
save invalidates only the file that changed.
Limitations
React Flow Tracer performs static analysis. It reports what the code
could do, and it deliberately declines to guess.
- No runtime guarantees. A branch that never executes still appears. The
trace shows reachable code, not an execution log.
- No conditional evaluation. Both sides of an
if are traced; the engine
does not evaluate conditions.
- Dynamic dispatch is not resolved.
handlers[key](), values from eval,
and runtime-constructed callables are reported as unresolved rather than
guessed.
- Dynamic URLs are not reconstructed.
fetch(`/users/${id}`) is shown
as GET <dynamic URL> (with the pattern in the tooltip).
- Library internals are not expanded. Calls into
node_modules are marked
as external and not traversed.
- Class components are partially supported. Methods resolve, but
this
binding through decorators or HOCs may not.
- Very large graphs are truncated at the configured limits, always with an
explicit truncation node and warning — never silently.
- Dynamic
import() is detected and reported, not followed.
- State detection depends on resolution. A
setXxx call is reported as
state only when it resolves to a useState/useReducer binding. In a plain
JavaScript project where the symbol cannot be resolved, the naming
convention is used as a fallback, so an unrelated setSomething() may be
reported as state there.
- Values returned from factory functions are not resolved. Given
const handler = getHandler(), the trace reports handler as unresolved
rather than guessing which function was returned.
- Analyzer names are matched by identifier, so a local function that
happens to be called
track is treated as an analytics event. Adjust
analyticsFunctionNames if this collides with your code.
When something cannot be resolved, the extension says so in the Warnings
section rather than inventing a plausible answer.
Roadmap
- [x] Reverse tracing: "which actions can reach this function?" (v0.2)
- [ ] Context provider and prop-drilling resolution
- [ ] Class component
this.handler binding
- [ ] GraphQL operation detection (Apollo, urql)
- [ ] WebSocket and EventSource detection
- [ ] Export a trace to Markdown or Mermaid
- [ ] Workspace-wide action inventory
- [ ] Optional AI layer for Explain Trace and Suggest Refactor
(strictly opt-in; the core stays offline and deterministic)
Contributing
npm install
npm run compile # build
npm run lint # eslint
npm test # unit tests (plain Node, no VS Code download)
npm run test:integration # VS Code integration tests
The unit suite covers the analyzer and runs anywhere. The integration suite
launches a real VS Code instance, so it needs network access on first run and
an environment that can start Electron — a desktop session, or xvfb-run on
Linux CI. In a headless or sandboxed shell it fails at launch with
bad option: --extensionDevelopmentPath; that is the environment, not the
tests. Pin a specific build with VSCODE_TEST_VERSION=1.96.0 if a release
regresses.
Press F5 to launch the Extension Development Host against
test-fixtures/cross-file.
Adding an analyzer
- Implement
BehaviorAnalyzer in src/analyzer/analyzers/.
- Register it in
createDefaultAnalyzers().
- Add a fixture under
test-fixtures/ and a test in src/tests/unit/.
Analyzer order matters: the first canAnalyze match wins.
Conventions for warnings — use "detected", "potential" and "could not
statically resolve". Never claim that code definitely runs. There is a test
that enforces this.
Privacy and security
- Works fully offline. No network requests.
- No telemetry in the current release.
- No source code leaves your machine.
- No project code is executed — analysis is purely syntactic and semantic.
- The graph webview runs under a strict Content Security Policy with a nonce,
and loads only local resources.
License
MIT