Frequently Asked Questions

Getting Started & Implementation

How do I programmatically create forms and capture submissions using Next.js and Hygraph?

You can programmatically create forms and capture submissions by defining a content model in Hygraph, creating the necessary models (Page, Form, FormInput, FormTextarea, FormSelect, FormCheckbox, FormOption), and then using Next.js to dynamically build pages and forms. Submissions are captured using the Hygraph Mutations API, allowing you to store form data in a Submission model. The full tutorial is available on the Hygraph blog.

What are the main steps to set up dynamic forms with Hygraph and Next.js?

The main steps include: 1) Defining a solid content model in Hygraph, 2) Creating models and their associations, 3) Adding content via the Content Editor, 4) Querying pages, forms, and fields with GraphQL, 5) Configuring public API access, 6) Setting up a Next.js project, 7) Building dynamic pages and form components, 8) Managing form state and submissions, 9) Submitting data to Hygraph using GraphQL mutations, and 10) Deploying your site (e.g., to Vercel). Each step is detailed in the official tutorial.

How do I configure public API access in Hygraph for querying forms and pages?

To enable public API access for querying data, go to your project Settings in Hygraph, open the API Access page, enable 'Content from stage Published' under Public API permissions, and save your changes. This allows you to use the API Playground and query published content for building pages and forms. For more details, see the tutorial section.

How do I deploy my Next.js site with Hygraph forms to Vercel?

To deploy your Next.js site to Vercel, install the Vercel CLI using npm i -g vercel (or yarn global add vercel), then run vercel in your project directory. Follow the prompts to complete deployment. Once deployed, you can access your forms at the appropriate URL (e.g., /contact). See the deployment section for more details.

What is the recommended way to manage form state and submissions in a Next.js app using Hygraph?

The recommended approach is to use the react-hook-form library to manage form state and handle submissions. This allows for real-world form handling, including validation and error management. The submitted data can then be sent to a Next.js API route, which forwards the data to Hygraph using the Mutations API. See the tutorial section for implementation details.

How do I securely submit form data to Hygraph without exposing my mutation token?

To securely submit form data, create a Next.js API route (e.g., /api/submit) that receives form submissions and forwards them to Hygraph using the Mutations API. Store your mutation token in environment variables and never expose it to the client. This approach ensures your token remains secure. See the tutorial section for code examples.

What models and fields should I create in Hygraph to support dynamic forms?

You should create the following models: Page, Form, FormInput, FormTextarea, FormSelect, FormCheckbox, FormOption, and Submission. Each model has specific fields (e.g., name, label, placeholder, required, type, options) to support various form field types and submissions. The Submission model stores form data as JSON and references the Form. See the content model section for details.

How do I use GraphQL Union Types in Hygraph for form fields?

Hygraph supports GraphQL Union Types, allowing you to define models for each form field type (FormInput, FormTextarea, FormSelect, FormCheckbox) and associate them with the Form model as a 'has many' field. This enables flexible querying and rendering of different field types in your forms. See the tutorial for schema examples.

How can I reorder form fields in Hygraph?

You can reorder form fields using the Hygraph Content Editor by simply dragging each field row into the desired order. This allows you to control the display order of fields on your forms without changing the underlying schema. See the reordering section for more details.

How do I connect form submissions to a specific form in Hygraph?

When creating the Submission model, add a reference field to the Form model. In your mutation, connect the submission to the form using the form's ID. This ensures each submission is associated with the correct form. See the mutation example for implementation details.

Can I use webhooks with Hygraph to process form submissions?

Yes, you can use Hygraph webhooks to listen for new submissions and forward data to external services like Zapier, Slack, or email. This enables automation and integration with other tools without writing additional backend code. Learn more in the webhooks blog post.

What are the required fields for a basic contact form in Hygraph?

A basic contact form typically includes: Name (FormInput, type TEXT, required), Email (FormInput, type EMAIL, required), Tel (FormInput, type TEL, optional), Message (FormTextarea, required), Terms agreement (FormCheckbox, required), and an optional Select field for preferences. Each field is modeled in Hygraph and can be customized as needed. See the example section for details.

How do I handle form validation in a Next.js app using Hygraph?

Form validation can be handled using react-hook-form in your Next.js app. You can specify required fields and validation rules in your form components, and errors are managed automatically by the library. This ensures users provide the necessary information before submission. See the form state section for more details.

What is the best way to structure form field components in a Next.js project using Hygraph?

The best practice is to create separate components for each field type (FormInput, FormTextarea, FormSelect, FormCheckbox) and use a mapping approach to render the correct component based on the field's __typename. This allows for flexible and maintainable code. See the component structure section for examples.

How do I use environment variables to manage API endpoints and tokens in my Next.js project?

Store your Hygraph API endpoint and mutation token in a .env file as environment variables (e.g., HYGRAPH_ENDPOINT, HYGRAPH_MUTATION_TOKEN). Access these variables in your code using process.env. This keeps sensitive information secure and out of your source code. See the environment setup section for details.

How can I test my forms and submissions locally before deploying?

You can test your forms locally by running your Next.js development server (yarn dev or npm run dev) and accessing the relevant page (e.g., http://localhost:3000/contact). Fill out and submit the form, then check the Hygraph Content Editor for new submissions. See the testing section for more information.

What are the benefits of using Hygraph for dynamic forms and submissions?

Hygraph offers a flexible, GraphQL-native CMS that allows you to model complex forms, manage content easily, and capture submissions securely. Benefits include dynamic content modeling, robust API access, integration with modern frameworks like Next.js, and secure data handling. For more on Hygraph's capabilities, see the features page.

Can I integrate Hygraph forms with other tools or platforms?

Yes, Hygraph supports integrations with various Digital Asset Management (DAM) systems (e.g., Aprimo, AWS S3, Bynder, Cloudinary, Imgix, Mux, Scaleflex Filerobot), as well as custom integrations via SDKs and APIs. You can also use webhooks to connect with external services. See the integrations documentation for more information.

Features & Capabilities

What APIs does Hygraph provide for developers?

Hygraph offers multiple APIs: Content API (read & write), High Performance Content API (low latency, high throughput), MCP Server API (for AI assistants), Asset Upload API, and Management API. Each API is designed for specific use cases, from content delivery to project management. For details, visit the API Reference Documentation.

Does Hygraph support high-performance content delivery?

Yes, Hygraph provides high-performance endpoints designed for low latency and high read-throughput content delivery. These endpoints are actively measured and optimized, as detailed in the performance improvements blog post and the GraphQL Survey 2024.

What technical documentation is available for Hygraph?

Hygraph offers comprehensive technical documentation, including API references, schema components, references, webhooks, and AI integrations. Access all resources at the Hygraph Documentation portal.

What are the key capabilities and benefits of Hygraph?

Key capabilities include GraphQL-native architecture, content federation, scalability, enterprise-grade security, user-friendly tools, Smart Edge Cache, localization, asset management, cost efficiency, and accelerated speed-to-market. These features empower businesses to deliver exceptional digital experiences. For more, see the features page.

What integrations does Hygraph offer?

Hygraph integrates with DAM systems (Aprimo, AWS S3, Bynder, Cloudinary, Imgix, Mux, Scaleflex Filerobot), Adminix, Plasmic, and supports custom integrations via SDKs and APIs. Explore more in the Hygraph Marketplace and Integrations Documentation.

Pricing & Plans

What does the Hygraph Hobby plan cost and what does it include?

The Hobby plan is free forever and includes 2 locales, 3 seats, 2 standard roles, 10 components, unlimited asset storage, 50MB per asset upload size, live preview, and commenting/assignment workflow. Ideal for individuals and personal projects. Sign up here.

What features are included in the Hygraph Growth plan?

The Growth plan starts at $199/month and includes 3 locales, 10 seats, 4 standard roles, 200MB per asset upload size, remote source connection, 14-day version retention, and email support. Designed for small businesses. Get started here.

What does the Hygraph Enterprise plan offer?

The Enterprise plan offers custom pricing and includes custom limits on users, roles, entries, locales, API calls, components, remote sources, version retention (1 year), scheduled publishing, dedicated infrastructure, global CDN, 24/7 monitoring, security/governance controls, SSO, multitenancy, instant backup recovery, custom workflows, dedicated support, and custom SLAs. Try for 30 days or request a demo.

Security & Compliance

What security and compliance certifications does Hygraph have?

Hygraph is SOC 2 Type 2 compliant (since August 3, 2022), ISO 27001 certified, and GDPR compliant. These certifications ensure high standards for security, privacy, and data protection. Learn more on the Secure features page.

How does Hygraph ensure data security and privacy?

Hygraph uses enterprise-grade features such as granular permissions, audit logs, SSO integrations, encryption at rest and in transit, regular backups, and dedicated hosting options. Data centers are ISO 27001 certified. Customers can report security incidents through a dedicated process. See the security page for details.

Use Cases & Customer Success

Who can benefit from using Hygraph for forms and submissions?

Developers, product managers, content creators, marketers, and solutions architects in enterprises, agencies, eCommerce, media, technology, and global brands can benefit from Hygraph's flexible content modeling and integration capabilities. See case studies for industry examples.

What industries are represented in Hygraph's case studies?

Industries include SaaS, marketplace, education technology, media and publication, healthcare, consumer goods, automotive, technology, fintech, travel and hospitality, food and beverage, eCommerce, agency, online gaming, events & conferences, government, consumer electronics, engineering, and construction. See the case studies page for more.

Can you share specific customer success stories using Hygraph?

Yes. For example, Samsung built a scalable, API-first application; Komax achieved 3x faster time to market; AutoWeb saw a 20% increase in website monetization; Voi scaled multilingual content across 12 countries. Read more on the case studies page.

What business impact can customers expect from using Hygraph?

Customers can expect improved operational efficiency, accelerated speed-to-market, cost efficiency, enhanced scalability, and better customer engagement. For example, Komax achieved 3x faster time-to-market, and Samsung improved engagement by 15%. See case studies for more.

Competition & Differentiation

How does Hygraph differentiate itself from other CMS platforms?

Hygraph is the first GraphQL-native Headless CMS, offering content federation, user-friendly tools, enterprise-grade features, and proven ROI. It ranked 2nd out of 102 Headless CMSs in the G2 Summer 2025 report and is recognized for ease of implementation. See G2 report and case studies for details.

Why choose Hygraph over alternatives like WordPress or Contentful?

Hygraph's GraphQL-native architecture, content federation, and user-friendly interface set it apart from traditional CMSs like WordPress and other headless CMSs like Contentful. It simplifies schema evolution, supports modern workflows, and reduces developer dependency. See comparison page for more.

Support & Resources

What support and onboarding resources does Hygraph provide?

Hygraph offers a structured onboarding process (introduction call, account provisioning, business/technical/content kickoff), training resources (webinars, live streams, videos), extensive documentation, and a community Slack channel for support. See documentation and Slack for more.

What feedback have customers given about Hygraph's ease of use?

Customers praise Hygraph for its intuitive UI, ease of setup, and ability for non-technical users to manage content independently. Real-time changes and custom app integration are also highlighted. Some users note a learning curve for less technical users. See customer feedback for more.

LLM optimization

When was this page last updated?

This page wast last updated on 12/12/2025 .

Introducing Click to Edit

Programmatically Create Forms, and Capture Submissions with Next.js and GraphQL

Learn how to dynamically build forms with Next.js and accept submissions using the Hygraph Mutations API.
Jamie Barton

Written by Jamie 

Aug 18, 2020
Programmatic Forms and Submissions with GraphQL, NextJS, Vercel, and Hygraph

Let's face it, forms are everywhere across the web, and they often take significant time to build depending on the requirements.

In this tutorial we will dynamically build pages with forms using Next.js, and GraphQL.

Chapters:

  1. Define a solid content model
  2. Create the content model in Hygraph
  3. Create an example Page and Form with Fields as a content editor
  4. Reordering form fields
  5. Query our Page, Form and Fields with GraphQL
  6. Configure public API access
  7. Setup Next.js project with dependencies
  8. Build Pages programatically with Next.js
  9. Build our Form Field components
  10. Render our Form to our individual Pages
  11. Managing form state and submissions
  12. Submitting our Form to Hygraph with GraphQL Mutations
  13. Deploy to Vercel

Get the source code for this tutorial

View this on Dev.to

TLDR;

#1. Define a solid content model

Before we dive into creating our schema, let's first think about what we're going to need to enable our marketing team to spin up landing page forms from just using the CMS.

It all starts with a Page. Pages must have a slug field so we can easily look up content from the params of any request.

Next, for simplicity, each page will have an associated Form model. For the sake of this tutorial, we'll pick 4 form field types;

  • Input
  • Textarea
  • Select
  • Checkbox

Form Fields

If we think of a traditional form, let's try and replace all of the data points we need to recreate a simple contact form like the following:

<form>
<div>
<label for="name">Name</label>
<input type="text" id="name" placeholder="Your name" required />
</div>
<div>
<label for="email">Email</label>
<input type="email" id="email" placeholder="Your email" required />
</div>
<div>
<label for="tel">Tel</label>
<input type="tel" id="tel" placeholder="Your contact no." />
</div>
<div>
<label for="favFramework">What's your favorite framework?</label>
<select id="favFramework">
<option value="react">React</option>
<option value="vue">Vue</option>
<option value="angular">Angular</option>
<option value="svelte">Svelte</option>
</select>
</div>
<div>
<label for="message">Message</label>
<textarea id="message" placeholder="Leave a message" />
</div>
<div>
<label for="terms">
<input id="terms" type="checkbox" />
I agree to the terms and privacy policy.
</label>
</div>
<div>
<button type="submit">Submit</button>
</div>
</form>

In the above form, we have some <input />'s that are required, some which are of type email, tel and text, while the <select /> has no placeholder or is required.

Hygraph has support for GraphQL Union Types. This means we can define models for each of our form field types, and associate them to our Form model as one "has many" field.

Our schema will end up looking a little something like the following...

Models

Page

  • Title, String, Single line text, Required, and used as a Title
  • Slug, String, Slug, Required - Specify {title} as the template
  • Form, Reference to Form

You can use the Slug field type that automatically generates a slug based on your title. You'll need to set the template. to{title} to make this work. You can always override slugs.

Form

  • Page, Reference, Accepts multiple Page values
  • Fields, Reference, Accepts multiple FormInput, FormTextarea, FormSelect and FormCheckbox values

FormInput

  • Name, String, Single line text, and used as a Title
  • Type, Enum, FormInputType dropdown
  • Label, String, Single line text
  • Placeholder, Single line text
  • Required, Boolean
  • Form, Reference to Form

FormTextarea

  • Name, String, Single line text, and used as a Title
  • Label, String Single line text
  • Placeholder, String, Single line text
  • Required, Boolean
  • Form, Reference to Form

FormSelect

  • Name, String, Single line text, and used as a Title
  • Label, String, Single line text
  • Required, Boolean
  • Choices, Reference, Accepts multiple FormOption values
  • Form, Reference to Form

FormOption

  • Value, String, Single line text, Required, and used as a Title
  • Option, String, Single line text
  • FormSelect, Reference, Belongs to FormSelect

FormCheckbox

  • Name, String, Single line text, and used as a Title

  • Label, String, Single line text, Required
  • Required, Boolean
  • Form, Reference to Form

Enumerations

FormInputType values

  • EMAIL
  • TEXT
  • TEL

🖐 You could add more, but it's not required for this tutorial.

#2. Create the models in Hygraph

Now we have an idea of how our content model looks like. Let's create the models and their associations with eachother inside Hygraph.

  1. You'll need an account to continue. Sign up or head to the Dashboard.

  2. Once logged in, head to the Schema editor by selecting Schema from the side.
  3. Click + Add in the sidebar above default system Asset model.
  4. Go ahead and create the 7 models above. Don't worry about creating relations just yet, you can do them all at once after creating the other fields.

#3. Create an example Page and Form with Fields as a content editor

So that we are able to query, and build our forms, we're going to need some content inside our models.

  1. Inside the Dashboard, head to the Content editor by selecting Content from the side.
  2. Select the Page model and click + Create New from the top right.
  3. Give your page a title and slug. I'll call use Contact Us, and contact, respectively.
  4. Now underneath Form, click Create and add a new form.
  5. Inside the inline Form content editor, click on Create and add a new document.
  6. From the dropdown, select FormInput.
  7. Inside the inline FormInput content editor, enter a name, type , label and placeholder for your form field. I'll add the values Name, TEXT, Your name, Name and set required to true.
  8. Now click Save and publish.

Repeat steps 5-8 to add additional fields.

🖐 To follow along with the rest of this tutorial, I will be using the following values for my fields...

 3 x FormInput's

  • Name

    • Name: name
    • Type: TEXT
    • Label: Name
    • Placeholder: Your name
    • Required: true
  • Email
    • Name: email
    • Type: EMAIL
    • Label: Email
    • Placeholder: Your email
    • Required: true
  • Tel
    • Name: tel
    • Type: TEL
    • Label: Tel
    • Placeholder: Your contact no.
    • Required: false

1 x FormTextarea

  • Message
    • Name: message
    • Label: Message
    • Placeholder: Leave a message
    • Required: true

1 x FormCheckbox

  • Terms
    • Name: terms
    • Label: I agree to the terms and privacy policy.
    • Required: true

1 x FormSelect

The FormSelect is a little special because it also references another model FormSelect.

First, create your FormSelect document as usual, entering the following.

  • Favourite Framework
    • Name: favFramework
    • Label: What's your favorite frontend framework?
    • Required: false
  • Next below Options, click on Create and add a new formOption.

Now for each of our choices below, repeat the steps to "Create and add a new formOption", and provide the value/option for each:

  1. react/React
  2. vue/Vue
  3. angular/Angular
  4. svelte/Svelte

Finally, click Save and publish on this and close each of the inline editors, making sure to publish any unsaved changes along the way.

#4. Reordering form fields

Now we have created our fields, we can now reorder them using the content editor. This may be useful if you decide to add or remove some fields later, you can order the fields exactly the way you want them to appear.

✨ Simply drag each of the Field rows into the order you want. ✨

Reordering Hygraph Fields

#5. Query our Page, Form and Fields with GraphQL

We have two pages, with two separate forms:

  • Contact Form
  • Request a Demo

Let's start by querying for all pages and their forms using the API Playground available from the sidebar within your project Dashboard.

Query pages, form and field __typename

{
pages {
title
slug
form {
id
fields {
__typename
}
}
}
}

Union Type Query

As we're using Union Types for our form fields, we must use the ... on TypeName notation to query each of our models.

Let's go ahead and query on all of our models we created earlier.

{
pages {
title
slug
form {
id
fields {
__typename
... on FormInput {
name
type
inputLabel: label
placeholder
required
}
... on FormTextarea {
name
textareaLabel: label
placeholder
required
}
... on FormCheckbox {
name
checkboxLabel: label
required
}
... on FormSelect {
name
selectLabel: label
options {
value
option
}
required
}
}
}
}
}

The response should look a little something like the following:

{
"data": {
"pages": [
{
"title": "Contact us",
"slug": "contact",
"form": {
"id": "ckb9j9y3k004i0149ypzxop4r",
"fields": [
{
"__typename": "FormInput",
"name": "Name",
"type": "TEXT",
"inputLabel": "Name",
"placeholder": "Your name",
"required": true
},
{
"__typename": "FormInput",
"name": "Email",
"type": "EMAIL",
"inputLabel": "Email address",
"placeholder": "you@example.com",
"required": true
},
{
"__typename": "FormInput",
"name": "Tel",
"type": "TEL",
"inputLabel": "Phone no.",
"placeholder": "Your phone number",
"required": false
},
{
"__typename": "FormSelect",
"name": "favFramework",
"selectLabel": "What's your favorite frontend framework?",
"options": [
{
"value": "React",
"option": "React"
},
{
"value": "Vue",
"option": "Vue"
},
{
"value": "Angular",
"option": "Angular"
},
{
"value": "Svelte",
"option": "Svelte"
}
],
"required": false
},
{
"__typename": "FormTextarea",
"name": "Message",
"textareaLabel": "Message",
"placeholder": "How can we help?",
"required": true
},
{
"__typename": "FormCheckbox",
"name": "Terms",
"checkboxLabel": "I agree to the terms and privacy policy.",
"required": true
}
]
}
}
]
}
}

#6. Configure public API access

Hygraph has a flexible permissions system, which includes enabling certain user groups to do actions, and most importantly restrict who can query what data.

For the purposes of querying data to build our pages and forms, we'll enable public API queries.

To do this, go to your project Settings.

  1. Open the API Access page
  2. Enable Content from stage Published under Public API permissions
  3. Save

That's it! You can test this works using the API Playground and selecting Environment: master Public from the dropdown in the section above your query/result.

API Playground Environment dropdown

🖐 Make sure to copy your API Endpoint to the clipboard. We'll need it in step 8.

#7. Setup Next.js project with dependencies

Now we have our schema, and content, let's begin creating a new Next.js project with all of the dependencies we'll need to build our pages and forms.

Inside the Terminal, run the following to create a new Next.js project.

npm init next-app dynamic-hygraph-forms

When prompted, select Default starter app from the template choices.

cd dynamic-hygraph-forms

This template will scaffold a rough folder structure following Next.js best practices.

Next, we'll install graphql-request for making GraphQL queries via fetch.

yarn add -E graphql-request # or npm install ...

Now, if you run the project, you should see the default Next.js welcome page at http://localhost:3000.

yarn dev # or npm run dev

#8. Build Pages programatically with Next.js

This comes in two significant parts. First we create the routes (or "paths") and then query for the data for each page with those path params.

8.1 Create programmatic page routes

First up is to add some code to our Next.js application that will automatically generate pages for us. For this we will be exporting the getStaticPaths function from a new file called [slug].js in our pages directory.

touch pages/[slug].js

Having a filename with square brackets may look like a typo, but rest assured this is a Next.js convention.

Inside pages/[slug].js add the following code to get going:

export default function Index(props) {
return (
<pre>{JSON.stringify(props, null, 2)}</pre>
)
}

If you're familiar with React already, you'll notice we are destructuring props from the Index function. We'll be updating this later to destructure our individual page data, but for now, we'll show the props data on each of our pages.

Inside pages/[slug].js, let's import graphql-request and initialize a new GraphQLClient client.

🖐 You'll need your API Endpoint from Step 6 to continue.

import { GraphQLClient } from "graphql-request";
const hygraph = new GraphQLClient("YOUR_HYGRAPH_ENDOINT_FROM_STEP_6");

Now the hygraph instance, we can use the request function to send queries (with variables) to Hygraph.

Let's start by querying for all pages, and get their slugs, inside a new exported function called getStaticPaths.

export async function getStaticPaths() {
const { pages } = await hygraph.request(`{
pages {
slug
}
}`)
return {
paths: pages.map(({ slug }) => ({ params: { slug } })),
fallback: false
}
}

There's quite a bit going on above, so let's break it down...

const { pages } = await hygraph.request(`{
pages {
slug
}
}`)

Here we are making a query and destructuring the response pages from the request. This will be similar to the results we got back in step 5.

return {
paths: pages.map(({ slug }) => ({ params: { slug } })),
fallback: false
}

Finally inside getStaticPaths we are returning paths for our pages, and a fallback. These build the dynamic paths inside the root pages directory, and each of the slugs will become pages/[slug].js.

The fallback is false in this example, but you can read more about using that here.

🖐 getStaticPaths alone does nothing, we need to next query data for each of the pages.

8.2 Query page data

Now we have programmatic paths being generated for our pages, it's now time to query the same data we did in step 5, but this time, send that data to our page.

Inside pages/[slug].js, export the following function:

export async function getStaticProps({ params: variables }) {
const { page } = await hygraph.request(
`query page($slug: String!) {
page(where: {slug: $slug}) {
title
slug
form {
fields {
__typename
... on FormInput {
name
type
inputLabel: label
placeholder
required
}
... on FormTextarea {
name
textareaLabel: label
placeholder
required
}
... on FormCheckbox {
name
checkboxLabel: label
required
}
... on FormSelect {
name
selectLabel: label
options {
value
option
}
required
}
}
}
}
}
`,
variables
);
return {
props: {
page,
},
};
}

Now just like before, there's a lot going on, so let's break it down...

export async function getStaticProps({ params: variables }) {
// ...
}

Here we are destructuring the params object from the request sent to our page. The params here will be what we sent in getStaticPaths, so we'd expect to see slug here.

🖐 As well as destructuring, we are also renaming (or reassigning) the variable params to variables.

const { page } = await hygraph.request(`...`, variables);
return {
props: {
page,
},
};

Next we're sending the same query we did in step 5, but this time we've given the query a name page which expects the String variable slug.

Once we send on our renamed params as variables, we return an object with our page inside of props.

Now all that's left to do is run our Next development server and see our response JSON on the page!

yarn dev # or npm run dev

Now you should see at http://localhost:3000/contact the data from Hygraph for our Page.

Next development server

#9. Build our Form Field components

We are now ready to dynamically build our form using the data from Hygraph.

The __typename value will come in handy when rendering our form, as this will decide which component gets renderered.

Inside a new directory components, add a Form.js file.

mkdir components
touch components/Form.js

In this this file, we will create the structure of our basic form, and map through each of our fields to return the appropreciate field.

Add the following code to components/Form.js

import * as Fields from "./FormFields";
export default function Form({ fields }) {
if (!fields) return null;
return (
<form>
{fields.map(({ __typename, ...field }, index) => {
const Field = Fields[__typename];
if (!Field) return null;
return <Field key={index} {...field} />;
})}
<button type="submit">Submit</button>
</form>
);
}

Once you have this component setup, now create the file components/FormFields/index.js and add the following:

export { default as FormCheckbox } from "./FormCheckbox";
export { default as FormInput } from "./FormInput";
export { default as FormSelect } from "./FormSelect";
export { default as FormTextarea } from "./FormTextarea";

All we're doing in this file is importing each of our different form fields and exporting them.

The reason we do this is that when we import using import * as Fields, we can grab any of the named exports by doing Fields['FormCheckbox'], or Fields['FormInput'] like you see in components/Form.js.

Now that that we are importing these new fields, we next need to create each of them!

For each of the imports above, create new files inside components/FormFields for:

  • FormCheckbox.js
  • FormInput.js
  • FormSelect.js
  • FormTextarea.js

Once these are created, let's export each of the components as default, and write a minimum amount of code to make them work.

The code in the below files isn't too important. What's key about this tutorial is how we can very easily construct forms, and in fact any component or layout, using just data from the CMS. Magic! ✨

FormCheckbox.js

export default function FormCheckbox({ checkboxLabel, ...rest }) {
const { name } = rest;
return (
<div>
<label htmlFor={name}>
<input id={name} type="checkbox" {...rest} />
{checkboxLabel || name}
</label>
</div>
);
}

FormInput.js

Since this component acts as a generic <input />, we will need to lowercase the type enumeration to pass to the input.

export default function FormInput({ inputLabel, type: enumType, ...rest }) {
const { name } = rest;
const type = enumType.toLowerCase();
return (
<div>
{inputLabel && <label htmlFor={name}>{inputLabel || name}</label>}
<input id={name} type={type} {...rest} />
</div>
);
}

FormSelect.js

export default function FormSelect({ selectLabel, options, ...rest }) {
const { name } = rest;
if (!options) return null;
return (
<div>
<label htmlFor={name}>{selectLabel || name}</label>
<select id={name} {...rest}>
{options.map(({ option, ...opt }, index) => (
<option key={index} {...opt}>
{option}
</option>
))}
</select>
</div>
);
}

 FormTextarea.js

export default function FormTextarea({ textareaLabel, ...rest }) {
const { name } = rest;
return (
<div>
<label htmlFor={name}>{textareaLabel || name}</label>
<textarea id={name} {...rest} />
</div>
);
}

We're done on the form components, for now...!

#10. Render our Form to our individual Pages

Let's recap...

  • We have our form model and content coming from Hygraph
  • We have our form fields created
  • We have our form pages automatically created

Let's now render the form we created in step 9 to our page.

Inside pages/[slug].js, we'll need to import our Form component and return that inside of the default export.

Below your current import (graphql-request), import our Form component:

import Form from "../components/Form";

Lastly, update the default export to return the <Form />.

export default function Index({ page }) {
const { form } = page;
return <Form {...form} />;
}

Next run the Next.js development server:

yarn dev # or npm run dev

Once the server has started, head to http://localhost:3000/contact (or a slug you defined in the CMS) to see your form!

Form Components

I'll leave the design and UI aesthetics up to you!


As far as creating dynamic forms with React, Next.js and GraphQL goes, this is it! Next we'll move onto enhancing the form to be accept submissions.

#11. Managing form state and submissions

In this step we will install a library to handle our form state, and submissions, as well as create an onSubmit that'll we'll use in Step 12 to forward onto Hygraph.

Inside the terminal, let's install a new dependency:

yarn add -E react-hook-form # or npm install ...

Now it's not essential we use react-hook-form for managing our form, I wanted to provide a little closer to real world scenario than your typical setState example that are used in tutorials.

After we complete this tutorial, you should be in a position to return to each of your form fields, add some CSS, error handling, and more, made easy with react-hook-form!

Inside components/Form.js, add the following import to the top of the file:

import { useForm, FormContext } from "react-hook-form";

Then inside your Form function after you return null if there are no fields, add the following:

const { handleSubmit, ...methods } = useForm();
const onSubmit = (values) => console.log(values);

Finally, you'll need to wrap the current <form> with <FormContext {...methods}>, and add a onSubmit prop to the <form> that is onSubmit={handleSubmit(onSubmit)}.

Your final components/Form.js should look like this:

import { useForm, FormContext } from "react-hook-form";
import * as Fields from "./FormFields";
export default function Form({ fields }) {
if (!fields) return null;
const { handleSubmit, ...methods } = useForm();
const onSubmit = (values) => console.log(values);
return (
<FormContext {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map(({ __typename, ...field }, index) => {
const Field = Fields[__typename];
if (!Field) return null;
return <Field key={index} {...field} />;
})}
<button type="submit">Submit</button>
</form>
</FormContext>
);
}

Now all that's happening here is we're initializing a new react-hook-form instance, and adding a FormContext provider around our form + fields.

Next we'll need to update each of our FormFields/*.js and register them with the react-hook-form context.

First update components/FormFields/FormInput.js to include the hook useFormContext from react-hook-form.

At the top of the file add the following import:

import { useFormContext } from 'react-hook-form'

Then inside the FormInput function, add the following before the return:

const { register } = useFormContext();

Now all that's left to do add register as a ref to our <input /> and pass in the required value.

<input
ref={register({ required: rest.required })}
id={name}
type={type}
{...rest}
/>

The final FormInput should look like:

import { useFormContext } from "react-hook-form";
export default function FormInput({ inputLabel, type: enumType, ...rest }) {
const { register } = useFormContext();
const { name } = rest;
const type = enumType.toLowerCase();
return (
<div>
{inputLabel && <label htmlFor={name}>{inputLabel || name}</label>}
<input
ref={register({ required: rest.required })}
id={name}
type={type}
{...rest}
/>
</div>
);
}

Great! Now let's do the same for the other 3 field components:

FormCheckbox.js

import { useFormContext } from "react-hook-form";
export default function FormCheckbox({ checkboxLabel, ...rest }) {
const { register } = useFormContext();
const { name } = rest;
return (
<div>
<label htmlFor={name}>
<input
ref={register({ required: rest.required })}
id={name}
type="checkbox"
{...rest}
/>
{checkboxLabel || name}
</label>
</div>
);
}

FormSelect.js

import { useFormContext } from "react-hook-form";
export default function FormSelect({ selectLabel, options, ...rest }) {
if (!options) return null;
const { register } = useFormContext();
const { name } = rest;
return (
<div>
<label htmlFor={name}>{selectLabel || name}</label>
<select ref={register({ required: rest.required })} id={name} {...rest}>
{options.map(({ option, ...opt }, index) => (
<option key={index} {...opt}>
{option}
</option>
))}
</select>
</div>
);
}

FormTextarea.js

import { useFormContext } from "react-hook-form";
export default function FormTextarea({ textareaLabel, ...rest }) {
const { register } = useFormContext();
const { name } = rest;
return (
<div>
<label>{textareaLabel || name}</label>
<textarea
ref={register({ required: rest.required })}
htmlFor={name}
id={name}
{...rest}
/>
</div>
);
}

🖐 Let's start the Next.js development server, and view the console when we submit the form!

yarn dev # or npm run dev

Once the server has started, head to http://localhost:3000/contact (or a slug you defined in the CMS) to see your form!

Open the browser developer tools console, and then fill out the form and click submit!

You should now see the form values submitted!

Developer Tools Console

#12. Submitting our Form to Hygraph with GraphQL Mutations

It's now time to take our form to the next level. We are going to update our Hygraph schema with a new Submission model that will be used to store submissions.

Inside the Hygraph Schema Editor, click + Add to create a new model.

  • Give the model a name of Submission,
  • Add a new JSON Editor field with the Display Name Form Data, and, API ID as formData,
  • Add a new Reference field with the Display Name/API ID Form/form , and select Form as the Model that can be referenced,
  • Configure the reverse field to Allow multiple values and set the default Display Name/API ID to (Submissions/submissions) respectively.

Things should look a little something like the following:

Submission model

And the Form model should now have a new field submisson:

Form Model

Since we want full control via the CMS what appears on our form, we'll just save all of that data inside formData JSON field.

🖐 Using something like webhooks would enable you to forward formData onto a service like Zapier, and do what you need to with the data, all without writing a single line of code! ✨

In order to use the Mutations API, we'll need to configure our API access to permit mutations and create a dedicated Permanent Auth Token. Don't enable Mutations for the Public API, as anybody will be able to query/mutate your data!

Head to Settings > API Access > Permanent Auth Tokens and create a token with the following setup:

Create Permanent Auth Token

Next, Copy the token to the clipboard once created.

Inside of the root of your Next.js project, create the file .env and, add the following, replacing YOUR_TOKEN_HERE with your token:

HYGRAPH_MUTATION_TOKEN=YOUR_TOKEN_HERE

With this token added, let's also do some housekeeping. Replace the API Endpoint you created in/pages/[slug].js with a the .env variable HYGRAPH_ENDPOINT and assign the value inside .env:

// pages/[slug].js
// ...
const hygraph = new GraphQLClient(process.env.HYGRAPH_ENDPOINT);
// ...

Now before we can use the HYGRAPH_MUTATION_TOKEN, we'll need to update our components/Form/index.js to POST the values to a Next.js API route.

Inside the form, let's do a few things:

  • import useState from React,
  • Invoke useState inside your Form function,
  • Replace the onSubmit function,
  • Render error after the submit <button />
import { useState } from 'react'
// ...
export default function Form({ fields }) {
if (!fields) return null;
const [success, setSuccess] = useState(null);
const [error, setError] = useState(null);
// ...
const onSubmit = async (values) => {
try {
const response = await fetch("/api/submit", {
method: "POST",
body: JSON.stringify(values),
});
if (!response.ok)
throw new Error(`Something went wrong submitting the form.`);
setSuccess(true);
} catch (err) {
setError(err.message);
}
};
if (success) return <p>Form submitted. We'll be in touch!</p>;
return (
// ...
<button type="submit">Submit</button>
{error && <span>{error}</span>}}
)
}

Finally we'll create the API route /api/submit that forwards requests to Hygraph securely. We need to do this to prevent exposing our Mutation Token to the public.

One of the best ways to scaffold your mutation is to use the API Playground inside your Hygraph project. It contains all of the documentation and types associated with your project/models.

API Playground

If you've followed along so far, the following mutation is all we need to create + connect form submissions.

mutation createSubmission($formData: Json!, $formId: ID!) {
createSubmission(data: {formData: $formData, form: {connect: {id: $formId}}}) {
id
}
}

The createSubmission mutation takes in 2 arguments; formData and formId.

In the onSubmit function above, we're passing along values which will be our formData. All we need to do now is pass along the form ID!

We are already querying for the form id inside pages/[slug].js, so we can use this id passed down to the Form component.

Inside components/Form.js, destructure id when declaring the function:

export default function Form({ id, fields }) {
// ...
}

.... and then pass that id into the onSubmit body:

const response = await fetch("/api/submit", {
method: "POST",
body: JSON.stringify({ id, ...values }),
});

Then, inside the pages directory, create the directory/file api/submit.js, and add the following code:

import { GraphQLClient } from "graphql-request";
export default async ({ body }, res) => {
const { id, ...data } = JSON.parse(body);
const hygraph = new GraphQLClient(process.env.HYGRAPH_ENDPOINT, {
headers: {
authorization: `Bearer ${process.env.HYGRAPH_MUTATION_TOKEN}`,
},
});
try {
const { createSubmission } = await hygraph.request(`
mutation createSubmission($data: Json!, $id: ID!) {
createSubmission(data: {formData: $data, form: {connect: {id: $id}}}) {
id
}
}`,
{
data,
id,
}
);
res.status(201).json(createSubmission);
} catch ({ message }) {
res.status(400).json({ message });
}
};

That's it! ✨

Now go ahead and submit the form, open the content editor and navigate to the Submission content.

You should see your new entry!

Submission entries

You could use Hygraph webhooks to listen for new submissions, and using another API route forward that onto a service of your choice, such as email, Slack or Zapier.

#13. Deploy to Vercel

Now all that's left to do is deploy our Next.js site to Vercel. Next.js is buil, and managed by the Vercel team and the community.

To deploy to Vercel, you'll need to install the CLI.

npm i -g vercel # or yarn global add vercel

Once installed, all it takes to deploy is one command!

vercel # or vc

You'll next be asked to confirm whether you wish to deploy the current directory, and what the project is named, etc. The defaults should be enough to get you going! 😅

Once deployed, you'll get a URL to your site. Open the deployment URL and append /contact to see your form!

View this on Dev.to

Blog Author

Jamie Barton

Jamie Barton

Jamie is a software engineer turned developer advocate. Born and bred in North East England, he loves learning and teaching others through video and written tutorials. Jamie currently publishes Weekly GraphQL Screencasts.

Share with others

Sign up for our newsletter!

Be the first to know about releases and industry news and insights.