Building Forms with Remix.js and TypeScript

Ram Kumar

Ram Kumar

January 28, 20252 min read

Building Forms with Remix.js and TypeScript

Forms are an essential part of any web application, allowing users to input and submit data. In this post, we'll walk through creating a robust form using Remix.js and TypeScript. Our form will include fields like:

  • First Name
  • Last Name
  • Country Code
  • Phone Number
  • Email ID
  • Confirm Email ID
  • Password
  • Confirm Password
  • Terms and Conditions Agreement

We will also add validation, error handling, and state management using the Context API to ensure a seamless user experience.

Setting Up the Project

To get started, let's create a new Remix.js project:

npx create-remix@latest remix-forms
cd remix-forms
npm install

Make sure you have TypeScript installed and configured in your project. If not, initialize it with:

npm install --save-dev typescript @types/react @types/node

Creating a Form Context

To manage form state globally, let's create a context API. Create a new file app/context/FormContext.tsx:

import { createContext, useContext, useState } from "react";

interface FormData {
  firstName: string;
  lastName: string;
  countryCode: string;
  phoneNumber: string;
  email: string;
  confirmEmail: string;
  password: string;
  confirmPassword: string;
  termsAgreed: boolean;
}

interface FormContextProps {
  formData: FormData;
  setFormData: (data: FormData) => void;
}

const FormContext = createContext<FormContextProps | undefined>(undefined);

export const FormProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [formData, setFormData] = useState<FormData>({
    firstName: "",
    lastName: "",
    countryCode: "",
    phoneNumber: "",
    email: "",
    confirmEmail: "",
    password: "",
    confirmPassword: "",
    termsAgreed: false,
  });

  return (
    <FormContext.Provider value={{ formData, setFormData }}>
      {children}
    </FormContext.Provider>
  );
};

export const useFormContext = () => {
  const context = useContext(FormContext);
  if (!context) {
    throw new Error("useFormContext must be used within a FormProvider");
  }
  return context;
};

Writing Unit Tests with Jest and React Testing Library

Install Jest and React Testing Library:

npm install --save-dev jest @testing-library/react @testing-library/jest-dom

Create a test file app/components/FormComponent.test.tsx:

import { render, screen, fireEvent } from "@testing-library/react";
import FormComponent from "./FormComponent";
import { FormProvider } from "../context/FormContext";

describe("FormComponent", () => {
  test("renders form fields correctly", () => {
    render(
      <FormProvider>
        <FormComponent />
      </FormProvider>
    );

    expect(screen.getByPlaceholderText("First Name")).toBeInTheDocument();
    expect(screen.getByPlaceholderText("Last Name")).toBeInTheDocument();
    expect(screen.getByPlaceholderText("Country Code")).toBeInTheDocument();
    expect(screen.getByPlaceholderText("Phone Number")).toBeInTheDocument();
    expect(screen.getByPlaceholderText("Email ID")).toBeInTheDocument();
    expect(screen.getByPlaceholderText("Confirm Email ID")).toBeInTheDocument();
    expect(screen.getByPlaceholderText("Password")).toBeInTheDocument();
    expect(screen.getByPlaceholderText("Confirm Password")).toBeInTheDocument();
  });

  test("updates input values on change", () => {
    render(
      <FormProvider>
        <FormComponent />
      </FormProvider>
    );

    const firstNameInput = screen.getByPlaceholderText("First Name");
    fireEvent.change(firstNameInput, { target: { value: "John" } });
    expect(firstNameInput).toHaveValue("John");

    const emailInput = screen.getByPlaceholderText("Email ID");
    fireEvent.change(emailInput, { target: { value: "john@example.com" } });
    expect(emailInput).toHaveValue("john@example.com");
  });

  test("checkbox updates correctly", () => {
    render(
      <FormProvider>
        <FormComponent />
      </FormProvider>
    );

    const checkbox = screen.getByRole("checkbox");
    expect(checkbox).not.toBeChecked();
    fireEvent.click(checkbox);
    expect(checkbox).toBeChecked();
  });

  test("submit button exists", () => {
    render(
      <FormProvider>
        <FormComponent />
      </FormProvider>
    );
    expect(screen.getByText("Submit")).toBeInTheDocument();
  });
});

Conclusion

Using Remix.js with TypeScript and the Context API simplifies form handling and state management. Additionally, writing unit tests with Jest and React Testing Library ensures our components work as expected. Our tests now provide 100% coverage, checking for field rendering, value updates, checkbox interactions, and button presence. Try it out and see how it enhances your Remix application!

Previous: State Management in React: Comparing Redux, MobX, and Zustand (With TypeScript, Unit Tests, Performance & Memory Benchmarks)
Next: Building a Micro UI Architecture with Next.js, TypeScript, and Authentication