React
May 31, 2022

Повторно используемые react компоненты

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


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

  1. Повторно используемая логика
  2. Повторно используемая отображение
  3. Разделение логики и отображения
  4. Масштабируемость

Я считаю, что лучший способ учиться - это учиться на примере, так что давайте попробуем собрать небольшой набор компонентов, который будет отвечать за то, чтобы отображать список машин.

Начнем с компонента, который будет отвечать за весь список CarList:

import React from 'react';

export const CarsList = () => {
  const [cars, setCars] = React.useState([]);
  
  React.useEffect(() => {
    const fetchCars = async () => {
      const response = await fetch('http://localhost:4000/cars');
      setCars(await response.json())
    }
    fetchCars();
  }, [])
  
  return (
    <>
      {cars.map((car, index) => <li key={index}>[{++index}]{car.name} - {car.price}lt;/li>)}
    </>
  )
};

Ничего сложного, компонент просто получает список автомобилей для отображения в хуке useEffect и потом рендерит список. Сейчас у нас с вами все лежит в одном файле, я имею ввиду, что мы смешали логику и представление в одном месте. Кажется, что файл все равно небольшой и это нормально, но мы быстро придем в тупик в случае сложных компонентов. И, вероятно, потребуется писать много повторяющегося кода.


Повторно используемая логика

Сейчас нашей задачей будет вынести логику получения данных о списке машин через API в отдельное место и сохранить следованию правилу DRY (не повторяй сам себя).
Давайте отделим всю логику с получением списка в отдельный кастомных хук useFetchCars:

import { useState, useEffect } from 'react';

export const useFetchCars = () => {
  const [cars, setCars] = useState([]);
  
  useEffect(() => {
    const fetchCars = async () => {
      const response = await fetch('http://localhost:4000/cars');
      setCars(await response.json())
    
    fetchCars();
  }, []);
  
  return cars;
};

Дальше просто воспользуемся этим хуком в нашем основном компоненте:

import React from 'react';
import { useFetchCars } from '../hooks/useFetchCars'

export const CarsList = () => {
  const cars = useFetchCars();
  
  return (
    <>
      {cars.map((car, index) => <li key={index}>[{++index}]{car.name} - {car.price}lt;/li>)}
    </>
  )
};

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

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

import React from 'react';
import { useFetchCars } from '../hooks/useFetchCars'

export const CarsTable = () => {
  const cars = useFetchCars();
  
  return (
    <table>
      <thead>
        <tr>
          <th>#</th>
          <th>Название</th>
          <th>Цена</th>
        </tr>
      </thead>
      <tbody>
        {cars.map((car, index) => (
          <tr key={index}>
            <td>{++index}</td>
            <td>{car.name}</td>
            <td>{car.price}lt;/td>
          </tr>
        ))}
      </tbody>
    </table>
  )
};


Повторно используемая отображение

Дальше давайте поговорим о повторном использовании отображения. Сейчас, компонент CarList может работать только с теми данными, которые приходят из хука useFetchCars (это не особо хорошо).

Предположим, мы хотим отображать список автомобилей в одинаковом виде, но при этом иметь возможность получать данные с разных источников, поэтому давайте не будет привязываться к формату выдачи списка автомобилей конкретной ручкой API, а передадим список через пропс в компонент CarList:

import React from 'react';

const CarsList = ({ cars }) => {
  return (
    <>
      {cars.map((car, index) => <li key={index}>[{++index}]{car.name} - {car.price}lt;/li>)}
    </>
  )
};

... и вот мы достигли возможности повторного использования отображения.

Только сейчас нам нужно обратно объединить наш компонент для отображения с логикой получения списка машин. Добавим компонент CarsListContainer, который будет отвечать за объединение логики, а компонент CarsList переименуем в CarsListPresentation:

import React from 'react';
import { useFetchCars } from '../hooks/useFetchCars'

export const CarsListPresentation = ({ cars }) => {
  return (
    <>
      {cars.map((car, index) => <li key={index}>[{++index}]{car.name} - {car.price}lt;/li>)}
    </>
  )
};

export const CarsListContainer = () => {
  const cars = useFetchCars();
  return (
   <CarsListPresentation cars={cars} />
  )
};

CarsListPresentation - ничего не знает о том, откуда он получает информацию, именно это делает наш компонент повторно используемым.


Масштабируемость

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

Все дело в том, что когда нам нужно будет добавить новую логику в контейнер, то придется менять наш контейнер. Я не хотела бы этого делать, чтобы не нарушать достаточно базовый принцип Открыто/Закрыто, который гласит: "Программные объекты должны быть открыты для расширения, но закрыты для модификации".

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

Мы можем добиться этого, используя принцип компонентов высшего порядка (HOC).

Наш хок будет получать компонент CarsPresentation как аргумент, а прокидывать в него пропс с данными о списке автомобилей:

import React from 'react';
import { useFetchCars } from '../hooks/useFetchCars'

const CarsListPresentation = ({ cars }) => {
  return (
    <>
      {cars.map((car, index) => <li key={index}>[{++index}]{car.name} - {car.price}lt;/li>)}
    </>
  )
};

const withFetchCarsHOC = (Cars) => () => {
  const cars = useFetchCars();
  return <Cars cars={cars} />;
};

В итоге нам остается только использовать хок withFetchCarsHOC с компонентом CarsListPresentation и все готово!

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

Итог

Хочу подвести небольшой итог, чтобы вам было проще не забыть все сказанное выше:

  • Мы сделали нашу логику готовой к повторному использовани/ благодаря извлечению ее в хук useFetchCars.
  • Отображение списка автомобилей получает данные в качестве пропса, что делает наш компонент логически независимым и многоразовым.
  • Наша логика и отображение разделены, потому что у нас есть CarsListPresentation, который ничего не знает об источнике данных, и хук useFetchCars, который не знает о презентации.
  • После создания withFetchCarsHOC мы можем добавить новое поведение в CarsList составным способом, не изменяя существующую реализацию.

На этом сегодня все, не забывайте подписываться на мой телеграм канал!