Skip to main content

Form

Within a Hydrogen app, React Server Components are used to fetch data and API routes are used to mutate data. The Form component provides a declarative way to send data to API routes and re-render server components. The component mimics the functionality of a native <form> element, but it provides an enhanced user experience with client-side JavaScript.

HTML form element

The Form component builds on the native HTML <form> element. The following is an example:

index.html
<form action="/login" method="post">
<label> Username <input type="text" name="username" /> </label>
<label> Password <input type="password" name="password" /> </label>
<button type="submit">Submit</button>
</form>

This example HTML doesn't run any JavaScript. When Submit is clicked, the browser sends a POST request to /login with each form field encoded. The browser also reloads the entire page to display the server's response. Learn more about native HTML forms.

Hydrogen Form component

Hydrogen's Form component mimics the functionality of a native <form> element, while providing an enhanced experience with client-side JavaScript.

Native HTML forms work without JavaScript. However, JavaScript can provide the following improvements:

  • Performance: JavaScript prevents the entire page from reloading to display responses from the server.
  • UX: JavaScript provides client-side validation and feedback. Client-side validation is quicker than making a round trip to the server, and feedback helps the user know when the form is in the process of submitting.

Client validation and feedback

The best user experience has client-side validation and gives user feedback while the form is submitting. This requires a client component.

Example code

The following example rewrites the example form element by substituting the native HTML with a Form component that's imported from Hydrogen.

Because the Form is within a client component, children can be a render prop. This enables you to give users feedback while the form is submitting.

LoginForm.client.jsx
import {Form} from '@shopify/hydrogen/experimental';

export default function Login() {
const url = useUrl();
return (
<Form action="/login" method="post">
{({loading}) => (
<>
<label>
Username <input type="text" name="username" />
</label>
<label>
Password <input type="password" name="password" />
</label>
{url.searchParams.get('error') ? (
<h2 className="text-red-700">Invalid username or password</h2>
) : null}
<button type="submit" disabled={loading}>
Submit
</button>
</>
)}
</Form>
);
}

Hidden fields

You can use the Form component for any mutation that doesn't include a text field.

For example, the following uses a Form component for adding items an item to a cart:

Product.server.jsx
import {Form} from '@shopify/hydrogen/experimental';

export default function Product({product}) {
return (
<ProductDetails>
<Form action="/addToCart" method="post">
<input type="hidden" name="productId" value={product.id} />
<button type="submit">Add to cart</button>
</Form>
</ProductDetails>
);
}

The hidden input field for the productId is sent to the server when the Add to cart button is clicked. The API route at /addToCart can contain all the logic to add the product to the cart and re-render the page. The button is actionable before the page fully loads and the JavaScript is hydrated.

Form in server components

We recommend that you use Form in client components for the best user experience. However, you can use Form in server components if required. The following is an example:

login.server.jsx
import {Form} from '@shopify/hydrogen/experimental';

export default function Login() {
return (
<Form action="/login" method="post">
<label>
Username <input type="text" name="username" />
</label>
<label>
Password <input type="password" name="password" />
</label>
<button type="submit">Submit</button>
</Form>
);
}

Form requires an API route

The action attribute must point to an API route. The following is an example implementation:

login.server.jsx
export async function api(request, {session}) {
// Access the form data
const data = await request.formData();
const username = data.get('username');
const password = data.get('password');

// Find the user
const userId = await getUser(username, password);

if (!userId) {
// We couldn't find the user.
// Re-render the login page with a login error displayed
await session.set('loginError', true);
return new Request('/login');
}

// Save the user to the session
await session.set('userId', userId);

// Forward the user to the account page
return new Request('/account');
}

Read data in the API route from the Form by using the FormData API. The API route must respond with a new Request(). This renders the server components for the given page. You can re-render the current page, or render an entirely different page in the app.

In the previous example, when the user is not found, the current page is re-rendered with an error set on the session. The following code updates the server component to render the login error:

login.server.jsx
import {Form, useFlashSession} from '@shopify/hydrogen/experimental';

export default function Login() {
// `useFlashSession` also clears the value after reading it. This way,
// if the user refreshes the page, the validation error goes away.
const loginError = useFlashSession('loginError');

return (
<Form action="/login" method="post">
<label>
Username <input type="text" name="username" />
</label>
<label>
Password <input type="password" name="password" />
</label>
{loginError ? (
<h2 className="text-red-700">Invalid username or password</h2>
) : null}
<button type="submit">Submit</button>
</Form>
);
}

The page initially loads without a session loginError value. If the login mutation fails, then the server components re-render with the loginError session value and display a message to the user. Because the component uses useFlashSession instead of useSession, the value is subsequently cleared. If the user refreshes the page, then the validation error goes away.

Props

The Form component shares the same props that are available to the native <form> element with the following additions:

NameTypeDescription
actionstringThe path to the API route that the form submits to. The API route must respond with a new Request().
encType?stringThe MIME type of the form submission. multipart/form-data is not yet supported.
onSubmit?FunctionA callback to intercept a submission event. The form doesn't submit if event.preventDefault() is called.
children?ReactNode or ({loading, error}) => ReactNodeEither pass any ReactNode, or a function that returns a ReactNode. That function will receive loading and error parameters. loading is true while forms are being submitted. error is populated when there's an error communicating with the server.

Component type

The Form component is a client component, so it renders on the client. For more information about component types, refer to React Server Components.