DEV Community

Solomon Wealth Code
Solomon Wealth Code

Posted on

Building 11 Free Finance Calculators in React: What I Learned About State, Forms, SEO, and Shipping Without a Backend

I shipped a site called Solomon Wealth Code with 11 free finance calculators (tithe, debt snowball, compound interest, mortgage payoff, net worth, emergency fund, generosity, retirement longevity, budget). Stack: React 18 + Vite + Tailwind + TypeScript, no backend, no database, no auth.

The calculators look simple. The lessons were not. Here is what I would tell my past self.

1. Controlled numeric inputs lie to you

Naive version:

const [income, setIncome] = useState(0);
<input type="number" value={income} onChange={(e) => setIncome(+e.target.value)} />
Enter fullscreen mode Exit fullscreen mode

This breaks the moment a user types 1,200 or $1200 or 1200.50 with a comma decimal (Europe). And type="number" blocks the comma on some browsers but allows it on others. Worse, on iOS Safari type="number" shows the wrong keyboard for amounts that need decimals.

What works: store the input as a string, parse on calculation.

const [incomeStr, setIncomeStr] = useState("");
const income = parseFloat(incomeStr.replace(/[^0-9.]/g, "")) || 0;
Enter fullscreen mode Exit fullscreen mode

For mobile, use inputMode="decimal" instead of type="number".

2. Debt snowball is not a one-liner

The snowball method looks trivial. In code, it is a month-by-month simulation. Interest first, then minimums, then snowball extra rolls onto whichever debt is smallest after interest.

function simulateSnowball(debts: Debt[], extraMonthly: number) {
  const months: MonthSnapshot[] = [];
  let remaining = debts.map(d => ({ ...d }));
  let month = 0;

  while (remaining.some(d => d.balance > 0) && month < 600) {
    month++;
    remaining.forEach(d => {
      d.balance += d.balance * (d.apr / 100 / 12);
    });
    remaining.sort((a, b) => a.balance - b.balance);
    remaining.forEach(d => {
      const pay = Math.min(d.balance, d.minimum);
      d.balance -= pay;
    });
    const target = remaining.find(d => d.balance > 0);
    if (target) {
      target.balance -= Math.min(target.balance, extraMonthly);
    }
    months.push({ month, snapshot: remaining.map(d => ({ ...d })) });
  }
  return months;
}
Enter fullscreen mode Exit fullscreen mode

Cap the loop at 600 months. There is always a user who enters $50,000 of debt and $5/month extra.

3. SEO on a SPA is a real problem

Vite + React Router = client-side rendering. Google can crawl it, but social previews (LinkedIn, Slack, Facebook, WhatsApp) cannot. They do not run JavaScript when fetching the preview card.

Two things helped:

  • Pre-rendering with Puppeteer (crawl every route, write HTML snapshots into dist/)
  • react-helmet-async for per-route titles, meta descriptions, canonical URLs, and JSON-LD

The JSON-LD matters more than people think. Adding Calculator, FAQPage, Article, and BreadcrumbList schema to each page got 4 calculators into Google's "People also ask" boxes within 6 weeks.

4. Calculators are content, not just tools

The instinct is to ship a clean input + output and stop. Those rank on page 5. What ranks: long-form context. 1,200 words below the fold explaining the framework, FAQ schema, footnotes. Google treats it as authoritative content with a calculator embedded, not as a calculator with thin SEO.

5. No backend = fewer bugs, faster shipping

I considered Supabase for saving results. Skipped it. Every calculation lives in useState. The whole site is a static bundle on a CDN. 0ms cold starts, $0/month hosting, no security surface.

6. Treat URLs like API endpoints

Without a database, state travels through the URL. Calculator inputs serialize to query params. Users can bookmark "my budget" or come back next month with the same numbers prefilled.

const [params, setParams] = useSearchParams();
const income = parseFloat(params.get("income") || "0");
const update = (k: string, v: string) => {
  params.set(k, v);
  setParams(params, { replace: true });
};
Enter fullscreen mode Exit fullscreen mode

Use replace: true so the browser history does not fill up.

7. Accessibility was easier than expected

<label> + <input> + <output> plus aria-live="polite" on the result region and aria-describedby linking each input to helper text. Lighthouse 88 → 100 in an hour.

8. Things I would skip if I started over

  • A monorepo. Single Vite app was fine for 11 calculators.
  • A component library. Tailwind + small custom components was faster.
  • Next.js. Nice tech, overkill for static calculators.

Live site

Full site: https://www.solomonwealthcode.com
Most complex calculator: https://www.solomonwealthcode.com/debt-snowball-calculator
Most popular: https://www.solomonwealthcode.com/compound-interest-calculator

Happy to answer questions about the stack in the comments.

Top comments (0)