Повторно используемые 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
составным способом, не изменяя существующую реализацию.
На этом сегодня все, не забывайте подписываться на мой телеграм канал!