If you want to build a dynamic, modern website on HubSpot CMS using React, this comprehensive guide covers everything. We'll walk through the setup process and share real-world debugging tips from an actual job-portal project. By following along, you'll learn both best practices and how to fix common issues that crop up in production.
Before you begin, make sure you have the essentials installed:
npm install -g @hubspot/cli)【8†L53-L56】.Next, authenticate your HubSpot CLI with your portal. You can run:
hs accounts auth
Follow the prompts to paste your personal access key. (HubSpot’s docs recommend using hs accounts auth over the older hs auth command for new projects【11†L117-L121】.)
Now initialize a new HubSpot CMS React theme. The recommended approach is to use the HubSpot project boilerplate script:
npx @hubspot/create-cms-theme@latest
# Follow prompts to name your theme (e.g., "job-portal-theme")
cd job-portal-theme
npm install
npm run start
Running npm run start launches the local dev server (powered by hs-cms-dev-server)【16†L13-L17】. You can then preview your work at http://localhost:3000 and even proxy live HubSpot pages to test changes.
After creation, your project has a structure like:
projectName/
├── hsproject.json
└── src/
└── theme/
├── theme-hsmeta.json
└── my-theme/
├── assets/
├── components/
├── styles/
├── templates/
├── fields.json
├── package.json
└── theme.json
Here, components/ will hold your React modules (and "islands" for interactivity), styles/ is for CSS, and theme.json contains your theme metadata【8†L88-L97】【8†L118-L123】. This separation helps manage assets: e.g. static images go in assets/, and page templates in templates/【8†L118-L123】.
HubSpot CMS React modules use a specific structure. Each module typically has:
index.jsx that exports a named Component and fields.MyComponent.jsx) for rendering JSX.Fields file defining HubSpot editor fields.For example, let's create a simple Header module under components/modules/Header/.
import { Component as HeaderComponent } from './HeaderComponent';
import { fields } from './HeaderFields';
export { fields };
export function Component(props) {
return <HeaderComponent {...props} />;
}
import styles from '../../../styles/header.module.css';
export function Component({ fieldValues }) {
return (
<header className={styles.header}>
<h1>{fieldValues.title}</h1>
</header>
);
}
import { ModuleFields, TextField } from '@hubspot/cms-components/fields';
export const fields = (
<ModuleFields>
<TextField
name="title"
label="Header Title"
default="Job Portal"
/>
</ModuleFields>
);
Note how we use HubSpot’s @hubspot/cms-components/fields to define editable fields. As HubSpot docs explain, React modules use the same field types as HubL modules (with added TypeScript support)【3†L43-L50】. In the example above, the field title will appear in the editor sidebar.
If you see an error like:
Module does not have a named Component export
It means your module entry file isn’t exporting function Component by name. HubSpot requires a named export called Component. The fix is to ensure your code looks like:
export function Component() {
return <div>Hello</div>;
}
In our job portal project, we needed dynamic job listings. We stored data in Google Sheets and exposed it via an Apps Script API.
const SHEET_ID = "YOUR_SHEET_ID";
const SHEET_NAME = "Jobs";
function doGet() {
const sheet = SpreadsheetApp
.openById(SHEET_ID)
.getSheetByName(SHEET_NAME);
const data = sheet.getDataRange().getValues();
const headers = data.shift();
const result = data.map(row => {
let obj = {};
headers.forEach((h, i) => obj[h] = row[i]);
return obj;
});
return ContentService
.createTextOutput(JSON.stringify(result))
.setMimeType(ContentService.MimeType.JSON);
}
import { useEffect, useState } from 'react';
export default function JobList() {
const [jobs, setJobs] = useState([]);
useEffect(() => {
fetch('YOUR_API_URL')
.then(res => res.json())
.then(data => setJobs(data));
}, []);
return (
<div>
{jobs.map((job, i) => (
<div key={i}>
<h3>{job.title}</h3>
<p>{job.location}</p>
</div>
))}
</div>
);
}
This code will render job entries fetched from your Google Sheets API. (Make sure to deploy your Apps Script as “Anyone with link” so it’s publicly accessible.)
If jobs don’t appear or you see a CORS error, it often means the Apps Script wasn’t publicly deployed. To fix this, go to the Apps Script editor and:
YOUR_API_URL.
HubSpot’s CMS React uses the “islands” architecture【3†L39-L47】. You can embed client-side React components (islands) within your HubL template or module. Use them for search filters, pagination, or any interactivity.
For instance, a simple job filter island might look like:
export default function Filters({ jobs }) {
const [query, setQuery] = useState('');
const filtered = jobs.filter(job =>
job.title.toLowerCase().includes(query.toLowerCase())
);
return (
<>
<input placeholder="Search jobs..." onChange={e => setQuery(e.target.value)} />
{filtered.map((job, i) => (
<div key={i}>{job.title}</div>
))}
</>
);
}
When used in a template, this island receives data (like the jobs array) from server-rendered context or props, and then runs in the browser for interactivity.
If you see a warning about hydration failed or UI mismatch, it means your server-rendered markup doesn’t match the client-side markup. This often happens if you use browser-specific APIs too early. A quick fix is:
if (typeof window !== 'undefined') {
// safe to run browser-only code
}
This ensures the code only runs in the browser, preventing server-client inconsistencies.
We set up a base global.css under styles/ to normalize the look:
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: system-ui, sans-serif;
}
h1, h2, h3 {
margin: 0;
}
Using global styles prevents weird layout shifts between modules. We also create CSS module files (like header.module.css) for component-specific styles, imported into each component as shown above.
Occasionally, a module’s content won’t show up in HubSpot. Common culprits include:
import styles from '../../../styles/header.module.css';.Once everything is ready, upload your project to HubSpot using the CLI:
hs project upload
This command builds and deploys your theme to your HubSpot account. During upload, HubSpot runs build health checks to catch issues early【3†L104-L109】. After a successful build, your theme will be available in the Design Manager for creating pages.
Here’s how our job portal project evolved:
Throughout this process, we encountered and fixed errors like missing component exports, hydration mismatches, and import path issues. Each fix made the solution more robust and scaled to real-world needs.
components/, islands/, etc.) for maintainability【8†L118-L123】.npm run start and preview pages to catch issues early (thanks to the Express + Vite dev server【5†L92-L100】).useEffect or serverless functions for APIs.Building a CMS React theme on HubSpot combines the familiarity of React development with the power of HubSpot’s CMS. It offers true component reusability, server-side rendering for SEO, and smooth client-side interactivity【3†L39-L47】. Most importantly, solving real errors teaches you the platform’s quirks: from required component exports to handling hydration issues.
This guide reflects our hands-on experience. If you’re planning a similar project—be it a job portal, marketing site, or any HubSpot web app—this setup is a solid foundation. We can extend it for multi-step forms, APIs, and more, while adhering to HubSpot best practices【3†L54-L62】.
If this walkthrough was helpful and you have a HubSpot React project in mind, feel free to reach out. We’ve built full HubSpot CMS solutions (including dynamic modules, workflows, and optimized themes) and can help bring your project to life with minimal fuss.