React Shipping Calculator

An interactive React component for calculating shipping rates with real-time updates. This example demonstrates how to integrate the Flash Americas API into a React application with a clean, responsive user interface.

Overview

This example includes:

  • Real-time rate calculation as users type
  • Address validation with error handling
  • Responsive design that works on all devices
  • Loading states and error handling
  • Carrier comparison with visual rate display
  • TypeScript support for type safety

Demo

Try the live demo: React Shipping Calculator Demo

Installation

Prerequisites

  • Node.js 16+ and npm/yarn
  • Flash Americas API key (get one here)
  • React 18+ application

Quick Start

Install dependenciesbash
npm install axios react-hook-form @types/react
# or
yarn add axios react-hook-form @types/react

Basic Implementation

1. API Client Setup

First, create an API client for making requests:

src/lib/flashAmericasApi.tstypescript
import axios, { AxiosInstance } from 'axios';

export interface Address {
address1: string;
address2?: string;
city: string;
state: string;
postalCode: string;
country: string;
}

export interface Cargo {
weight: number;
length: number;
width: number;
height: number;
quantity: number;
description: string;
}

export interface QuoteRequest {
originAddress: Address;
destinationAddress: Address;
cargo: Cargo[];
shipDate: string;
}

export interface Rate {
provider: string;
service: string;
totalCost: number;
currency: string;
transitDays: number;
deliveryDate: string;
isGuaranteed: boolean;
breakdown: {
  baseCost: number;
  fuelSurcharge: number;
  accessorials: Record<string, number>;
};
}

export interface QuoteResponse {
quoteId: string;
rates: Rate[];
expiresAt: string;
}

class FlashAmericasAPI {
private client: AxiosInstance;

constructor(apiKey: string) {
  this.client = axios.create({
    baseURL: 'https://ship.flashamericas.com/api/v1',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    timeout: 10000,
  });
}

async getQuotes(request: QuoteRequest): Promise<QuoteResponse> {
  const response = await this.client.post('/quotes', request);
  return response.data.data;
}

async validateAddress(address: Partial<Address>): Promise<Address> {
  const response = await this.client.post('/addresses/validate', address);
  return response.data.data;
}
}

export default FlashAmericasAPI;

2. Shipping Calculator Component

src/components/ShippingCalculator.tsxtsx
import React, { useState, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import FlashAmericasAPI, { QuoteRequest, Rate, Address, Cargo } from '../lib/flashAmericasApi';

interface FormData {
originAddress: Address;
destinationAddress: Address;
weight: number;
length: number;
width: number;
height: number;
description: string;
}

interface ShippingCalculatorProps {
apiKey: string;
onRateSelect?: (rate: Rate) => void;
}

const ShippingCalculator: React.FC<ShippingCalculatorProps> = ({ 
apiKey, 
onRateSelect 
}) => {
const [rates, setRates] = useState<Rate[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const api = new FlashAmericasAPI(apiKey);

const { register, handleSubmit, watch, formState: { errors } } = useForm<FormData>({
  defaultValues: {
    originAddress: {
      address1: '',
      city: '',
      state: '',
      postalCode: '',
      country: 'US'
    },
    destinationAddress: {
      address1: '',
      city: '',
      state: '',
      postalCode: '',
      country: 'US'
    },
    weight: 1,
    length: 12,
    width: 12,
    height: 12,
    description: 'Package'
  }
});

const onSubmit = async (data: FormData) => {
  setLoading(true);
  setError(null);
  
  try {
    const quoteRequest: QuoteRequest = {
      originAddress: data.originAddress,
      destinationAddress: data.destinationAddress,
      cargo: [{
        weight: data.weight,
        length: data.length,
        width: data.width,
        height: data.height,
        quantity: 1,
        description: data.description
      }],
      shipDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().split('T')[0]
    };

    const response = await api.getQuotes(quoteRequest);
    setRates(response.rates);
  } catch (err: any) {
    setError(err.response?.data?.error?.message || 'Failed to get shipping rates');
  } finally {
    setLoading(false);
  }
};

const formatCurrency = (amount: number) => {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD'
  }).format(amount);
};

return (
  <div className="max-w-4xl mx-auto p-6 bg-white rounded-lg shadow-lg">
    <h2 className="text-2xl font-bold text-gray-900 mb-6">
      Shipping Rate Calculator
    </h2>
    
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
      {/* Origin Address */}
      <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
        <div>
          <h3 className="text-lg font-medium text-gray-900 mb-4">From</h3>
          <div className="space-y-4">
            <input
              {...register('originAddress.address1', { required: 'Address is required' })}
              placeholder="Street Address"
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
            />
            {errors.originAddress?.address1 && (
              <p className="text-red-500 text-sm">{errors.originAddress.address1.message}</p>
            )}
            
            <div className="grid grid-cols-2 gap-2">
              <input
                {...register('originAddress.city', { required: 'City is required' })}
                placeholder="City"
                className="px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
              />
              <input
                {...register('originAddress.state', { required: 'State is required' })}
                placeholder="State"
                className="px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
              />
            </div>
            
            <input
              {...register('originAddress.postalCode', { required: 'ZIP code is required' })}
              placeholder="ZIP Code"
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
            />
          </div>
        </div>

        {/* Destination Address */}
        <div>
          <h3 className="text-lg font-medium text-gray-900 mb-4">To</h3>
          <div className="space-y-4">
            <input
              {...register('destinationAddress.address1', { required: 'Address is required' })}
              placeholder="Street Address"
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
            />
            
            <div className="grid grid-cols-2 gap-2">
              <input
                {...register('destinationAddress.city', { required: 'City is required' })}
                placeholder="City"
                className="px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
              />
              <input
                {...register('destinationAddress.state', { required: 'State is required' })}
                placeholder="State"
                className="px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
              />
            </div>
            
            <input
              {...register('destinationAddress.postalCode', { required: 'ZIP code is required' })}
              placeholder="ZIP Code"
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
            />
          </div>
        </div>
      </div>

      {/* Package Details */}
      <div>
        <h3 className="text-lg font-medium text-gray-900 mb-4">Package Details</h3>
        <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
              Weight (lbs)
            </label>
            <input
              type="number"
              step="0.1"
              min="0.1"
              {...register('weight', { required: 'Weight is required', min: 0.1 })}
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
            />
          </div>
          
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
              Length (in)
            </label>
            <input
              type="number"
              step="0.1"
              min="0.1"
              {...register('length', { required: 'Length is required', min: 0.1 })}
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
            />
          </div>
          
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
              Width (in)
            </label>
            <input
              type="number"
              step="0.1"
              min="0.1"
              {...register('width', { required: 'Width is required', min: 0.1 })}
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
            />
          </div>
          
          <div>
            <label className="block text-sm font-medium text-gray-700 mb-1">
              Height (in)
            </label>
            <input
              type="number"
              step="0.1"
              min="0.1"
              {...register('height', { required: 'Height is required', min: 0.1 })}
              className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
            />
          </div>
        </div>
      </div>

      {/* Submit Button */}
      <button
        type="submit"
        disabled={loading}
        className="w-full bg-blue-600 text-white py-3 px-6 rounded-md hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
      >
        {loading ? 'Calculating Rates...' : 'Get Shipping Rates'}
      </button>
    </form>

    {/* Error Display */}
    {error && (
      <div className="mt-6 p-4 bg-red-50 border border-red-200 rounded-md">
        <p className="text-red-700">{error}</p>
      </div>
    )}

    {/* Rates Display */}
    {rates.length > 0 && (
      <div className="mt-8">
        <h3 className="text-lg font-medium text-gray-900 mb-4">
          Available Shipping Options
        </h3>
        <div className="space-y-3">
          {rates.map((rate, index) => (
            <div
              key={index}
              className="p-4 border border-gray-200 rounded-md hover:border-blue-300 cursor-pointer transition-colors"
              onClick={() => onRateSelect?.(rate)}
            >
              <div className="flex justify-between items-start">
                <div>
                  <h4 className="font-medium text-gray-900">
                    {rate.provider} - {rate.service}
                  </h4>
                  <p className="text-sm text-gray-600">
                    Delivers in {rate.transitDays} business day{rate.transitDays !== 1 ? 's' : ''}
                    {rate.isGuaranteed && (
                      <span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
                        Guaranteed
                      </span>
                    )}
                  </p>
                </div>
                <div className="text-right">
                  <p className="text-xl font-bold text-gray-900">
                    {formatCurrency(rate.totalCost)}
                  </p>
                  <p className="text-sm text-gray-600">
                    by {new Date(rate.deliveryDate).toLocaleDateString()}
                  </p>
                </div>
              </div>
            </div>
          ))}
        </div>
      </div>
    )}
  </div>
);
};

export default ShippingCalculator;

3. Usage in Your App

src/App.tsxtsx
import React from 'react';
import ShippingCalculator from './components/ShippingCalculator';
import './App.css';

function App() {
const handleRateSelect = (rate) => {
  console.log('Selected rate:', rate);
  // Handle rate selection (e.g., proceed to checkout)
};

return (
  <div className="App">
    <div className="min-h-screen bg-gray-50 py-12">
      <div className="container mx-auto px-4">
        <h1 className="text-3xl font-bold text-center text-gray-900 mb-8">
          Flash Americas Shipping Calculator
        </h1>
        
        <ShippingCalculator
          apiKey={process.env.REACT_APP_FLASH_AMERICAS_API_KEY!}
          onRateSelect={handleRateSelect}
        />
      </div>
    </div>
  </div>
);
}

export default App;

Advanced Features

Real-time Validation

Add real-time address validation as users type:

Real-time validation hooktypescript
import { useState, useEffect, useCallback } from 'react';
import { debounce } from 'lodash';
import FlashAmericasAPI, { Address } from '../lib/flashAmericasApi';

export const useAddressValidation = (apiKey: string) => {
const [validationResults, setValidationResults] = useState<Record<string, boolean>>({});
const [validating, setValidating] = useState<Record<string, boolean>>({});

const api = new FlashAmericasAPI(apiKey);

const validateAddress = useCallback(
  debounce(async (key: string, address: Partial<Address>) => {
    if (!address.city || !address.state || !address.postalCode) return;
    
    setValidating(prev => ({ ...prev, [key]: true }));
    
    try {
      await api.validateAddress(address);
      setValidationResults(prev => ({ ...prev, [key]: true }));
    } catch (error) {
      setValidationResults(prev => ({ ...prev, [key]: false }));
    } finally {
      setValidating(prev => ({ ...prev, [key]: false }));
    }
  }, 500),
  [api]
);

return { validateAddress, validationResults, validating };
};

Error Boundary

Wrap your component with an error boundary for better error handling:

src/components/ErrorBoundary.tsxtsx
import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
children: ReactNode;
}

interface State {
hasError: boolean;
error?: Error;
}

class ErrorBoundary extends Component<Props, State> {
public state: State = {
  hasError: false
};

public static getDerivedStateFromError(error: Error): State {
  return { hasError: true, error };
}

public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
  console.error('Shipping calculator error:', error, errorInfo);
}

public render() {
  if (this.state.hasError) {
    return (
      <div className="p-6 bg-red-50 border border-red-200 rounded-md">
        <h2 className="text-lg font-medium text-red-800 mb-2">
          Something went wrong
        </h2>
        <p className="text-red-700">
          We encountered an error while calculating shipping rates. 
          Please try again or contact support if the problem persists.
        </p>
        <button
          onClick={() => this.setState({ hasError: false })}
          className="mt-4 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700"
        >
          Try Again
        </button>
      </div>
    );
  }

  return this.props.children;
}
}

export default ErrorBoundary;

Environment Variables

Create a .env file in your project root:

.envbash
REACT_APP_FLASH_AMERICAS_API_KEY=your_api_key_here
REACT_APP_API_URL=https://ship.flashamericas.com/api/v1

Testing

Example unit tests for the shipping calculator:

src/components/__tests__/ShippingCalculator.test.tsxtypescript
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import ShippingCalculator from '../ShippingCalculator';

// Mock the API
jest.mock('../lib/flashAmericasApi');

describe('ShippingCalculator', () => {
const mockApiKey = 'test-api-key';

beforeEach(() => {
  jest.clearAllMocks();
});

test('renders shipping calculator form', () => {
  render(<ShippingCalculator apiKey={mockApiKey} />);
  
  expect(screen.getByText('Shipping Rate Calculator')).toBeInTheDocument();
  expect(screen.getByText('From')).toBeInTheDocument();
  expect(screen.getByText('To')).toBeInTheDocument();
  expect(screen.getByText('Package Details')).toBeInTheDocument();
});

test('validates required fields', async () => {
  render(<ShippingCalculator apiKey={mockApiKey} />);
  
  const submitButton = screen.getByText('Get Shipping Rates');
  fireEvent.click(submitButton);

  await waitFor(() => {
    expect(screen.getByText('Address is required')).toBeInTheDocument();
  });
});

test('displays rates after successful API call', async () => {
  const mockRates = [
    {
      provider: 'FEDEX',
      service: 'FEDEX_GROUND',
      totalCost: 12.45,
      currency: 'USD',
      transitDays: 2,
      deliveryDate: '2025-02-22',
      isGuaranteed: false,
      breakdown: { baseCost: 10.95, fuelSurcharge: 1.50, accessorials: {} }
    }
  ];

  // Mock successful API response
  const mockApi = require('../lib/flashAmericasApi').default;
  mockApi.prototype.getQuotes = jest.fn().mockResolvedValue({
    quoteId: 'test-quote-id',
    rates: mockRates,
    expiresAt: '2025-02-22T10:00:00Z'
  });

  render(<ShippingCalculator apiKey={mockApiKey} />);
  
  // Fill out form
  fireEvent.change(screen.getByPlaceholderText('Street Address'), {
    target: { value: '123 Main St' }
  });
  // ... fill other required fields
  
  fireEvent.click(screen.getByText('Get Shipping Rates'));

  await waitFor(() => {
    expect(screen.getByText('Available Shipping Options')).toBeInTheDocument();
    expect(screen.getByText('FEDEX - FEDEX_GROUND')).toBeInTheDocument();
    expect(screen.getByText('$12.45')).toBeInTheDocument();
  });
});
});

Deployment

Vercel Deployment

Deploy to Vercelbash
# Install Vercel CLI
npm i -g vercel

# Deploy
vercel

# Set environment variables
vercel env add REACT_APP_FLASH_AMERICAS_API_KEY

Netlify Deployment

Deploy to Netlifybash
# Build the project
npm run build

# Deploy to Netlify (drag the build folder to netlify.com)
# Or use Netlify CLI
npm install -g netlify-cli
netlify deploy --prod --dir=build

Next Steps

  1. Add address autocomplete using Google Places API
  2. Implement carrier logos for better visual appeal
  3. Add package templates for common shipping sizes
  4. Integrate with payment processing for complete checkout flow
  5. Add shipment tracking after booking

Support

Related Examples