DEV Community

Muhammad Shahid
Muhammad Shahid

Posted on

How to Build a Dynamic CPC & Max Bid Calculator in Next.js / React

When building marketing tools or internal ad-ops dashboards, calculating Pay-Per-Click (PPC) metrics in real time is a common requirement. A robust CPC (Cost Per Click) Calculator needs to do two things:

  1. Solve for missing metrics dynamically (Ad Spend, Total Clicks, or CPC).
  2. Help developers and ad ops team members determine their Max CPC Bid based on Target CPA and conversion rates.

In this tutorial, we will build a production-ready CPC & Max Bid calculator using React, TypeScript, and clean mathematical logic.


1. Understanding the CPC Core Formulas

Before diving into the React implementation, let's look at the underlying math:

  • Basic Cost Per Click (CPC):
    CPC = Total Ad Spend \ Total Clicks

  • Max CPC Bid Formula:
    Max CPC = Target CPA * Conversion Rate %


2. Setting Up the State Logic in React

We want our calculator to update dynamically when any two input parameters are present. Here is a TypeScript hook that encapsulates the calculation logic safely without state mutation bugs:

import { useState, useMemo } from 'react';

interface CPCInput {
  adSpend: number | '';
  clicks: number | '';
  cpc: number | '';
  targetCPA: number | '';
  conversionRate: number | ''; // input as percentage (e.g., 2.5)
}

export const useCPCCalculator = () => {
  const [inputs, setInputs] = useState<CPCInput>({
    adSpend: '',
    clicks: '',
    cpc: '',
    targetCPA: '',
    conversionRate: '',
  });

  const handleInputChange = (key: keyof CPCInput, value: string) => {
    const numericValue = value === '' ? '' : parseFloat(value);
    setInputs((prev) => ({ ...prev, [key]: numericValue }));
  };

  const calculatedResults = useMemo(() => {
    const spend = typeof inputs.adSpend === 'number' ? inputs.adSpend : 0;
    const clicks = typeof inputs.clicks === 'number' ? inputs.clicks : 0;
    const cpc = typeof inputs.cpc === 'number' ? inputs.cpc : 0;

    let derivedCPC = cpc;
    let derivedSpend = spend;
    let derivedClicks = clicks;

    // Dynamically calculate missing parameter
    if (spend > 0 && clicks > 0) {
      derivedCPC = parseFloat((spend / clicks).toFixed(2));
    } else if (spend > 0 && cpc > 0) {
      derivedClicks = Math.round(spend / cpc);
    } else if (clicks > 0 && cpc > 0) {
      derivedSpend = parseFloat((clicks * cpc).toFixed(2));
    }

    // Max CPC calculation based on Target CPA
    let maxBid = 0;
    if (typeof inputs.targetCPA === 'number' && typeof inputs.conversionRate === 'number') {
      maxBid = parseFloat((inputs.targetCPA * (inputs.conversionRate / 100)).toFixed(2));
    }

    return {
      cpc: derivedCPC,
      spend: derivedSpend,
      clicks: derivedClicks,
      maxBid,
    };
  }, [inputs]);

  return { inputs, handleInputChange, calculatedResults };
};
Enter fullscreen mode Exit fullscreen mode

3. Building the Component Interface

Now, let's connect this custom hook into a React form component:

import React from 'react';
import { useCPCCalculator } from './useCPCCalculator';

export const CPCCalculatorWidget: React.FC = () => {
  const { inputs, handleInputChange, calculatedResults } = useCPCCalculator();

  return (
    <div className="p-6 max-w-lg mx-auto bg-white rounded-xl shadow-md space-y-4">
      <h2 className="text-xl font-bold">Ad Budget & CPC Solver</h2>

      <div className="space-y-3">
        <div>
          <label className="block text-sm font-medium">Total Ad Spend ($)</label>
          <input
            type="number"
            value={inputs.adSpend}
            onChange={(e) => handleInputChange('adSpend', e.target.value)}
            className="w-full p-2 border rounded"
            placeholder="e.g. 1000"
          />
        </div>

        <div>
          <label className="block text-sm font-medium">Total Clicks</label>
          <input
            type="number"
            value={inputs.clicks}
            onChange={(e) => handleInputChange('clicks', e.target.value)}
            className="w-full p-2 border rounded"
            placeholder="e.g. 2500"
          />
        </div>

        <div>
          <label className="block text-sm font-medium">Target CPA ($)</label>
          <input
            type="number"
            value={inputs.targetCPA}
            onChange={(e) => handleInputChange('targetCPA', e.target.value)}
            className="w-full p-2 border rounded"
            placeholder="e.g. 50"
          />
        </div>

        <div>
          <label className="block text-sm font-medium">Conversion Rate (%)</label>
          <input
            type="number"
            value={inputs.conversionRate}
            onChange={(e) => handleInputChange('conversionRate', e.target.value)}
            className="w-full p-2 border rounded"
            placeholder="e.g. 3.5"
          />
        </div>
      </div>

      <div className="mt-4 p-4 bg-gray-50 rounded-lg">
        <h3 className="font-semibold text-lg">Results</h3>
        <p>Calculated CPC: <strong>${calculatedResults.cpc}</strong></p>
        <p>Optimal Max CPC Bid: <strong>${calculatedResults.maxBid}</strong></p>
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Conclusion & Live Reference

Building client-side marketing utility calculators in Next.js is a quick way to deliver interactive user experiences without hitting backend APIs.

If you want to test a full-featured live tool including industry benchmark breakdowns, check out the live CPC Calculator on GM Calculator.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.