Компоненты обертки в React.js

Компоненты обертки в React.js

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

Вы можете спросить, в чем же преимущество такого подхода? Есть три важных аспекта - переиспользуемость, расширяемость и заменяемость. Давайте представим, что в нашем React приложении есть формы и в полях типа "Дата" мы используем сторонный компонент, date picker. Мы можем очень просто установить сторонную библиотеку React Date Picker и использовать его везде, где можно в нашем приложении.

Но что делать, если нам нужно добавть дополнительную функциональность? Например, мы хотим везде отображать заголовок рядом с полем? В этом случае нам нужно обновить все формы на всех страницах, чтобы добавить разметку в нужном месте. Однако, если мы создадим компонент обертку, мы можем очень просто внедрять дополнительные функции в сторонную библиотеку. Вот пример компонента, который я создал в одном из моих проектов:

import { useState } from 'react';

import { Form } from 'react-bootstrap';

import DatePicker from 'react-datepicker';

import { FormattedMessage } from 'react-intl';

import 'react-datepicker/dist/react-datepicker.css';

 

import { FormField } from './index.ts';

 

interface DateFieldProps extends FormField {

  value?: string | null;

}

 

export const DateField = ({

  module,

  field,

  register,

  errors,

  onChange,

  value = null,

}: DateFieldProps) => {

  const [startDate, setStartDate] = useState<Date | null>(value ? new Date(value) : null);

 

  return (

    <div className="mb-3">

      <Form.Label>

        <FormattedMessage id={module.name + '.' + field} />

      </Form.Label>

      <DatePicker

        className="form-control"

        dateFormat="yyyy.MM.dd"

        {...register(field)}

        selected={startDate}

        onChange={(date) => {

          onChange(field, formatDate(date ?? new Date()));

          setStartDate(date);

        }}

      />

      {errors[field] && <div className="d-block invalid-tooltip">{errors[field]?.message}</div>}

    </div>

  );

};

 

function formatDate(date: Date) {

  const year = date.getFullYear();

  const month = ('0' + (date.getMonth() + 1)).slice(-2); // Months are 0-based

  const day = ('0' + date.getDate()).slice(-2);

 

  return `${year}-${month}-${day}`;

}

 

 

Как можно заметить, компонент очень простой, поэтому и тест для него тоже будет очень простым:

 

import { fireEvent, render, screen } from '@testing-library/react';

import { FieldErrors } from 'react-hook-form';

import { expect, test, describe, vi } from 'vitest';

 

import '@testing-library/jest-dom';

import { WrapToRouterAndIntl } from '../../../../lib/tests.tsx';

import { Module } from '../../../module/types';

 

import { DateField } from './DateField.tsx';

 

describe('DateField', () => {

  test('should render the label and date picker', () => {

    const module = { name: 'Assets' } as Module;

    const field = 'datesold';

    const registerMock = vi.fn();

    const errors = {};

    const onChangeMock = vi.fn();

    const value = '2022.06.01';

 

    render(

      <WrapToRouterAndIntl>

        <DateField

          module={module}

          field={field}

          register={registerMock}

          errors={errors}

          onChange={onChangeMock}

          value={value}

        />

      </WrapToRouterAndIntl>

    );

 

    const label = screen.getByText('Date Sold');

    const datePicker = screen.getByRole('textbox');

 

    expect(label).toBeInTheDocument();

    expect(datePicker).toBeInTheDocument();

    expect(datePicker).toHaveValue(value);

  });

  test('triggers onChange event and updates date on date selection', () => {

    const module = { name: 'Assets' } as Module;

    const field = 'datesold';

    const registerMock = vi.fn();

    const errors = {};

    const onChangeMock = vi.fn();

    const value = '2022.06.01';

 

    render(

      <WrapToRouterAndIntl>

        <DateField

          module={module}

          field={field}

          register={registerMock}

          errors={errors}

          onChange={onChangeMock}

          value={value}

        />

      </WrapToRouterAndIntl>

    );

 

    const datePicker = screen.getByRole('textbox');

 

    fireEvent.change(datePicker, { target: { value: '2022.06.15' } });

 

    expect(onChangeMock).toHaveBeenCalledWith(field, '2022-06-15');

    expect(datePicker).toHaveValue('2022.06.15');

  });

 

  test('displays error message when there is an error', async () => {

    const module = { name: 'Assets' } as Module;

    const field = 'datesold';

    const registerMock = vi.fn();

    const errors = { datesold: { message: 'Invalid date' } } as unknown as FieldErrors;

    const onChangeMock = vi.fn();

 

    render(

      <WrapToRouterAndIntl>

        <DateField

          module={module}

          field={field}

          register={registerMock}

          errors={errors}

          onChange={onChangeMock}

        />

      </WrapToRouterAndIntl>

    );

 

    const errorTooltip = screen.getByText('Invalid date');

 

    expect(errorTooltip).toBeInTheDocument();

  });

});

 

Как можете заметить, наш кастомный компонент получает только необходимые параметры, а не все, что нужно сторонней библиотеке. В этом случае мы максимально изолируемся от внешней зависимости и в будущем при необходимости можем очень быстро перейти к другой библиотеке. Если же мы хотим в наш компонент передавать параметры, которые принимает Ract Date Picker, то наш компонент выглядил бы примерно следующим образом:

 

import { useState } from 'react';

import { Form } from 'react-bootstrap';

import DatePicker, { ReactDatePickerProps } from 'react-datepicker';

import { FormattedMessage } from 'react-intl';

import 'react-datepicker/dist/react-datepicker.css';

 

import { FormField } from './index.ts';

 

interface DateFieldProps extends FormField {

  value?: string | null;

} & ReactDatePickerProps;

 

export const DateField = ({

  module,

  field,

  register,

  errors,

  onChange,

  value = null,

  ...datePickerProps

}: DateFieldProps) => {

  const [startDate, setStartDate] = useState<Date | null>(value ? new Date(value) : null);

 

  return (

    <div className="mb-3">

      <Form.Label>

        <FormattedMessage id={module.name + '.' + field} />

      </Form.Label>

      <DatePicker

        className="form-control"

        dateFormat="yyyy.MM.dd"

        {...register(field)}

        selected={startDate}

        {...datePickerProps}

        onChange={(date) => {

          onChange(field, formatDate(date ?? new Date()));

          setStartDate(date);

        }}

      />

      {errors[field] && <div className="d-block invalid-tooltip">{errors[field]?.message}</div>}

    </div>

  );

};

 

function formatDate(date: Date) {

  const year = date.getFullYear();

  const month = ('0' + (date.getMonth() + 1)).slice(-2); // Months are 0-based

  const day = ('0' + date.getDate()).slice(-2);

 

  return `${year}-${month}-${day}`;

}

 

Теперь мы можем использовать наш компонент в других местах приложения:

 

import { getFieldByName } from '../../../module/services/fields.ts';

import { RelatedField } from '../../types';

 

import { CheckboxField } from './CheckboxField.tsx';

import { DateField } from './DateField.tsx';

import { FormField } from './index.ts';

import { NumberField } from './NumberField.tsx';

import { PicklistField } from './PicklistField.tsx';

import { StringField } from './StringField.tsx';

import { TextareaField } from './TextareaField.tsx';

 

interface GenerateFieldTypeProps extends FormField {

  value: string | number | boolean | RelatedField | string[];

}

 

export const GenerateFieldType = ({

  field,

  module,

  value,

  register,

  errors,

  onChange,

}: GenerateFieldTypeProps) => {

  const fieldModel = getFieldByName(module, field);

  if (!fieldModel) {

    return null;

  }

 

  if (fieldModel.type.name == 'date') {

    return (

      <DateField

        field={field}

        register={register}

        errors={errors}

        module={module}

        value={String(value)}

        onChange={onChange}

      ></DateField>

    );

  } 

  ...

};

 

В этом примере DateField компонент получает минимальное количество параметров - наименование поля, объект register от react-hook-forms, объект с ошибками, наименование модуля и функция, которая отрабатывает на изменение поля. Ну и конечно же первоначальное значение поля. Нет здесь ничего необычного, но как вы можете заметить, у нас появляется возможность добавлять функционал в библиотеку, не изменяя ее. Наш кастомный компонент теперь может использоваться в любых формах, а заголовок поля будет везде одинаковый.

Таким образом, подход в создании компонентов оберток дает нам несомненные премущества - переиспользуемость и расширяемость. Другое примущество состоит в том, что если мы захотим заменить библиотеку React Date Picker на какую-нибудь другую, мы можем это сделать посредством внесения изменений только в один файл, не затрагивая другие модули системы. Но будьте осторожны с пробрасыванием параметров из сторонней библиотеки. Лучше передавать только те данные, которые вам действительно нужны.

Популярное

Самые популярные посты

Как быть максимально продуктивным на удалённой работе?
Business

Как быть максимально продуктивным на удалённой работе?

Я запустил собственный бизнес и намеренно сделал всё возможное, чтобы работать из любой точки мира. Иногда я сижу с своём кабинете с большим 27-дюймовым монитором в своей квартире в г. Чебоксары. Иногда я нахожусь в офисе или в каком-нибудь кафе в другом городе.

Привет! Меня зовут Сергей Емельянов и я трудоголик
Business PHP

Привет! Меня зовут Сергей Емельянов и я трудоголик

Я программист. В душе я предприниматель. Я начал зарабатывать деньги с 11 лет, в суровые 90-е годы, сдавая стеклотару в местный магазин и обменивая её на сладости. Я зарабатывал столько, что хватало на разные вкусняшки.

Акция! Профессиональный разработчик CRM за 2000 руб. в час

Выделю время под ваш проект. Знания технологий Vtiger CRM, SuiteCRM, Laravel, Vue.js, Golang, React.js. Предлагаю варианты сотрудничества, которые помогут вам воспользоваться преимуществами внешнего опыта, оптимизировать затраты и снизить риски. Полная прозрачность всех этапов работы и учёт временных затрат. Оплачивайте только рабочие часы разработки после приемки задачи. Экономьте на платежах по его содержанию разработчика в штате. Возможно заключение договора по ИП. С чего начать, чтобы нанять профессионального разработчика на full-time? Просто заполните форму!

Telegram
@sergeyem
Telephone
+4915211100235