Компоненты обертки могут быть очень полезные, когда вы работаете со сторонними библиотеками, которые предоставляют собственные компоненты. В этом случае, компонент обертка - это в самом простом случае тот компонент, который оборачивает другой компонент и передает ему необходимые вам параметры.
Вы можете спросить, в чем же преимущество такого подхода? Есть три важных аспекта - переиспользуемость, расширяемость и заменяемость. Давайте представим, что в нашем 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 на какую-нибудь другую, мы можем это сделать посредством внесения изменений только в один файл, не затрагивая другие модули системы. Но будьте осторожны с пробрасыванием параметров из сторонней библиотеки. Лучше передавать только те данные, которые вам действительно нужны.