React Hook Form - невероятно полезный пакет для создания простых и сложных веб-форм. В этой статье показан подход нашей команды к организации и тестированию компонентов вложенных форм с использованием ловушек <FormProvider /> и useFormContext() React Hook Form с последующим тестированием компонентов формы с помощью Библиотеки тестирования.

Стандартная настройка React Hook Form

Когда наши формы были небольшими и находились на стадии прототипирования, было разумно инициализировать React Hook Form стандартным способом в соответствии с его документами.

import React from "react"; 
import { useForm } from "react-hook-form";
export default function App() { 
  const { register, handleSubmit, watch, errors } = useForm();
  const onSubmit = data => console.log(data);
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="firstName" defaultValue="Zoe" ref={register} />
      <input type="submit" />
    </form>
  );  
  
}

Организация сложной формы

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

Вот общий пример настройки некоторых наших форм. Наше приложение-репозиторий содержит формы метаданных, которые содержат 30–50 элементов, от простых входных данных до раскрывающихся списков ввода, до массивов полей (многозначных) входных данных и т. Д.

Мы организуем сложные, единичные формы в разделы.

// The form
<form onSubmit={handleSubmit(onSubmit)}>
  {/* Won't repeat, but initially we explicitly drilled React Hook Form props down to any and all child components which may need them.  Not good:) */}
  <CoreMetadata register={register} errors={errors} {...etc} />
  
  <ControlledTerms />
  <DescriptiveMetadata />
  ...
  <input type="submit" />
</form>

Компонент раздела:

// CoreMetadata.js
...
return (
  <>
   <SubCategory1 register={register} errors={errors} {...etc} />
   <SubCategory2 />
   <SubCategory3 />
  </>
);

Вложенный компонент:

// SubCategory1.js
...
return(
  <>  
    <UIFormInput name="yo" required register={register} errors={errors} />
    <UIFormSelect name="bigList" options={listOfOptions} required />
  
    {/* Maybe some element goes here which dynamically renders depending on form values or state of the form? */}
  </>
);

И, в конечном итоге, дочерний компонент «конечного уровня», в котором мы подключаем React Hook Form к элементу формы.

// UIFormInput.js

function UIFormInput({name, type, register, errors}) {
  return (
    <>
      <input 
        name={name} 
        type={type} 
        ref={register && register({ required })} 
        className={`input ${errors[name] ? "is-danger" : ""}`
        {...passedInProps} 
      />
{errors[name] && (
        <p data-testid="input-errors" className="help is-danger">
          {label || name} field is required
        </p>
      )}
    </>
  );
}

Первоначальный шаблон детализации методов React Hook Form как props для каждого дочернего компонента в стеке компонентов копировался снова и снова, и мы дублировали этот неэффективный шаблон, потому что он работал, но чувствовал себя не совсем правильно.

useFormContext спешит на помощь

Недавно мы перевели наши реализации React Hook Form и дочерние компоненты на использование useFormContext. Поскольку мы стремимся использовать собственные библиотеки компонентов и ищем согласованное решение, теперь мы настраиваем наши формы с помощью Context:

// Updated Form
import React from "react"; 
import { useForm, FormProvider } from "react-hook-form";
export default function App() { 
  const methods = useForm();
  const onSubmit = data => console.log(data);
return(
    <FormProvider {...methods}>
      <form onSubmit={handleSubmit(onSubmit)}>
        <CoreMetadata {...PASS_WHATEVER_PROPS_HERE} />
        <ControlledTerms />
        <DescriptiveMetadata />
      </form>
    </FormProvider>
  );  
  
}

Этот подход работает с шаблонами React Context / Provider, и любой дочерний компонент в дереве предков может получить контекст формы React Hook, если ему это нужно. Компоненты среднего уровня, которые не заботятся о register или error, освобождаются от багажа.

import React from "react";
import { useFormContext } from "react-hook-form";
const UIFormInput = ({ name, required, ...passedInProps}) => { 
  // All these values are from the component's parent <form />
  const { control, errors, register } = useFormContext();
  return (
    <>
      <input 
        name={name} 
        ref={register({ required })} 
        className={`input ${errors[name] ? "is-danger" : ""}`
        {...passedInProps} 
      />
      {errors[name] && (
        <p data-testid="input-errors" className="help is-danger">
          {label || name} field is required
        </p>
      )}
    </>
  );
}

Так что, если в вашем приложении несколько форм? В React Hook Form Context, в какой бы форме ни находился компонент, это данные формы, которые компонент получает через ловушку. Это также позволяет упростить тестирование компонентов за счет передачи любого контекста формы в каждый тест.

Тестирование

Хорошо, теперь у нас намного меньше кода в наших компонентах. Посмотри на этот пиар. Ого, намного лучше. Перейдем к тестированию компонентов React Hook Form useContext() с Библиотекой тестирования.

Эта renderWithReactHookForm вспомогательная функция действует так же, как и другие рецепты библиотеки тестирования, возвращая то, что вернула функция render() библиотеки тестирования. Например, вот рецепт библиотеки тестирования для обертывания с помощью React Router.

Пример теста с использованием renderWithReactHookForm может выглядеть так:

В нашем тесте выше мы оборачиваем тестируемый компонент с помощью <FormProvider /> React Hook Form и можем инициализировать форму с некоторыми значениями по умолчанию.

Почему это поможет?

Обработка со значениями по умолчанию

Допустим, у вас есть форма, которая собирает список значений, но имеет начальные значения. (У нас также есть приличное количество реализаций сложных форм, которые используют хук useFieldArray React Hook Form).

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

// Super simplified example HTML
<input name="multiValueField[0].url" />
<select name="multiValueField[0].category>
  ...
</select>

// How React Hook Form keeps track of the form data
multiValueField[0].url
multiValueField[0].category
multiValueField[1].url
multiValueField[1].category
...and so forth

Компонент формы <UIFormRelatedURL /> отображает список существующих значений, полученных из API, которые пользователь может удалить. Пользователь также может добавлять столько дополнительных значений, сколько пожелает. В тестах мы вводим значения по умолчанию в React Hook Form точно так же, как на самом деле это делает код. Таким образом, мы можем проверить, действительно ли наш компонент отображает правильные начальные значения.

Объединение ‹FormProvider /› с другими провайдерами

Допустим, вы используете в своем приложении другие инструменты, такие как GraphQL с Apollo Client или React Router, и ваше приложение выглядит примерно так:

...
export default class Root extends React.Component {
  render() {
    return (
      <ReactiveBase
        app={ELASTICSEARCH_INDEX_NAME}
        url={ELASTICSEARCH_PROXY_ENDPOINT}
      >
        <AuthProvider>
          <BatchProvider>
            <BrowserRouter>
              <Switch>
                <Route exact path="/login" component={Login} />    
                <PrivateRoute exact path="/project/list" component={ScreensProjectList}
                />
                ...

Если вы тестируете компонент, который оборачивается другими поставщиками тестирования, такими как Apollo Client, React Router, ElasticSearch и т. Д., Мы можем перенаправить шаблон renderWithReactHookForm на компонент более высокого порядка, который возвращает обычный Component вместо render() функции библиотеки тестирования React.

И мы бы использовали его следующим образом:

Расширение renderWithReactHookForm()

Теперь, когда мы можем настроить индивидуальный контекст формы при тестировании компонентов, мы также можем расширить renderWithReactHookForm, чтобы проверить, как компонент реагирует на определенные значения контекста формы, без отправки формы, что невозможно при тестировании глубоко вложенного компонента, который не отображает элемент <form /> или кнопку submit.

Примечание. Я не на 100% уверен, что это хорошая идея или шаблон, но он позволяет, по крайней мере, протестировать проверку формы вложенных компонентов и то, как пользовательский интерфейс должен реагировать на неверные данные формы. Может быть, вы можете взять эту идею и усовершенствовать ее для своих вариантов использования или как-то улучшить… просто поэкспериментировав.

В строке № 18 вы увидите toPassBack, который представляет собой массив методов React Hook Form, например setError.

В строках № 24–26 мы добавляем методы к нашему вспомогательному объекту reactHookFormMethods.

Затем в строку №32 мы включаем дополнительный объект reactHookFormMethods, который добавляется к тому, что возвращает функция библиотеки тестирования render() (наряду с такими методами, как getByTestId и т. Д.).

Вот примерный пример того, как это можно использовать:

Заключение

Возможно, вы найдете эту вспомогательную функцию-оболочку чем-то полезной. React Hook Form и Библиотека тестирования - лучшие пакеты React, на которых разработчики строят множество вещей, поэтому приятно видеть, как упростить тестирование. Любые мысли / комментарии / мнения более чем приветствуются.

Если вы хотите увидеть пример кода в контексте приложения Elixir / React с открытым исходным кодом, вот ссылка на репозиторий Github: