{"id":135338,"date":"2025-11-05T13:10:45","date_gmt":"2025-11-05T13:10:45","guid":{"rendered":"https:\/\/phppot.com\/?p=24497"},"modified":"2025-11-05T13:10:45","modified_gmt":"2025-11-05T13:10:45","slug":"save-react-form-data-to-google-sheets-without-a-backend-step-by-step-guide","status":"publish","type":"post","link":"https:\/\/sickgaming.net\/blog\/2025\/11\/05\/save-react-form-data-to-google-sheets-without-a-backend-step-by-step-guide\/","title":{"rendered":"Save React Form Data to Google Sheets Without a Backend (Step-by-Step Guide)"},"content":{"rendered":"<div class=\"modified-on\" readability=\"7.1489361702128\"> by <a href=\"https:\/\/phppot.com\/about\/\">Vincy<\/a>. Last modified on November 12th, 2025.<\/div>\n<p>React form can be tied to a Google Sheets to store the submitted data. It maintains the form responses in an Excel format without database. This can be done by deploying a Google App Script for the target sheet.<\/p>\n<p>In this tutorial, you will learn the steps to create a new Google App Script and deploy it for a Google Sheets.<\/p>\n<p>The Google Sheets will have columns relevant to the React form fields. The Google web app script URL parameters are in the same order as the column. In a previous tutorial, we saw how to <a href=\"https:\/\/phppot.com\/php\/google-sheet-php\/\">connect Google Sheets via API from a PHP<\/a> application.<\/p>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-large wp-image-24588\" src=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-google-sheets-no-backend-form-550x288.jpeg\" alt=\"React Google Sheets No Backend Form\" width=\"550\" height=\"288\" srcset=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-google-sheets-no-backend-form-550x288.jpeg 550w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-google-sheets-no-backend-form-300x157.jpeg 300w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-google-sheets-no-backend-form-768x402.jpeg 768w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-google-sheets-no-backend-form.jpeg 1200w\" sizes=\"auto, (max-width: 550px) 100vw, 550px\"><\/p>\n<h2>Steps to get Google Sheets URL to post form data<\/h2>\n<p>There are 5 simple steps to get the Google Sheets web app URL by registering an app script for a target sheet. At the end of these 5 steps, it will generate a URL that has to be configured in the React frontend code.<\/p>\n<p>In the frontend, this URL will have a form data bundle to process the data row insertion as coded in the app script.<\/p>\n<h3>1. Create a Google sheet with the column relevant to the React form<\/h3>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-large wp-image-24589\" src=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/target-google-sheet-550x336.png\" alt=\"Target Google Sheet\" width=\"550\" height=\"336\" srcset=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/target-google-sheet-550x336.png 550w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/target-google-sheet-300x183.png 300w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/target-google-sheet-768x469.png 768w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/target-google-sheet-1536x938.png 1536w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/target-google-sheet.png 1966w\" sizes=\"auto, (max-width: 550px) 100vw, 550px\"><\/p>\n<h3>2. Navigate Extension -&gt; App Script to add JS script to build row to insert<\/h3>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-large wp-image-24590\" src=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/append-row-via-javascript-550x304.png\" alt=\"Append Row via JavaScript\" width=\"550\" height=\"304\" srcset=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/append-row-via-javascript-550x304.png 550w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/append-row-via-javascript-300x166.png 300w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/append-row-via-javascript-768x425.png 768w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/append-row-via-javascript-1536x849.png 1536w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/append-row-via-javascript-2048x1133.png 2048w\" sizes=\"auto, (max-width: 550px) 100vw, 550px\"><\/p>\n<h3>3. Choose Deploy -&gt; New Deployment to configure web app<\/h3>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-large wp-image-24591\" src=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/configure-web-app-type-550x360.png\" alt=\"Configure Web App Type\" width=\"550\" height=\"360\" srcset=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/configure-web-app-type-550x360.png 550w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/configure-web-app-type-300x196.png 300w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/configure-web-app-type-768x503.png 768w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/configure-web-app-type-1536x1005.png 1536w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/configure-web-app-type-2048x1340.png 2048w\" sizes=\"auto, (max-width: 550px) 100vw, 550px\"><\/p>\n<h3>4. Set ownership configurations and authorize the app<\/h3>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-large wp-image-24592\" src=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/configure-webapp-restriction-settings-550x359.jpg\" alt=\"Configure Web App Restriction Settings\" width=\"550\" height=\"359\" srcset=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/configure-webapp-restriction-settings-550x359.jpg 550w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/configure-webapp-restriction-settings-300x196.jpg 300w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/configure-webapp-restriction-settings-768x501.jpg 768w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/configure-webapp-restriction-settings-1536x1003.jpg 1536w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/configure-webapp-restriction-settings-2048x1337.jpg 2048w\" sizes=\"auto, (max-width: 550px) 100vw, 550px\"><\/p>\n<h3>5. Click Deploy and copy the Google Sheets web app URL<\/h3>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-large wp-image-24593\" src=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/web-app-url-generation-550x359.jpg\" alt=\"Web App URL Generation\" width=\"550\" height=\"359\" srcset=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/web-app-url-generation-550x359.jpg 550w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/web-app-url-generation-300x196.jpg 300w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/web-app-url-generation-768x501.jpg 768w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/web-app-url-generation-1536x1003.jpg 1536w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/web-app-url-generation-2048x1337.jpg 2048w\" sizes=\"auto, (max-width: 550px) 100vw, 550px\"><\/p>\n<h2>React frontend form JSX with required handlers<\/h2>\n<p>The <code>ReactForm<\/code> JSX component includes the form UI and hooks to process the form submit. This simple form collects payment details to store in the Google Sheets.<\/p>\n<p>In the above steps we get the Google App script URL to target the sheet from the frontend. This URL is used in this JSX with the form\u2019s <code>handleSubmit<\/code> function. This URL is added to the <code>GOOGLE_SHEET_URL<\/code> variable and used in the form action hook.<\/p>\n<p>The <code>URLSearchParams<\/code> builds the argument list with the submitted <a href=\"https:\/\/phppot.com\/react\/react-hook-user-registration-form\/\">React form data<\/a>. Google Sheets URL will receive these arguments in <code>key1=value1&amp;key2=value2..<\/code> format.<\/p>\n<p>Once the submitted data is added to the Google Sheets, the frontend will clear the form and show a success toast message to the user.<\/p>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-large wp-image-24501\" src=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-google-sheet-form-550x518.jpg\" alt=\"react google sheet form\" width=\"550\" height=\"518\" srcset=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-google-sheet-form-550x518.jpg 550w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-google-sheet-form-300x282.jpg 300w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-google-sheet-form-768x723.jpg 768w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-google-sheet-form.jpg 940w\" sizes=\"auto, (max-width: 550px) 100vw, 550px\"><\/p>\n<p class=\"code-heading\"><code>src\/components\/ReactForm.jsx<\/code><\/p>\n<pre class=\"prettyprint\"><code class=\"language-jsx\">import { useState } from \"react\";\nimport axios from \"axios\";\nimport { ToastContainer, toast } from \"react-toastify\";\nimport \"react-toastify\/dist\/ReactToastify.css\";\nimport \"..\/..\/public\/assets\/css\/react-style.css\";\nimport PaymentFormFields from \".\/PaymentFormFields\";\nconst GOOGLE_SHEET_URL = \"Paste your Google Apps Script Web App URL here\";\nconst ReactForm = () =&gt; { const [formData, setFormData] = useState({ projectName: \"\", amount: \"\", currency: \"\", paymentDate: \"\", invoiceNumber: \"\", paymentMode: \"\", note: \"\", }); const handleChange = (e) =&gt; { const { name, value } = e.target; setFormData((prev) =&gt; ({ ...prev, [name]: value })); }; const handleSubmit = async (e) =&gt; { e.preventDefault(); try { const params = new URLSearchParams(formData).toString(); const response = await axios.post(`${GOOGLE_SHEET_URL}?${params}`); if (response.data.status === \"success\") { toast.success(\"Data saved to Google Sheet!\", { position: \"top-center\" }); setFormData({ projectName: \"\", amount: \"\", currency: \"\", paymentDate: \"\", invoiceNumber: \"\", paymentMode: \"\", note: \"\", }); } else { toast.error(\"Failed to save data. Try again.\", { position: \"top-center\" }); } } catch (error) { console.error(\"Error:\", error); toast.error(\"Something went wrong while submitting.\", { position: \"top-center\", }); } }; return ( &lt;div className=\"form-wrapper\"&gt; &lt;h2 className=\"form-title\"&gt;Payment Entry&lt;\/h2&gt; &lt;form onSubmit={handleSubmit} className=\"payment-form\"&gt; &lt;PaymentFormFields formData={formData} handleChange={handleChange} \/&gt; &lt;button type=\"submit\" className=\"submit-btn disabled={loading}\"&gt; {loading ? \"Processing...\" : \"Submit\"} &lt;\/button&gt; &lt;\/form&gt; &lt;ToastContainer \/&gt; &lt;\/div&gt; );\n};\nexport default ReactForm; <\/code><\/pre>\n<p class=\"code-heading\"><code>src\/components\/PaymentFormFields.jsx<\/code><\/p>\n<pre class=\"prettyprint\"><code class=\"language-jsx\">const PaymentFormFields = ({ formData, handleChange }) =&gt; { return ( &lt;&gt; &lt;div className=\"form-group\"&gt; &lt;label className=\"form-label\"&gt;Project Name&lt;\/label&gt; &lt;input type=\"text\" name=\"projectName\" value={formData.projectName} onChange={handleChange} className=\"form-input\" required \/&gt; &lt;\/div&gt; &lt;div className=\"form-group\"&gt; &lt;label className=\"form-label\"&gt;Amount&lt;\/label&gt; &lt;input type=\"number\" name=\"amount\" value={formData.amount} onChange={handleChange} className=\"form-input\" required \/&gt; &lt;\/div&gt; &lt;div className=\"form-group\"&gt; &lt;label className=\"form-label\"&gt;Currency&lt;\/label&gt; &lt;select name=\"currency\" value={formData.currency} onChange={handleChange} className=\"form-input\" required &gt; &lt;option value=\"\"&gt;Select Currency&lt;\/option&gt; &lt;option value=\"USD\"&gt;USD&lt;\/option&gt; &lt;option value=\"INR\"&gt;INR&lt;\/option&gt; &lt;option value=\"EUR\"&gt;EUR&lt;\/option&gt; &lt;\/select&gt; &lt;\/div&gt; &lt;div className=\"form-group\"&gt; &lt;label className=\"form-label\"&gt;Payment Date&lt;\/label&gt; &lt;input type=\"date\" name=\"paymentDate\" value={formData.paymentDate} onChange={handleChange} className=\"form-input\" required \/&gt; &lt;\/div&gt; &lt;div className=\"form-group\"&gt; &lt;label className=\"form-label\"&gt;Invoice Number&lt;\/label&gt; &lt;input type=\"text\" name=\"invoiceNumber\" value={formData.invoiceNumber} onChange={handleChange} className=\"form-input\" required \/&gt; &lt;\/div&gt; &lt;div className=\"form-group\"&gt; &lt;label className=\"form-label\"&gt;Payment Mode&lt;\/label&gt; &lt;select name=\"paymentMode\" value={formData.paymentMode} onChange={handleChange} className=\"form-input\" required &gt; &lt;option value=\"\"&gt;Select Mode&lt;\/option&gt; &lt;option value=\"Cash\"&gt;Cash&lt;\/option&gt; &lt;option value=\"Bank Transfer\"&gt;Bank Transfer&lt;\/option&gt; &lt;option value=\"Credit Card\"&gt;Credit Card&lt;\/option&gt; &lt;option value=\"UPI\"&gt;UPI&lt;\/option&gt; &lt;\/select&gt; &lt;\/div&gt; &lt;div className=\"form-group\"&gt; &lt;label className=\"form-label\"&gt;Note&lt;\/label&gt; &lt;textarea name=\"note\" value={formData.note} onChange={handleChange} className=\"form-input\" rows=\"3\" &gt;&lt;\/textarea&gt; &lt;\/div&gt; &lt;\/&gt; );\n}; export default PaymentFormFields;\n<\/code><\/pre>\n<h2>Appending new row to the Google Sheets using the Web App Script<\/h2>\n<p>I gave the web app script in the downloadable source code added to this tutorial. This JS script is added to the Google Sheets App script extension.<\/p>\n<p>This script will be executed when the form post action calls the web app URL.&nbsp; The <code>doPost()<\/code> function builds the Google Sheets row instance with the parameters posted from the form.<\/p>\n<p>With the line <code>sheet.appendRow(row);<\/code> we can return the ContentService with a success response.<\/p>\n<p>The <code>formatOnly<\/code> step is optional to maintain all the rows with the same styles as the sheet header has. For example, if you <a href=\"https:\/\/phppot.com\/jquery\/jquery-table-row-column-highlight-on-hover\/\">highlight any column with a bright background<\/a>, that will be carried over to the next rows added by the app script.<\/p>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-large wp-image-24503\" src=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-google-sheet-form-data-550x127.jpg\" alt=\"react google sheet form data\" width=\"550\" height=\"127\" srcset=\"https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-google-sheet-form-data-550x127.jpg 550w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-google-sheet-form-data-300x69.jpg 300w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-google-sheet-form-data-768x177.jpg 768w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-google-sheet-form-data-1536x355.jpg 1536w, https:\/\/phppot.com\/wp-content\/uploads\/2025\/11\/react-google-sheet-form-data.jpg 1757w\" sizes=\"auto, (max-width: 550px) 100vw, 550px\"><\/p>\n<p class=\"code-heading\"><code>google-sheet-app\/app-script-target.js<\/code><\/p>\n<pre class=\"prettyprint\"><code class=\"language-js\">function doPost(e) { if (!e || !e.parameter) { return ContentService .createTextOutput(JSON.stringify({ status: \"error\", message: \"No parameters received\" })) .setMimeType(ContentService.MimeType.JSON); } const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(\"Sheet1\"); const row = [ e.parameter.projectName || \"\", e.parameter.amount || \"\", e.parameter.currency || \"\", e.parameter.paymentDate || \"\", e.parameter.invoiceNumber || \"\", e.parameter.paymentMode || \"\", e.parameter.note || \"\", ]; sheet.appendRow(row); const lastRow = sheet.getLastRow(); const lastColumn = sheet.getLastColumn(); const headerRange = sheet.getRange(1, 1, 1, lastColumn); const newRowRange = sheet.getRange(lastRow, 1, 1, lastColumn); headerRange.copyTo(newRowRange, { formatOnly: true }); return ContentService .createTextOutput(JSON.stringify({ status: \"success\" })) .setMimeType(ContentService.MimeType.JSON);\n}\n<\/code><\/pre>\n<h2>Conclusion<\/h2>\n<p>By linking a React form to a Google Sheets via the Google Apps Script, form data is stored in excel format. This will be very useful to maintain form responses without a backend database. The App Script created for this tutorial provided a feature to keep the row column formatting with the newly added rows.<\/p>\n<p>As an enhancement, we can extend this code to <a href=\"https:\/\/phppot.com\/php\/php-read-google-sheet\/\">read Google sheets<\/a> and show the latest records to the UI.<\/p>\n<p><strong>References<\/strong><\/p>\n<ol>\n<li><a href=\"https:\/\/developers.google.com\/apps-script\/guides\/web\" target=\"_blank\" rel=\"noopener\">Google Apps Script Web App documentation<\/a>.<\/li>\n<li><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/URLSearchParams\" target=\"_blank\" rel=\"noopener\">Bundling form data with URL parameters<\/a>.<\/li>\n<\/ol>\n<p><a class=\"download\" href=\"https:\/\/phppot.com\/downloads\/react\/react-google-sheets-no-backend-form-tutorial.zip\">Download<\/a><\/p>\n<div class=\"written-by\" readability=\"9.8427672955975\">\n<div class=\"author-photo\"> <img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/phppot.com\/wp-content\/themes\/solandra\/images\/Vincy.jpg\" alt=\"Vincy\" width=\"100\" height=\"100\" title=\"Vincy\"> <\/div>\n<div class=\"written-by-desc\" readability=\"14.764150943396\"> Written by <a href=\"https:\/\/phppot.com\/about\/\">Vincy<\/a>, a web developer with 15+ years of experience and a Masters degree in Computer Science. She specializes in building modern, lightweight websites using PHP, JavaScript, React, and related technologies. Phppot helps you in mastering web development through over a decade of publishing quality tutorials. <\/div>\n<\/p><\/div>\n<p> <!-- #comments --> <\/p>\n<div class=\"related-articles\">\n<h2>Related Tutorials<\/h2>\n<\/p><\/div>\n<p> <a href=\"https:\/\/phppot.com\/react\/react-google-sheets-no-backend-form-tutorial\/#top\" class=\"top\">\u2191 Back to Top<\/a> <\/p>\n","protected":false},"excerpt":{"rendered":"<p>by Vincy. Last modified on November 12th, 2025. React form can be tied to a Google Sheets to store the submitted data. It maintains the form responses in an Excel format without database. This can be done by deploying a Google App Script for the target sheet. In this tutorial, you will learn the steps [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":135339,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[65],"tags":[],"class_list":["post-135338","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-php-updates"],"_links":{"self":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/posts\/135338","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/comments?post=135338"}],"version-history":[{"count":0,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/posts\/135338\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/media\/135339"}],"wp:attachment":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/media?parent=135338"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/categories?post=135338"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/tags?post=135338"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}