Валидация формы в Remix с помощью Zod

Remix – это потрясающий фреймворк React для создания современного веб-опыта SSR (Sever Side Rendering). Это означает, что мы можем работать как с бэкендом, так и с фронтендом в одном приложении Remix. Remix действительно уникален и обладает множеством замечательных функций. Одна из самых ярких – это работа с формами. Remix возвращает традиционный метод работы с формами.

Remix предоставляет функции (называемые action и loaders), которые мы можем использовать для выполнения операций на стороне сервера и доступа к данным формы. С помощью этих функций нам больше не нужно передавать JavaScript на фронтенд для отправки формы, тем самым уменьшая куски javascript в браузере.

Когда мы выполняем валидацию, одной из моих личных библиотек является Zod. Zod – это библиотека объявления схем и валидации на основе TypeScript. С помощью Zod мы один раз объявляем валидатор, и Zod автоматически выводит статический тип TypeScript. Легко компоновать простые типы в сложные структуры данных.

Зачем нужна валидация?

Мы хотим, чтобы данные, предоставляемые пользователями, были безопасными и соответствовали ожиданиям. Есть три основные причины, по которым нам нужна валидация при создании приложения.

  • Мы хотим получить правильные данные в правильном формате. Наши приложения не будут работать должным образом, если данные пользователей хранятся в неправильном формате, неверны или вообще отсутствуют.

  • Мы хотим защитить данные наших пользователей. Заставляя наших пользователей вводить надежные пароли, мы облегчаем защиту их учетных данных.

  • Мы хотим защитить себя. Существует множество способов, с помощью которых злоумышленники могут использовать незащищенные формы для нанесения вреда приложению.

Что мы создаем

Мы создаем валидацию формы с нуля в Remix с помощью Zod. Часто возникает необходимость проверки данных на стороне сервера. Это – убийственная комбинация, которую мы можем иметь, чтобы наши данные, которые мы получаем от нашего API, были полностью типизированы, и мы получали только достоверные данные, которые нам нужны. Мы заставим пользователей просто отправлять данные, которые мы собираемся получить, чтобы проверить пользовательский ввод в маршруте, прежде чем данные будут сохранены, независимо от того, где мы хотим хранить данные.

Форма в Remix

Remix предоставляет пользовательский компонент Form, с которым мы можем работать идентично родному HTML-элементу. При работе с React нам нужно было слушать событие onChange во всех полях формы и обновлять состояние. Но вместо этого Remix использует данные формы из веб-интерфейса API formData().

Form – это компонент HTML-формы с поддержкой Remix, который ведет себя как обычная форма, за исключением того, что взаимодействие с сервером осуществляется с помощью fetch-запросов, а не запросов нового документа. Форма автоматически выполняет POST-запрос к маршруту текущей страницы. Однако мы можем настроить ее на PUT и DELETE и изменить в соответствии с нашими потребностями наряду с методом действия, необходимым для обработки запросов формы.

import { Form, useActionData } from '@remix-run/react';

export async function action({ request }) {
  //handle logic with form data and return a value  
}

export default function Index() {
  const actionData = useActionData(); 
//we access the return value of the action with this hook
  return (
    <Form
      method="post">
      //add our form fields here
      <button type="submit">Create Account</button>
    </Form>
  );
}
Вход в полноэкранный режим Выход из полноэкранного режима

Мы используем встроенный компонент формы Remix и применяем хук useActionData. Это специальный хук, который поможет нам отправить запрос (в данном случае POST) с данными формы на сервер, используя fetchAPI. Он возвращает данные JSON из действия маршрута. Чаще всего он используется при последующей обработке ошибок валидации формы.

Добавление нашей формы

Мы можем воспользоваться формой, импортированной из Remix, и использовать ее в нашей форме. Посмотрите на фрагмент ниже, как это просто.

<div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
        <div className="max-w-lg w-full space-y-8">
          <div>
            <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
              Remix Form Validation with Zod
            </h2>
          </div>
          <Form method="post" noValidate={true}>
            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="company-website"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Full name
                </label>
                <input
                  name="name"
                  type="text"
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                />
                <span className="text-sm text-red-500">
                  {/* print errors here */}
                </span>
              </div>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="Email"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Email
                </label>
                <input
                  name="email"
                  type="text"
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                />
                <span className="text-sm text-red-500">
                      {/* print errors here */}
                </span>
              </div>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="omfirm Email"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Confirm Email
                </label>
                <input
                  name="confirmEmail"
                  type="email"
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                />
              </div>
              <span className="text-sm text-red-500">
                    {/* print errors here */}
              </span>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="Expertise"
                  className="block text-sm font-medium text-gray-700"
                >
                  Expertise
                </label>
                <select
                  name="expertise"
                  className="mt-1 block w-full py-2 px-4 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                >
                  <option></option>
                  <option>Product Designer</option>
                  <option>Frontend Developer</option>
                  <option>Backend Developer</option>
                  <option>Fullstack Developer</option>
                </select>
              </div>
              <span className="text-sm text-red-500">
                        {/* print errors here */}
              </span>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="company-website"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Github URL
                </label>
                <input
                  name="url"
                  type="text"
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                />
              </div>
              <span className="text-sm text-red-500">
                        {/* print errors here */}
              </span>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="company-website"
                  className="block text-sm font-medium text-gray-700"
                >
                  Currently Available
                </label>
                <select
                  name="availability"
                  className="mt-1 block w-full py-2 px-4 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                >
                  <option></option>
                  <option>Full-time</option>
                  <option>Part-time</option>
                  <option>Contract</option>
                  <option>Freelance</option>
                </select>
              </div>
              <span className="text-sm text-red-500">
                       {/* print errors here */}
              </span>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="company-website"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Description
                </label>
                <textarea
                  name="description"
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                />
              </div>
              <span className="text-sm text-red-500">
                {/* print errors here */}
              </span>
            </div>

            <div>
              <button
                type="submit"
                className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
              >
                Submit
              </button>
            </div>
          </Form>
        </div>
      </div>
Вход в полноэкранный режим Выйти из полноэкранного режима

У нас есть базовая структура формы, мы также подключили кнопку для отправки, которая использует родной API submit formData().

Добавление логики валидации (с помощью Zod)

Когда пользователь нажимает на кнопку отправки. Вызывается функция действия. Это место, где мы добавим всю логику, необходимую для выполнения валидации.

Давайте установим нашу библиотеку, прежде чем мы сможем использовать ее с

npm i zod

Вход в полноэкранный режим Выход из полноэкранного режима
import { ActionFunction } from '@remix-run/node';
import { z } from 'zod';

export const action: ActionFunction = async ({ request }) => {
  const formPayload = Object.fromEntries(await request.formData());

  const validationSchema = z
    .object({
      name: z.string().min(3),
      email: z.string().email(),
      confirmEmail: z.string().email(),
      expertise: z.enum([
        'Product Designer',
        'Frontend Developer',
        'Backend Developer',
        'Fullstack Developer',
      ]),
      url: z.string().url().optional(),
      availability: z.enum(['Full-time', 'Part-time', 'Contract', 'Freelance']),
      description: z.string().nullable(),
    })
    .refine((data) => data.email === data.confirmEmail, {
      message: 'Email and confirmEmail should be same email',
      path: ['confirmEmail'],
    });

  try {
    const validatedSchema = validationSchema.parse(formPayload);
    console.log('Form data is valid for submission:', validatedSchema); //API call can be made here
  } catch (error) {
    return {
      formPayload,
      error,
    };
  }
  return {} as any;
};

Войти в полноэкранный режим Выход из полноэкранного режима

Есть несколько вещей, которые происходят в логике валидации. Мы определили нашу схему здесь с помощью метода z.object({}), предоставленного нам Zod. В приведенных ключах мы добавляем логику валидации по своему усмотрению.

Вы могли заметить, что мы охватили широкий спектр валидации, который включает в себя только проверку строк, email, минимальный символ, использование перечисления, url, необязательное поле или nullable. Позже мы также использовали метод .refine schema, который помогает нам добавлять пользовательскую логику валидации с помощью уточнений.

.refine(validator: (data:T)=>any, params?: RefineParams)

С его помощью мы можем определить пользовательскую проверку валидности в любой из схем Zod. В данном случае мы проверили, что оба поля электронной почты должны соответствовать друг другу. Подробнее об этом методе вы можете узнать из документации Zod здесь.

Мы продолжим и добавим дополнительные атрибуты, такие как key и defaultValue в поля нашей формы. Использование key={} в полях формы. Это gotcha, чтобы заставить React перерендерить компонент. В противном случае ваши данные могут не обновиться. Это происходит потому, что при использовании defaultValue={}, создавая неконтролируемый компонент, React будет считать, что данные неизменяемы, и не будет перерисовывать компонент при изменении значения.

Теперь разметка нашей формы будет выглядеть примерно так


export default function Index() {
  const actionData = useActionData();
  return (
    <div>
      <div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
        <div className="max-w-lg w-full space-y-8">
          <div>
            <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
              Remix Form Validation with Zod
            </h2>
          </div>
          <Form method="post" noValidate={true}>
            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="company-website"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Full name
                </label>
                <input
                  name="name"
                  type="text"
                  defaultValue={actionData?.formPayload?.name}
                  key={actionData?.formPayload?.name}
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                />
                <span className="text-sm text-red-500">
                  {actionData?.error?.issues[0]?.message}
                </span>
              </div>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="Email"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Email
                </label>
                <input
                  name="email"
                  type="text"
                  defaultValue={actionData?.formPayload?.email}
                  key={actionData?.formPayload?.email}
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                />
                <span className="text-sm text-red-500">
                  {actionData?.error?.issues[1]?.message}
                </span>
              </div>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="Confirm Email"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Confirm Email
                </label>
                <input
                  name="confirmEmail"
                  type="email"
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                  defaultValue={actionData?.formPayload?.confirmEmail}
                  key={actionData?.formPayload?.confirmEmail}
                />
              </div>
              <span className="text-sm text-red-500">
                {actionData?.error?.issues[2]?.message}
              </span>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="Expertise"
                  className="block text-sm font-medium text-gray-700"
                >
                  Expertise
                </label>
                <select
                  name="expertise"
                  className="mt-1 block w-full py-2 px-4 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                  defaultValue={actionData?.formPayload?.expertise}
                  key={actionData?.formPayload?.expertise}
                >
                  <option></option>
                  <option>Product Designer</option>
                  <option>Frontend Developer</option>
                  <option>Backend Developer</option>
                  <option>Fullstack Developer</option>
                </select>
              </div>
              <span className="text-sm text-red-500">
                {actionData?.error?.issues[3]?.message}
              </span>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="company-website"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Github URL
                </label>
                <input
                  name="url"
                  type="text"
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                  defaultValue={actionData?.formPayload?.url}
                  key={actionData?.formPayload?.url}
                />
              </div>
              <span className="text-sm text-red-500">
                {actionData?.error?.issues[4]?.message}
              </span>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="company-website"
                  className="block text-sm font-medium text-gray-700"
                >
                  Currently Available
                </label>
                <select
                  name="availability"
                  className="mt-1 block w-full py-2 px-4 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
                  defaultValue={actionData?.formPayload?.availability}
                  key={actionData?.formPayload?.availability}
                >
                  <option></option>
                  <option>Full-time</option>
                  <option>Part-time</option>
                  <option>Contract</option>
                  <option>Freelance</option>
                </select>
              </div>
              <span className="text-sm text-red-500">
                {actionData?.error?.issues[5]?.message}
              </span>
            </div>

            <div className="rounded-md shadow-sm -space-y-px">
              <div className="mb-6">
                <label
                  htmlFor="company-website"
                  className="block text-sm font-medium text-gray-700 pb-2"
                >
                  Description
                </label>
                <textarea
                  name="description"
                  className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                  placeholder=""
                  defaultValue={actionData?.formPayload?.description}
                  key={actionData?.formPayload?.description}
                />
              </div>
              <span className="text-sm text-red-500">
                {actionData?.error?.issues[6]?.message}
              </span>
            </div>

            <div>
              <button
                type="submit"
                className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
              >
                Submit
              </button>
            </div>
          </Form>
        </div>
      </div>
    </div>
  );
}
Вход в полноэкранный режим Выход из полноэкранного режима

Заключение

Мы успешно выполнили валидацию формы. Но следует отметить, что мы только что выполнили валидацию на стороне сервера, но клиентская сторона все еще остается. Рекомендуется проводить валидацию как на клиенте, так и на сервере, чтобы получить данные, которые мы ожидаем от пользователей. Об этом мы расскажем в нашей следующей статье.

Исходный код, использованный в этой статье, вы можете найти в репозитории Github.

Счастливого кодинга!

Оцените статью
Procodings.ru
Добавить комментарий