🔧 AI Nachrichten Major AI platforms go down in unprecedented simultaneous outage(03.09.2026 um 17:34 Uhr)
🔧 AI Nachrichten ChatGPT, Claude, and Grok Down? Users Report Widespread Outages(03.09.2026 um 19:14 Uhr)
🔧 AI Nachrichten OpenAI Launches GPT-6 Astra, Says We May Have Entered the AGI Era(03.09.2026 um 22:08 Uhr)
🔧 AI Nachrichten Claude Comes to CarPlay as Fifth Major AI Chatbot App(05.09.2026 um 05:31 Uhr)
🔧 AI Nachrichten OpenAI’s GPT-6 Astra Is AGI, Says NVIDIA CEO Jensen Huang(07.09.2026 um 06:31 Uhr)
🔧 AI Nachrichten Blame AI companies for Mac mini and Mac Studio shortage(31.08.2026 um 10:32 Uhr)
🔧 AI Nachrichten Major AI platforms go down in unprecedented simultaneous outage(03.09.2026 um 17:34 Uhr)
🔧 AI Nachrichten ChatGPT, Claude, and Grok Down? Users Report Widespread Outages(03.09.2026 um 19:14 Uhr)
🔧 AI Nachrichten OpenAI Launches GPT-6 Astra, Says We May Have Entered the AGI Era(03.09.2026 um 22:08 Uhr)
🔧 AI Nachrichten Claude Comes to CarPlay as Fifth Major AI Chatbot App(05.09.2026 um 05:31 Uhr)
🔧 AI Nachrichten OpenAI’s GPT-6 Astra Is AGI, Says NVIDIA CEO Jensen Huang(07.09.2026 um 06:31 Uhr)
🔧 AI Nachrichten Blame AI companies for Mac mini and Mac Studio shortage(31.08.2026 um 10:32 Uhr)

🔧 Programmierung 🕛 kürzlich 17 Min Lesezeit
0

How to Build a Quote Request System in React

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

Most "contact us" forms on freelancer and agency sites do the same thing. A visitor types a message, hits send, and the message lands somewhere in an inbox. There is no structure to it. You cannot tell what service someone wants, what their budget looks like, or how urgent the project is until you open the email and read it line by line.



A quote request system fixes that. Instead of one open text box, you ask the questions that actually help you price and prioritize work: what kind of project this is, what the scope looks like, what budget range they have in mind, and how soon they need it done. Every submission arrives already sorted.



In this tutorial, you will build a complete quote request system in React. Not just a form, a full pipeline: a form component that collects structured project details, client-side validation, a submit handler that posts to a backend, an instant email notification, and a lead management view where you can track every request from New to Contacted to Converted.



Here is the architecture you are building:




CODE
Form Component -> Validation -> Submit -> Backend API -> Email Notification -> Lead Management






The frontend is plain React. For the backend, email notification, and lead management pieces, you will use Formgrid, an open-source form backend, so you can focus this tutorial on the part that actually needs your attention: building a form that captures the right information and converts.









What You Are Building



A multi-field quote request form in React with:




  • Full name, email, company, and phone fields

  • A radio group for the type of work being quoted

  • A textarea for project scope

  • Radio groups for budget range and timeline

  • Client-side validation before anything gets submitted

  • A loading state while the request is in flight

  • A success state once the request lands



On the backend side, every submission becomes:




  • An instant email notification to your inbox

  • A row in your Formgrid submissions dashboard

  • A tracked lead with a status of New, which you can move to Contacted or Converted, add notes to, and set a follow-up reminder on



No server code. No database. No separate CRM.









Prerequisites



Before starting, make sure you have:




  • Node.js 18 or higher installed

  • Basic familiarity with React and hooks

  • A free Formgrid account (you will create one during this tutorial)









Step 1: Set Up the React Project



Create a new React project using Vite:




CODE
npm create vite@latest quote-request-system -- --template react
cd quote-request-system
npm install
npm run dev






Open the local dev server URL in your browser and confirm the default page loads.



and sign up using Google or email. No credit card required.





You will land on the form's Overview tab. Copy your endpoint URL. It looks like this:




CODE
https://formgrid.dev/api/f/your-form-id






and customize it without creating an account first. You can later embed it in your website.



This tutorial builds the form by hand in React so you understand every piece, but the template is there if you want a shortcut for the Formgrid side of things.







Step 3: Build the Project Structure



Inside src, create a components folder and a QuoteRequestForm.jsx file inside it:




CODE
src/
components/
QuoteRequestForm.jsx
QuoteRequestForm.css
App.jsx






Open App.jsx and replace its contents with:




CODE
import { useMemo, useState } from 'react';
import './QuoteRequestForm.css';

const FORMGRID_ENDPOINT = 'https://formgrid.dev/api/f/brcy3qd4';

const HERO_IMAGE =
'https://images.unsplash.com/photo-1521737604893-d14cc237f11d?auto=format&fit=crop&w=1400&q=80';

const initialState = {
company: '',
fullName: '',
email: '',
phone: '',
quoteFor: '',
budget: '',
timeline: '',
projectScope: '',
};

const quoteForOptions = [
'Web design and development',
'Branding and creative',
'Marketing and advertising',
'Construction or renovation',
'Consulting or professional services',
'Other',
];

const budgetOptions = [
'Under $5,000',
'$5,000 – $15,000',
'$15,000 – $50,000',
'$50,000+',
'Not sure yet',
];

const timelineOptions = [
'ASAP — within 2 weeks',
'Within 1 month',
'Within 3 months',
'Flexible: No fixed deadline',
];

const requiredFields = ['fullName', 'email', 'quoteFor', 'budget', 'projectScope'];

export default function QuoteRequestForm() {
const [formData, setFormData] = useState(initialState);
const [errors, setErrors] = useState({});
const [status, setStatus] = useState('idle');

const filledCount = useMemo(
() => requiredFields.filter((key) => formData[key].trim() !== '').length,
[formData],
);
const progress = Math.round((filledCount / requiredFields.length) * 100);

function handleChange(e) {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors((prev) => ({ ...prev, [name]: undefined }));
}
}

function validate() {
const nextErrors = {};

if (!formData.fullName.trim()) nextErrors.fullName = 'Please enter your name';

if (!formData.email.trim()) {
nextErrors.email = 'Email is required';
} else if (!/^\S+@\S+\.\S+$/.test(formData.email)) {
nextErrors.email = 'Enter a valid email address';
}

if (!formData.quoteFor) nextErrors.quoteFor = 'Select what you need';
if (!formData.budget) nextErrors.budget = 'Select an estimated budget';
if (!formData.projectScope.trim()) nextErrors.projectScope = 'Tell us about your project';

setErrors(nextErrors);
return Object.keys(nextErrors).length === 0;
}

async function handleSubmit(e) {
e.preventDefault();
if (!validate()) return;

setStatus('sending');

try {
const res = await fetch(FORMGRID_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ formData }),
});

if (res.ok) {
setStatus('success');
setFormData(initialState);
} else {
setStatus('error');
}
} catch {
setStatus('error');
}
}

if (status === 'success') {
return (
<div className="qf-card qf-success">
<div className="qf-success__badge" aria-hidden="true">
<svg viewBox="0 0 24 24" width="34" height="34" fill="none">
<path
d="m5 13 4 4L19 7"
stroke="currentColor"
strokeWidth="2.4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<h2>Request received</h2>
<p>
Thanks! We've got your details and will send a tailored quote within 2
business days.
</p>
<button
type="button"
className="qf-btn qf-btn--ghost"
onClick={() => setStatus('idle')}
>
Submit another request
</button>
</div>
);
}

return (
<div className="qf-card">
<header className="qf-topbar">
<div>
<p className="qf-topbar__title">Quote Request Form</p>
<p className="qf-topbar__hint">
Fill in all required fields marked with <span className="qf-req">*</span>
</p>
</div>
<div className="qf-progress" aria-hidden="true">
<span className="qf-progress__label">
{filledCount} of {requiredFields.length} required fields filled
</span>
<span className="qf-progress__track">
<span className="qf-progress__bar" style={{ width: `${progress}%` }} />
</span>
</div>
</header>

<div className="qf-hero">
<h1>Request a quote</h1>
<p>
Tell us about your project and we&apos;ll send a tailored quote within 2 business
days. No commitment required.
</p>
<div className="qf-hero__image">
<img src={HERO_IMAGE} alt="Two colleagues collaborating at a desk" />
</div>
</div>

<form className="qf-form" onSubmit={handleSubmit} noValidate>
<section className="qf-section">
<h2 className="qf-section__title">Contact details</h2>
<div className="qf-grid">
<div className="qf-field">
<label htmlFor="company">Company</label>
<input
id="company"
name="company"
type="text"
placeholder="Acme Inc."
value={formData.company}
onChange={handleChange}
/>
</div>

<div className="qf-field">
<label htmlFor="fullName">
Your name <span className="qf-req">*</span>
</label>
<input
id="fullName"
name="fullName"
type="text"
placeholder="Jane Smith"
aria-invalid={Boolean(errors.fullName)}
value={formData.fullName}
onChange={handleChange}
/>
{errors.fullName && <span className="qf-error">{errors.fullName}</span>}
</div>

<div className="qf-field">
<label htmlFor="email">
Email <span className="qf-req">*</span>
</label>
<input
id="email"
name="email"
type="email"
placeholder="[email protected]"
aria-invalid={Boolean(errors.email)}
value={formData.email}
onChange={handleChange}
/>
{errors.email && <span className="qf-error">{errors.email}</span>}
</div>

<div className="qf-field">
<label htmlFor="phone">Phone</label>
<input
id="phone"
name="phone"
type="tel"
placeholder="+1 (555) 000-0000"
value={formData.phone}
onChange={handleChange}
/>
</div>
</div>
</section>

<div className="qf-divider" />

<section className="qf-section">
<h2 className="qf-section__title">Project details</h2>

<div className="qf-field">
<label htmlFor="quoteFor">
What do you need? <span className="qf-req">*</span>
</label>
<div className="qf-select">
<select
id="quoteFor"
name="quoteFor"
aria-invalid={Boolean(errors.quoteFor)}
value={formData.quoteFor}
onChange={handleChange}
>
<option value="" disabled>
Select…
</option>
{quoteForOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<svg
className="qf-select__chevron"
viewBox="0 0 24 24"
width="18"
height="18"
aria-hidden="true"
>
<path
d="m6 9 6 6 6-6"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
{errors.quoteFor && <span className="qf-error">{errors.quoteFor}</span>}
</div>

<fieldset className="qf-field qf-fieldset">
<legend>
Estimated budget <span className="qf-req">*</span>
</legend>
<div className="qf-radios">
{budgetOptions.map((option) => (
<label
key={option}
className={`qf-radio ${formData.budget === option ? 'is-selected' : ''
}`}
>
<input
type="radio"
name="budget"
value={option}
checked={formData.budget === option}
onChange={handleChange}
/>
<span>{option}</span>
</label>
))}
</div>
{errors.budget && <span className="qf-error">{errors.budget}</span>}
</fieldset>

<div className="qf-field">
<label htmlFor="timeline">When do you need to start?</label>
<div className="qf-select">
<select
id="timeline"
name="timeline"
value={formData.timeline}
onChange={handleChange}
>
<option value="" disabled>
Select…
</option>
{timelineOptions.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
<svg
className="qf-select__chevron"
viewBox="0 0 24 24"
width="18"
height="18"
aria-hidden="true"
>
<path
d="m6 9 6 6 6-6"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
</div>

<div className="qf-field">
<label htmlFor="projectScope">
Describe your project <span className="qf-req">*</span>
</label>
<textarea
id="projectScope"
name="projectScope"
rows={5}
placeholder="Share goals, deliverables, timeline, references, or links to inspiration…"
aria-invalid={Boolean(errors.projectScope)}
value={formData.projectScope}
onChange={handleChange}
/>
{errors.projectScope && (
<span className="qf-error">{errors.projectScope}</span>
)}
</div>
</section>

{status === 'error' && (
<p className="qf-alert" role="alert">
Something went wrong while sending your request. Please try again.
</p>
)}

<button type="submit" className="qf-btn" disabled={status === 'sending'}>
{status === 'sending' ? 'Sending…' : 'Submit quote request'}
</button>
</form>
</div>
);
}






Replace FORMGRID_ENDPOINT with the endpoint URL you copied in Step 2.











Step 6: Confirm the Submission Arrives



Fill in the form with real-looking test data and click Request Quote.





Second, the same submission appears in your Leads tab with a status of New. This is the part a plain email notification cannot give you.





Add a private note after every call. What they asked for, what you quoted, what the next step is.




CODE
Spoke on the phone Tuesday. Wants the full 6- to 8-page site plus blog. Quoted $9,500.
Waiting on her to confirm the October trade show date before we lock the timeline.









This is the part that separates a form that just collects submissions from a system that actually helps you close work. A quote request that goes cold because nobody followed up is a lost project. A tracked lead with a reminder attached is not.









Step 8: Add Spam Protection



Before this goes live, turn on spam protection from your form settings.



Go to your form's Settings tab and scroll to Security Settings. You can enable a honeypot field, CAPTCHA, rate limiting, or restrict submissions to your own domain.


















Why Not Just Use a Plain Contact Form



A single message box is faster to build, but it pushes all the sorting work onto you after the fact. You have to read every message to figure out what someone actually wants, guess at their budget, and manually track who you have followed up with, usually in your own head or a scattered set of email threads.



A structured quote request form does that sorting up front. By the time a request lands in your inbox, you already know the service, the scope, the budget range, and the timeline. Paired with a lead pipeline, you also know exactly where every request stands, instead of losing track of the ones that went quiet.









Deploying



Build the project for production:




CODE
npm run build






Deploy the dist folder to any static host; Vercel, Netlify, or Cloudflare Pages all work with zero configuration for a Vite project. Your form will keep working identically in production, since Formgrid handles the backend processing regardless of where the frontend is hosted.









Final Thoughts



A quote request form is a small amount of extra structure that pays off every time someone submits one. You spend less time reading and guessing, and more time responding with an actual number, because the information you need was collected upfront.



The React side of this tutorial is a form component, a validation function, and a fetch call. The part that turns it into a system, tracking who you have contacted, who converted, and who needs a follow-up, comes from Formgrid running underneath it.



If you want to build this for your own site, start free at

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
3 Quellen
GPT-6 Astra Release Today? OpenAI’s Next Major AI Model Is Almost Here
1 Quelle
Apple accuses OpenAI of destroying evidence as trade-secrets fight intensifies
1 Quelle
Major AI platforms go down in unprecedented simultaneous outage
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten How to Build a Quote Request System in React

Thematisch verwandte Begriffe: Build, Quote, Request, System · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...