Integrating Hubspot Forms with Gatsby

Posted: December 9th, 2019

Topics: Gatsby, React, Hubspot

Hubspot is a really awesome CRM tool that connects potential customers to any number of digital touchpoints in your websites & applications. Recently I was tasked with a Hubspot integration on a Gatsby build for a client.

For this project, we needed to hook up their forms feature to forms on the site. Typically when integrating with a third party form provider, there are a couple routes. You can either use embeddable code provided by the vendor, or if available, post data from your own UI elements to some API. Hubspot has both options, but their form embed code is just a javascript tag, and I'd like to load as few vendor scripts into the site as possible. Plus I prefer the control of writing my own components - so let's demo how to post form data to their API.

In this demo I'm just going to post straight to the Hubspot endpoint because no auth is required, but there actually is a web client on Github if you prefer that. I messed around with it for a few minutes but kept hitting some odd response codes. It'll probably work just fine for someone with a little more patience.

One dependency we'll be adding to the gatsby project is gatsby-plugin-hubspot -- which just loads Hubspot's basic tracking pixel elegantly. You'll want this for a couple reasons I'll outline in the snippet below. You'll also need to create the actual form you want to post data to in the Hubspot form builder.

Here's a stripped down demo of what my contact form component looks like. Hope this helps!

import React, { Fragment, useState } from 'react';
import { graphql, useStaticQuery } from 'gatsby';

// You'll need some polyfilled fetching library, 
// if thats your jam.  In addition, something to easily
// grab cookies from the browser. 

import fetch from 'isomorphic-unfetch';
import Cookies from 'js-cookie';


// This is what a headless CMS query could
// look like for the purposes of the demo. I 
// set this up so my client could change out forms 
// from an admin panel 

const contactQuery = graphql`
  {
    cmsContact {
      formSuccessMessage
      formErrorMessage
      formId
      portalId
    }
  }
`;

export default function ContactForm() {
  const [loading, setLoading] = useState(false);
  const [success, setSuccess] = useState(false);
  const [error, setError] = useState(false);
  const [firstname, setFirstname] = useState('');
  const [lastname, setLastname] = useState('');
  const [message, setMessage] = useState('');
  const {
    cmsContact: { formSuccessMessage, formErrorMessage, formId, portalId },
  } = useStaticQuery(contactQuery);

  const submitForm = e => {
    if (e) e.preventDefault();
		
		// In Gatsby, we're building our static site in a Node
		// environment, rather than a browser environment, so anything
		// browser related needs to be wrapped in some version of the
		// following isBrowser variable. 
		
		// What we're getting here is some contextual data to send 
		// along to Hubspot so it can organize and track forms as 
		// they relate to specific users in the CRM.  The Hubspot 
		// plugin we installed earlier provides this hutk value as a cookie.
		// pageName & pageUri should be pretty self explanatory. 
		
    const isBrowser = typeof window !== 'undefined';
    const hutk = isBrowser ? Cookies.get('hubspotutk') : null;
    const pageUri = isBrowser ? window.location.href : null;
    const pageName = isBrowser ? document.title : null;
    const postUrl = `${https://api.hsforms.com/submissions/v3/integration/submit}/${portalId}/${formId}`;

    setLoading(true);
		
		// This object shape is what's required in the Hubspot API 
		// documentation.  There's an additional legalConsentOptions object 
		// I've left out here, but would be important if you're working on a 
		// legitimate organization's site & they want to be safe from any sort 
		// of GDPR guff. 
		//
		// Read more here:
		// https://developers.hubspot.com/docs/methods/forms/submit_form_v3

    const body = {
      submittedAt: Date.now(),
      fields: [
        {
          name: 'firstname',
          value: firstname,
        },
        {
          name: 'lastname',
          value: lastname,
        },
        {
          name: 'message',
          value: message,
        },
      ],
      context: {
        hutk,
        pageUri,
        pageName,
      },
    };

		// These specific headers are required for whatever reason,
		// so don't forget to include them. 
		
    fetch(postUrl, {
      method: 'post',
      body: JSON.stringify(body),
      headers: new Headers({
        'Content-Type': 'application/json',
        Accept: 'application/json, application/xml, text/plain, text/html, *.*',
      }),
    })
      .then(res => res.json())
      .then(() => {
        setSuccess(true);
        setError(false);
        setLoading(false);
        setFirstname('');
        setLastname('');
        setMessage('');
      })
      .catch(err => {
        setSuccess(false);
        setError(err);
        setLoading(false);
      });
  };
	
	// Finally, the data-form-id and data-portal-id attributes 
	// don't actually do anything for us in terms of getting our 
	// data from A to B.  However, if these aren't included on the 
	// form element, Hubspot gets a little confused when labelling 
	// submissions in the dashboard, so be sure to include these attrs 
	// if you don't want your submission names looking like styled-component
	// class names a la "New Submission from Contact__Form__xydhf2_kskl"

  return (
    <Fragment>
      <FormContentWrapper>
        <Form
          data-form-id={formId}
          data-portal-id={portalId}
          disabled={loading}
          onSubmit={submitForm}
        >
          {success && (
            <Success>
              <p>{formSuccessMessage}</p>
            </Success>
          )}
          {!success && (
            <Fragment>
              <InputGroup>
                <Input>
                  <Label htmlFor="contact-first-name">First name *</Label>
                  <TextInput
                    id="contact-first-name"
                    type="text"
                    value={firstname}
                    onChange={e => setFirstname(e.target.value)}
                    required
                  />
                </Input>
                <Input>
                  <Label htmlFor="contact-last-name">Last name *</Label>
                  <TextInput
                    id="contact-last-name"
                    type="text"
                    value={lastname}
                    onChange={e => setLastname(e.target.value)}
                    required
                  />
                </Input>
              </InputGroup>
              <Input>
                <Label htmlFor="contact-message">Message *</Label>
                <TextArea
                  id="contact-message"
                  value={message}
                  onChange={e => setMessage(e.target.value)}
                  required
                />
              </Input>
              {error && (
                <ErrorBox>
                  <p>{formErrorMessage}</p>
                </ErrorBox>
              )}
              <Button type="submit">
                Send
              </Button>
            </Fragment>
          )}
        </Form>
      </FormContentWrapper>
    </Fragment>
  );
}