remix-hook-form

repository·main·Indexed 19 days ago

https://github.com/code-forge-io/remix-hook-form

A lightweight, zero-dependency utility wrapper around react-hook-form designed for React Router v7+ and Remix applications. It simplifies the synchronization of form state between client and server, providing seamless progressive enhancement and integration with server-side actions. Key features include the useRemixForm hook for client-side state management, getValidatedFormData for server-side validation (supporting Zod), and middleware for accessing form data in loaders and actions.

Tokens
7.9K
Snippets
24
Records
36
Agent score
65%

What's inside remix-hook-form

  1. Understand client-to-server value serialization

    main

    By default, remix-hook-form uses double stringification for complex values (arrays, objects, booleans, numbers) when sending data from client to server. This ensures that the server-side utilities can accurately parse the data back into its original types (e.g., a boolean true becomes the string '"true"' so it can be parsed back to true).

    Customizing Serialization

    1. Disable double stringification on the client: Set stringifyAllValues: false in the useRemixForm hook. This prevents strings from being double-wrapped.

    2. Disable parsing on the server: If you prefer to handle type conversion manually (e.g., using Zod's coerce), set the third argument of getValidatedFormData to true (preserveStringified: true). This tells the utility to return the raw stringified values instead of attempting to parse them back to their original types.

    // Client: Disable double stringification
    const { handleSubmit, formState, register } = useRemixForm({
      mode: "onSubmit",
      resolver,
      stringifyAllValues: false,
    });
    
    // Server: Use raw stringified values (requires manual coercion in schema)
    const { errors, data } = await getValidatedFormData<SchemaFormData>(
      request,
      resolver,
      true,
    );
  2. Deploy using Docker

    main

    The template provides optimized Dockerfiles for different package managers. Choose the Dockerfile that matches your package manager (npm, pnpm, or bun), build the image, and run the container on port 3000.

    # For npm
    docker build -t my-app .
    
    # For pnpm
    docker build -f Dockerfile.pnpm -t my-app .
    
    # For bun
    docker build -f Dockerfile.bun -t my-app .
    
    # Run the container
    docker run -p 3000:3000 my-app
  3. Install remix-hook-form and dependencies

    main

    To use remix-hook-form with validation, install the core package along with react-hook-form. For a complete setup with Zod validation, you should also install @hookform/resolvers and zod.

    npm install remix-hook-form react-hook-form
    npm install zod @hookform/resolvers
  4. Use Middleware mode to access form data

    main

    In version 7+, you can use unstable_extractFormDataMiddleware to make form data available in the context object of loaders and actions. This allows you to access validated or raw form data without manually calling getValidatedFormData from the request object.

    1. Setup: Add the middleware to your unstable_middleware array in root.tsx.
    2. Access: Use getFormData or getValidatedFormData from remix-hook-form/middleware, passing the context instead of the request.
    // root.tsx
    import { unstable_extractFormDataMiddleware } from "remix-hook-form/middleware";
    
    export const unstable_middleware = [unstable_extractFormDataMiddleware()];
    
    // loader or action
    import { getFormData, getValidatedFormData } from "remix-hook-form/middleware";
    
    export const action = async ({ context }: ActionFunctionArgs) => {
      const { data, errors, receivedValues } = await getValidatedFormData<FormData>(
        context,
        resolver,
      );
      // ...
    };
  5. Deploy manually (DIY Deployment)

    main

    If you are deploying a Node application manually, ensure you deploy the output generated by npm run build. The required structure includes the package manifest and the build/ directory:

    ├── package.json
    ├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
    ├── build/
    │   ├── client/    # Static assets
    │   └── server/    # Server-side code
  6. Basic usage of remix-hook-form with Zod

    main

    Use useRemixForm on the client to manage form state and getValidatedFormData in the server-side action to validate incoming data. The getValidatedFormData utility automatically returns errors and receivedValues (default values), which useRemixForm picks up to populate the form state if validation fails.

    import { useRemixForm, getValidatedFormData } from "remix-hook-form";
    import { Form } from "react-router";
    import { zodResolver } from "@hookform/resolvers/zod";
    import * as zod from "zod";
    import type { Route } from "./+types/home";
    
    const schema = zod.object({
      name: zod.string().min(1),
      email: zod.string().email().min(1),
    });
    
    type FormData = zod.infer<typeof schema>;
    const resolver = zodResolver(schema);
    
    export const action = async ({ request }: Route.ActionArgs) => {
      const { errors, data, receivedValues: defaultValues } =
        await getValidatedFormData<FormData>(request, resolver);
      if (errors) {
        return { errors, defaultValues };
      }
      return data;
    };
    
    export default function MyForm() {
      const {
        handleSubmit,
        formState: { errors },
        register,
      } = useRemixForm<FormData>({
        mode: "onSubmit",
        resolver,
      });
    
      return (
        <Form onSubmit={handleSubmit} method="POST">
          <label>
            Name:
            <input type="text" {...register("name")} />
            {errors.name && <p>{errors.name.message}</p>}
          </label>
          <label>
            Email:
            <input type="email" {...register("email")} />
            {errors.email && <p>{errors.email.message}</p>}
          </label>
          <button type="submit">Submit</button>
        </Form>
      );
    }
  7. Extract data from search parameters with getFormDataFromSearchParams

    main
    When performing a GET request, data is not available in the request.formData(). Use getFormDataFromSearchParams to extract data from the URL's search parameters, assuming your form data has been mapped to those parameters.
  8. Validate form data in an action with getValidatedFormData

    main

    Use getValidatedFormData in your Remix/React Router action to parse and validate incoming request data. It is designed to handle both standard JS-enabled submissions and no-JS (progressive enhancement) scenarios.

    Behavior:

    • With JS: Automatically extracts data from the request object and validates it.
    • No-JS (POST): Parses the formData object and converts it to the same shape as the data object returned by useRemixForm.
    • No-JS (GET): If used in a loader with a GET request, it attempts to extract data from search parameters.

    Return Value:

    Returns an object containing:

    • errors: An object matching the shape of useRemixForm errors (or undefined if valid).
    • data: The validated data object (or undefined if invalid).
    • receivedValues: The raw values received from the request. Returning this from your action allows useRemixForm to populate the form with these values (useful for persisting input on error when JS is disabled).

    To persist values on error, return defaultValues from your action, which maps to receivedValues.

    // Example: Returning errors and persisting values for no-js support
    export const action = async ({ request }: Route.ActionArgs) => {
      const { errors, data, receivedValues: defaultValues } = 
        await getValidatedFormData<FormData>(request, resolver);
    
      if (errors) {
        // Returning defaultValues allows the form to repopulate in no-js mode
        return { errors, defaultValues };
      }
    
      // Process valid data...
      return { success: true };
    };
  9. Validate existing formData with validateFormData

    main

    Use validateFormData when you have already extracted the formData object through a custom method and simply want to apply the validation logic. Unlike getValidatedFormData, it does not attempt to extract data from a Request object automatically.

    Arguments:

    • formData: The formData object to validate.
    • resolver: The validation resolver function.

    Returns:

    • errors: An object matching the shape of useRemixForm errors.
    • data: The validated data object.
    export const action = async ({ request }: Route.ActionArgs) => {
      // Custom way of getting data
      const formData = await myCustomParser(request);
      
      const { errors, data } = await validateFormData<FormData>(formData, resolver);
      
      if (errors) return { errors };
      // ...
    };
  10. Convert data to FormData with createFormData

    main

    The createFormData utility converts a plain JavaScript object (typically the data object returned by react-hook-form's handleSubmit) into a FormData instance. It converts values to strings and correctly appends File objects.

    This is primarily used when overriding the onValid handler in useRemixForm to perform custom submission logic.

    const { handleSubmit } = useRemixForm({
      submitHandlers: {
        onValid: (data) => {
          const formData = createFormData(data);
          // Now you can use fetch or custom logic with the formData
        }
      }
    });