← Blog
·Updated ·8 min read

Building a Trade Calculator App in React Native

How we built FieldDojo — 16+ code-cited calculators for electrical, HVAC, plumbing, and construction — with React Native, Expo, and an offline-first SQLite stack.


Key takeaways: Keep calculation logic in pure functions, completely separate from UI — it makes formulas testable, code references trivial, and redesigns cheap. One shared numeric-input hook across 16+ calculators beats 16 ad-hoc input handlers. Physical-device QA is non-negotiable for keyboard-heavy apps.

The Problem

Most trade calculator apps own one trade. Wire sizing. Or duct sizing. Or pipe sizing. Never all four. A journeyman electrician who also runs HVAC on small jobs needs three separate apps, and still pulls out a code book to verify results.

We wanted: all four trades, all code-referenced (NEC, ASHRAE, UPC, IRC), free. The full product story is in the FieldDojo case study; this post is the engineering walkthrough.

Tech Stack

  • React Native 0.83 with Expo SDK 55
  • Expo Router with NativeTabs for file-based, per-trade navigation
  • TypeScript throughout
  • NativeWind v5 + Tailwind v4 for styling
  • op-sqlite + Drizzle ORM for local calculation history
  • MMKV for user preferences (unit system, default trade)
  • Hono + Drizzle + Postgres backend with Better-Auth sessions
  • Orval generates the typed React Query client from the backend's OpenAPI spec
  • EAS Build for iOS and Android production builds

Architecture: Separating Calculation Logic from UI

The most important decision we made early: keep calculation logic completely separate from React components.

Each calculator is a pure function. The wire sizing calculator takes current, length, voltage, material, and conduit type as inputs, looks up ampacity from the NEC 310.15 table, calculates voltage drop, and returns the recommended gauge along with the NEC reference. Simplified, the shape looks like this:

function calculateWireSize(input: WireSizeInput): WireSizeResult {
  const ampacity = lookupAmpacity(input.current, input.material, input.conduitType); // NEC 310.15
  const voltageDrop = (2 * K[input.material] * input.lengthFt * input.current) / circularMils(ampacity.gauge);

  return {
    gauge: ampacity.gauge,
    voltageDropPercent: (voltageDrop / input.voltage) * 100,
    formula: "VD = (2 x K x L x I) / CM",
    codeReference: {
      standard: "NEC",
      section: "310.15",
      description: "Ampacities of insulated conductors",
      year: 2023,
    },
  };
}

No React imports, no state, no side effects. This pattern meant:

  1. We could test calculation logic independently of the UI
  2. Showing the formula and code reference was trivial — it came from the function, not the component
  3. Swapping UI without touching logic was easy

The useNumericField Hook

Numeric input on mobile is notoriously painful. We wrote a useNumericField hook that handles:

  • Decimal point edge cases (typing "0." shouldn't immediately evaluate)
  • Preventing leading zeros
  • Allowing empty state during input without triggering calculation
  • Immediate recalculation on every valid change

This single hook is used across all 16+ calculators. Consistent behavior everywhere. When QA found a keyboard edge case, the fix landed once and every calculator inherited it — that's the payoff of resisting per-screen input logic.

Navigation: Expo Router + NativeTabs

We used Expo Router's file-based routing with a tab layout. Each trade (electrical, HVAC, plumbing, construction) gets its own tab with nested routes for individual calculators.

NativeTabs gave us native tab bar behavior on both iOS and Android — proper swipe gestures, correct safe area handling, platform-appropriate animations.

Code References: The Differentiator

Every calculation in FieldDojo shows the code standard it's based on. This was non-trivial to implement well.

Our approach: each calculation function returns a codeReference object containing the standard (NEC, ASHRAE, UPC, or IRC), section number, description, and year. The UI renders this consistently across all calculators.

The result is a calculation that says "per NEC 310.15" — something a tradesperson can verify and cite to an inspector. It's also the reason the pure-function architecture matters: the citation lives next to the math it justifies, not in a UI string someone forgets to update.

History and State Persistence

Calculation history is stored locally in SQLite via op-sqlite, with Drizzle ORM giving us a typed schema and migrations. We store the full calculation parameters and result, so history items can be restored and re-calculated with different inputs. A one-time startup migration moves legacy MMKV history into SQLite on existing installs. Pro tier adds cloud sync via the FieldDojo API.

Everything works fully offline — a job site with no signal is the primary environment, not an edge case. That constraint drove the offline-first architecture we use for most mobile builds.

EAS Build + App Store Submission

We used EAS Build for both development and production builds. Physical device QA before submission was crucial — the numeric input behavior and keyboard handling are impossible to validate properly in the simulator.

What We'd Do Differently

More aggressive calculation abstraction early. We had some duplication across trade calculators before we extracted the common patterns. The refactor took longer than it should have.

Better unit test coverage on calculation functions. The pure function approach made this easy — we just didn't write enough tests early. Some edge cases in the conduit fill calculation bit us in QA.

Design system earlier. We built the CalculatorSection component partway through and had to retrofit earlier screens. Starting with the component library would have saved time.

The App

FieldDojo is live on iOS and Android. All 16+ calculators are free. Read the full FieldDojo case study for the product side — problem, approach, architecture diagrams, and outcomes.

Built by NOTchip](/) — a mobile and web app development agency. If you're building something similar or need a React Native team, [get in touch.