Повторно используемые react компоненты
Эта статья покажет вам, как вы можно создавать повторно используемые и более читаемые компоненты в React, придерживаясь всего нескольких простых правил.
Для того чтобы вам понять о чем сейчас пойдет речь, я приведу небольшой список того, что мы сейчас разберем:
- Повторно используемая логика
- Повторно используемая отображение
- Разделение логики и отображения
- Масштабируемость
Я считаю, что лучший способ учиться - это учиться на примере, так что давайте попробуем собрать небольшой набор компонентов, который будет отвечать за то, чтобы отображать список машин.
Начнем с компонента, который будет отвечать за весь список 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составным способом, не изменяя существующую реализацию.
На этом сегодня все, не забывайте подписываться на мой телеграм канал!