CMS React modules are similar to traditional HubL modules, in that they have fields, can be edited in the content editor, and can be used in drag and drop areas. But the HTML for a React module is generated by a React component instead of HubL, and its fields can be generated with either JSX or JSON.
Below, learn more about building CMS React modules, including required directory structure, defining fields, and working with data.
CMS React modules must be located in the /components/modules
subdirectory of your CMS React project. The JavaScript file can either live within that directory or be contained within another directory, using the module name as the file/directory name.
/components/modules/ExampleModule/index.jsx
js-package/
└── components/
└── modules/
└── ExampleModule/
└── index.jsx
/components/modules/ExampleModule.jsx
xxxxxxxxxx
js-package/
└── components/
└── modules/
└── ExampleModule.jsx
The path will determine how you reference the module in HubL, as shown below.
// ExampleModule/index.jsx
{% module "ExampleModule"
path="@projects/your-project/js-package/components/modules/ExampleModule",
%}
// ExampleModule.jsx
{% module "ExampleModule"
path="@projects/your-project/js-package/components/modules/ExampleModule.jsx",
%}
Whichever path you choose, the file (i.e., either ExampleModule/index.jsx
or ExampleModule.jsx
) must contain the following named exports:
Component
: a React component to be rendered. It may contain islands.meta
: a JavaScript object, equivalent to themeta.json
file in HubL modules.fields
: a JSX tree using components from@hubspot/cms-components/fields
to define module fields or a traditional JavaScript fields object.
xxxxxxxxxx
// Directory Structure for module-as-directory
js-package/
└── components/
└── modules/
└── ExampleModule/
├── ExampleModuleFields.jsx
├── ExampleModuleComponent.jsx
├── ExampleModuleMeta.js
└── index.jsx
As an example, your index.jsx
or ExampleModule.jsx
file exports might look similar to the following:
xxxxxxxxxx
import { ModuleFields, TextField } from '@hubspot/cms-components/fields';
import footerStyles from '../../../styles/footer.module.css';
export function Component({ fieldValues }: any) {
return (
<footer className={footerStyles.footer}>
<p>{fieldValues.footerText}</p>
</footer>
);
}
export const fields = (
<ModuleFields>
<TextField label="Footer Text" name="footerText" default="Be Well." />
</ModuleFields>
);
export const meta = {
label: 'Footer Module',
};
Note that you may use re-exports, as shown in the example index.jsx
file below:
xxxxxxxxxx
/* Note: index.js re-exports
ExampleModuleFields.jsx,
ExampleModuleMeta.js,
and ExampleModuleComponent.jsx
as named exports
*/
export { default as Component } from './ExampleModuleComponent.jsx';
export { fields } from './ExampleModuleFields.jsx';
export { meta } from './ExampleModuleMeta.js';
Module fields can be expressed as a JSX tree using field components from @hubspot/cms-components/fields
. These are the same module fields that HubL modules use. TypeScript definitions are included so that you can benefit from autocomplete and validation when defining fields. You can find all available fields in the fields reference documentation.
You may express field definitions as an array of JavaScript objects identical to the traditional HubL module JSON structure, exporting the same way as fields
. However, using JSX syntax is recommended, as it comes with the benefits of improved readability, sharing field logic, and more.
By writing module fields using JSX, you'll benefit from improved readability, along with the ability to dynamically generate fields, share field logic between modules, and create custom abstractions around field definitions.
For example, below is a FullNameField
custom field component that abstracts out a group of two or three text fields:
xxxxxxxxxx
import {
ModuleFields,
TextField,
FieldGroup,
BooleanField
} from '@hubspot/cms-components/fields';
const FullNameField = ({ includeMiddleName = false }) => (
<FieldGroup
name="full_name"
label="Full Name"
>
<TextField
name="given_name"
label="Given Name"
default="HubSpot"
required={true}
/>
{includeMiddleName && (
<TextField
name="middle_name"
label="Middle Name"
default=""
/>
)}
<TextField
name="family_name"
label="Family Name"
default="Developer"
/>
</FieldGroup>
);
export const fields = (
<ModuleFields>
<TextField
name="example_field"
label="Example Text Field"
default="Placeholder text" />
<FieldGroup name="group_of_fields" label="Field Group">
<BooleanField
name="child_boolean_field"
label="Child Boolean Field"
default={true}
/>
<TextField
name="child_text_field"
label="Child Text Field"
default="Child Field"
/>
</FieldGroup>
{/*Using the custom field component alongside other fields*/}
<FullNameField includeMiddleName={false}>
</ModuleFields>
);
Note that the root component of the fields
export must be ModuleFields
. Additionally, the above code makes use of FieldGroup
, which is the component type that creates the field groups you would use in HubL modules.
In the FullNameField
React component for the module fields defined above, props will have the following shape:
xxxxxxxxxx
// FullNameField module field props
{
example_field: "Placeholder text",
group_of_fields: {
child_boolean_field: true,
child_text_field: "Child Field"
},
full_name: {
given_name: "HubSpot",
family_name: "Developer",
}
}
Note that the default was used to fill in the value field once it was passed. This is because module values are passed from the server, so if someone changes the value of a field in the page editor, the new value will be passed to your module. But in the above case where no page-level field value is set, the server passes the default value to your props.
In addition to ModuleFields
and FieldGroup
, another component type from @hubspot/cms-components/fields
is RepeatedFieldGroup
. It creates a field group repeater, and can be used as shown below:
xxxxxxxxxx
export const fields = (
<ModuleFields>
<RepeatedFieldGroup
name="default_todos"
label="Default Todos"
occurrence={{
min: 1,
max: 500,
default: 1,
}}
default={[
{
text: 'Todo Test 1a',
completed: false,
},
{
text: 'Todo Test 2',
completed: true,
},
]}
>
<TextField label="Todo title" name="text" default="Todo..." required />
<BooleanField label="Todo Completed" name="completed" default={false} />
</RepeatedFieldGroup>
</ModuleFields>
);
Field values are passed as props to the Component
export of the module. For example, the code below would use the field structure from the previous example inside your module's component:
xxxxxxxxxx
export const Component = ({ fieldValues }) => {
return (
<ul>
<li>{fieldValues.example_field}</li>
<li>{fieldValues.group_of_fields.child_text_field}</li>
</ul>
);
};
Like in HubL modules, you can bind a GraphQL data query to a React module to fetch data. The GraphQL integration currently supports querying data from HubDB and custom objects.
Adding a named query
export to a module will provide the query result to render in the component props as dataQueryResult
. You can import and re-export a .graphql
query file or a JavaScript expression that evaluates to a GraphQL query (e.g., with gql-query-builder).
xxxxxxxxxx
// index.js
import ModuleComponent from './ModuleComponent.js';
import ModuleFields from './ModuleFields.js';
import ModuleMeta from './ModuleMeta.js';
import myQuery from './myQuery.graphql';
// This component will receive the query result via `dataQueryResult`
export const Component = ModuleComponent;
export const meta = ModuleMeta;
export const fields = ModuleFields;
export const query = myQuery;
Accessing the data in ModuleComponent
is shown below:
xxxxxxxxxx
//ModuleComponent.jsx
export default function ModuleComponent(props) {
return (
<div>
<span>
{props.dataQueryResult.data.CRM.contact_collection.items[0].firstname}
</span>
<span>
{props.dataQueryResult.data.CRM.contact_collection.items[0].lastname}
</span>
</div>
);
}
Using GraphQL in this way will connect any module and subsequent downstream pages to updates to the query and upstream data. Updates to data sources referenced from the query will cause the page to prerender. Learn more about fetching data.
To explore your account's GraphQL schema and build queries, you can use the GraphiQL tool in your account. Learn more about testing and running queries using GraphiQL.
To automatically attach and pass through HubL context variables to your React modules, you can use the hublDataTemplate
API.
First, export a string from your module via hublDataTemplate
. In that string, set the hublData
variable.
xxxxxxxxxx
export const hublDataTemplate = `{% set hublData = "Hello from HubL!" %}`;
The template will accept any valid HubL, including filters and functions and module field references. The data will come through on your React module as a top level prop of hublData
.
xxxxxxxxxx
import ModuleFields from './ModuleFields.js';
import ModuleMeta from './ModuleMeta.js';
export function Component(props) {
return (
<div>
<div>My total posts: {props.hublData.totalBlogPostCount}</div>
<a href={props.hublData.blogAllPostsUrl}>View all posts</a>
</div>
);
}
export const meta = ModuleMeta;
export const fields = ModuleFields;
export const hublDataTemplate = `
{% set blogId = module.blog_field %}
{% set hublData = {
"totalBlogPostCount": blog_total_post_count(blogId),
"blogAllPostsUrl": blog_all_posts_url(blogId)
}
%}
`;
While developing modules in cms-dev-server
, update the URL to include /preview/
between the port number of your locally running server and the /module/...
routes to have your local hublDataTemplate
string resolve.
For example: hslocal.net:3000/preview/module/...