Wrapper components in React.js

Wrapper components in React.js

Wrapper components can be very useful when you're working with third-party libraries that provide their own components. In this case, a wrapper component is, at its simplest, a component that wraps another component and passes it the props you want.

You may ask, what's the advantage of this approach? There are three important points: reusability, extensibility and substitutability. Let's imagine that our React application has forms and we use a third-party component, date picker, in fields like "Date". We can very easily install the third-party React Date Picker library and use it wherever we can in our application.

But what if we need to add additional functionality? For example, do we want to display a header near the field everywhere? In that case, we need to update all the forms on all the pages to add the markup in the right place. However, if we create a wrapper component, we can very easily implement additional features in a third-party library. Here's an example of a component I created in one of my projects:

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}`;

}

 

As you can see, the component is very simple, so the test for it will also be very simple:

 

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();

  });

});

 

As you can see, our custom component gets only the necessary props, not everything the third-party library needs. In this case, we are isolated from external dependencies as much as possible and can very quickly switch to another library if needed in the future. If, on the other hand, we want to pass the props that Ract Date Picker accepts to our component, our component would look something like the following:

 

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}`;

}

 

We can now use our component elsewhere in the application as follows:

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>

    );

  } 

  ...

};

 

In this example, the DateField component gets the minimum number of props - field name, register object from react-hook-forms, error object, module name and the function that operates on the field change. And, of course, the original value of the field. There is nothing unusual here, but as you can notice we have the ability to add functionality to the library without changing it. Our custom component can now be used in any form and the field header will be the same everywhere.

Thus, the approach in creating wrapper components gives us the undoubted advantages of reusability and extensibility. Another advantage is that if we want to replace React Date Picker library with some other one, we can do it by making changes only in one file without affecting other modules of the system. But be careful about passing props from a third-party library. It is better to pass only the data you really need.

Popular Posts

My most popular posts

Maximum productivity on remote job
Business

Maximum productivity on remote job

I started my own business and intentionally did my best to work from anywhere in the world. Sometimes I sit with my office with a large 27-inch monitor in my apartment in Cheboksary. Sometimes I’m in the office or in some cafe in another city.

Hello! I am Sergey Emelyanov and I am hardworker
Business PHP

Hello! I am Sergey Emelyanov and I am hardworker

I am a programmer. I am an entrepreneur in my heart. I started making money from the age of 11, in the harsh 90s, handing over glassware to a local store and exchanging it for sweets. I earned so much that was enough for various snacks.

Hire Professional CRM developer for $25 per hour

I will make time for your project. Knowledge of Vtiger CRM, SuiteCRM, Laravel, and Vue.js. I offer cooperation options that will help you take advantage of external experience, optimize costs and reduce risks. Full transparency of all stages of work and accounting for time costs. Pay only development working hours after accepting the task. Accept PayPal and Payoneer payment systems. How to hire professional developer? Just fill in the form

Telegram
@sergeyem
Telephone
+4915211100235