expo-router carries expo/expo's highest activity risk — 3 functions to address first

expo/expo's top structural hotspots are concentrated in expo-router's navigation layer, where functions combining CC 59 and fan-out 66 are actively changing — a live regression risk at commit 4b854eb.

Stephen Collins ·
oss typescript refactoring code-health

Antipatterns Detected

complex_branching5deeply_nested5exit_heavy5long_function5god_function4hub_function1

Key Points

What is a god function and why does it matter in expo?

A god function is one that has taken on so many responsibilities that it becomes the single point of coupling for a large part of the system. In expo/expo, four of the top five hotspots are flagged as god functions — most notably `useNavigationBuilder`, which calls 66 distinct functions and manages multiple concerns simultaneously. When a function knows too much, a change to any one of its responsibilities risks breaking the others, and testing it in isolation becomes extremely difficult.

How do I reduce cyclomatic complexity in TypeScript?

The most direct technique is extract-method refactoring: identify groups of related conditional branches and move them into named, single-purpose helper functions, each of which can be tested independently. Replacing complex switch or if-else chains with lookup tables or strategy objects also removes execution paths without changing behavior.

Is expo actively maintained?

Yes — three of the top five hotspots, including `useNavigationBuilder`, `BaseNavigationContainer`, and `getDirectoryTree`, are in the 'fire' quadrant, meaning they are both structurally complex and actively receiving commits right now. Their activity-weighted risk scores range from 19.0 to 19.8, confirming that expo-router in particular is under active development.

How do I reproduce this analysis?

Run the Hotspots CLI against the expo/expo repository at commit `4b854eb` to reproduce these exact scores.

What does activity-weighted risk mean?

Complexity × recent commit frequency — functions that are hard to understand AND actively changing are the highest priority for refactoring.

Across expo/expo’s 15,835 analyzed functions, 835 are rated critical — and the top of that list is dominated by expo-router’s navigation layer. The highest-ranked hotspot, useNavigationBuilder, sits in the ‘fire’ quadrant with an activity-weighted risk of 19.8 and cyclomatic complexity of 59, meaning it is both structurally hard to reason about and actively being changed right now, a combination that creates live regression risk on every commit. The second-ranked function, BaseNavigationContainer, compounds that risk with a max nesting depth of 10 — the deepest in the top five — while also carrying an activity-weighted risk of 19.4.

The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).

Top 5 Hotspots

FunctionFileRiskCCNDFO
useNavigationBuilderpackages/expo-router/src/react-navigation/core/useNavigationBuilder.tsx19.859566
BaseNavigationContainerpackages/expo-router/src/react-navigation/core/BaseNavigationContainer.tsx19.4241037
interactiveDashboardtools/src/commands/CIInspectCommand.ts19.1221225
getDirectoryTreepackages/expo-router/src/getRoutesCore.ts19.058630
parsePlistXMLpackages/@expo/plist/src/parse.ts18.938128

Large Repo Analysis

expo is a large repository. To stay within memory constraints, this analysis used hybrid touch mode: structural complexity — CC, ND, FO — is measured precisely for every function. Git activity is tracked at the function level (via git log -L) only for files with 5 or more commits in the last 30 days; other files use a file-level approximation. Rankings therefore surface functions that are both structurally complex and in the most actively-changing parts of the codebase. Dormant code with high structural complexity will rank lower than it would under a full per-function analysis — to surface it, run hotspots analyze . --per-function-touches on a machine with sufficient memory.

Codemod / Tooling Files in Results

The function interactiveDashboard in tools/src/commands/CIInspectCommand.ts and parsePlistXML in packages/@expo/plist/src/parse.ts appear in the top five but fall outside expo-router’s application runtime. interactiveDashboard is an internal tooling command — likely a CLI dashboard for CI inspection — rather than product code, so its structural complexity has limited impact on end users. parsePlistXML is a plist file parser in the @expo/plist utility package; its CC of 38 and ND of 12 reflect the inherent branching required to handle the plist XML format’s many node types, but it is a lower-level utility with narrow scope. To exclude internal tooling from future analyses, add { "exclude": ["tools/"] } to your .hotspotsrc.json; to exclude vendored or bundled utility packages, add "packages/@expo/plist/" to the same list.

Hotspot Analysis

useNavigationBuilder — packages/expo-router/src/react-navigation/core/useNavigationBuilder.tsx

Based on its name and location in expo-router’s react-navigation core, useNavigationBuilder is almost certainly the React hook responsible for constructing and wiring up a navigator — connecting route state, child descriptors, navigation actions, and context for a given navigator type. Its cyclomatic complexity of 59 means there are at minimum 59 independent execution paths through this hook, each one a required test case and a potential bug surface. The fan-out of 66 — the highest in the entire top five — classifies it as a hub function: it calls 66 distinct functions, meaning a subtle change here can produce ripple effects across virtually every part of the navigation stack. With an activity-weighted risk of 19.8 in the ‘fire’ quadrant, this is not a dormant debt item; it is actively changing right now, which makes each of those 59 paths a live regression risk.

Recommendation: Before the next feature change, add characterization tests that cover the most common navigator configurations to lock in current behavior. Then begin extract-method refactoring to decompose the hook’s responsibilities — state wiring, action dispatch, and child descriptor construction are likely separable concerns that could each become independently testable units.

BaseNavigationContainer — packages/expo-router/src/react-navigation/core/BaseNavigationContainer.tsx

As the foundational container component for all navigation in expo-router, BaseNavigationContainer is likely responsible for providing navigation context, managing top-level navigation state, and coordinating ref forwarding and lifecycle events across the navigator tree. Its max nesting depth of 10 is a strong refactoring signal — ND 8+ is where control flow becomes genuinely difficult to trace, and at ND 10 this function exceeds that threshold significantly. Despite a cyclomatic complexity of 24 being lower than useNavigationBuilder, the combination of ND 10, fan-out of 37, and an activity-weighted risk of 19.4 in the ‘fire’ quadrant makes it the second most urgent hotspot in the repo — it is both structurally opaque and actively changing.

Recommendation: Target the deepest nesting branches first: identify the conditional blocks responsible for the ND 10 reading and extract them into named helper functions with single, clear responsibilities. This will reduce visual nesting immediately and make the control flow auditable during active development.

getDirectoryTree — packages/expo-router/src/getRoutesCore.ts

Located in expo-router’s route-building core, getDirectoryTree is likely responsible for traversing the file system structure that expo-router uses to derive its route configuration — converting directory and file names into a navigable route tree. A cyclomatic complexity of 58 places it just below useNavigationBuilder as one of the most branch-heavy functions in the codebase, and its fan-out of 30 indicates it delegates to a broad set of utilities during that traversal. With an activity-weighted risk of 19.0 in the ‘fire’ quadrant, it is being actively changed — likely as expo-router’s file-based routing features evolve — meaning each of its 58 execution paths is a potential source of regression on new commits.

Recommendation: Map the function’s major branching axes — likely file type classification, special route segment handling, and recursive descent — and extract each into a focused, independently testable helper. The exit-heavy pattern flagged here suggests many early-return guards; reviewing whether those guards can be unified or reordered will reduce the effective path count and lower the CC score measurably.

Patterns Found

Antipatterns detected across the top functions in this snapshot:

PatternOccurrences
complex_branching5
deeply_nested5
exit_heavy5
long_function5
god_function4
hub_function1

These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.

Key Takeaways

  • useNavigationBuilder has a fan-out of 66 — the highest in the top five — meaning it is a hub that couples to a large fraction of the navigation stack; any change there should be preceded by a targeted test suite covering all major navigator types.
  • BaseNavigationContainer’s max nesting depth of 10 exceeds the strong-refactoring-signal threshold of ND 8 and is actively changing (activity-weighted risk 19.4); flatten the deepest conditional blocks before adding new navigation lifecycle features.
  • getDirectoryTree in getRoutesCore.ts has CC 58 and is in the ‘fire’ quadrant — as expo-router’s file-based routing continues to expand, this function will accumulate more paths; decomposing it now reduces the risk surface before the next routing feature lands.

Reproduce This Analysis

git clone https://github.com/expo/expo
cd expo
git checkout 4b854eb668377ff198a580b497b31dbb806111f6
hotspots analyze . --mode snapshot --explain-patterns --force --hybrid-touches 5

To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.

Hotspots highlights structural and activity risk — not “bad code.” Findings are a prioritization aid, not a bug predictor. Editorial policy →

Run this on your own codebase

Hotspots runs locally in under a minute — no account, no data leaves your machine.

macOS
$ brew install Stephen-Collins-tech/tap/hotspots
Linux / cargo
$ cargo install hotspots-cli
Run in any repo
$ hotspots analyze .
★ Star on GitHub

Related Analyses