3 шаблона проектирования компонентов React, о которых вы должны знать

3 шаблона проектирования компонентов React, о которых вы должны знать

Содержание
  1. 1. Паттерн презентационных и контейнерных компонентов
  2. 2. Паттерн провайдера
  3. 3. Паттерн составных компонентов
    1. React.cloneElement React Context
  4. Заключение

React, пожалуй, самая популярная библиотека JavaScript для создания пользовательских интерфейсов, и одной из причин этого является ее беспристрастный характер. Независимо от того, рассматриваете ли вы React как фреймворк или как библиотеку, можно согласиться с одним - это ее свободный подход к тому, как разработчики должны создавать приложения на React, что дает разработчикам и командам разработчиков свободу решать, как они хотят сделать свои приложения. Поработав над разными React-приложениями в разных командах и изучив другие созданные React-приложения, вы заметите несколько общих паттернов проектирования.

В этой статье мы рассмотрим три популярных паттерна проектирования для создания React-приложений.

1. Паттерн презентационных и контейнерных компонентов

Этот паттерн был придуман Дэном Абрамовым. В этом шаблоне компоненты делятся на:

Презентационные компоненты: Это компоненты, которые отвечают за внешний вид пользовательского интерфейса. Они не имеют зависимостей от какой-либо части приложения и используются для отображения данных. Примером может служить список:

function ItemList(props) {
	return (
		<ul>
			{props.items.map(({ id, url, name }) => (
				<li key={id}>
					<a href={url}>{name}</a>
				</li>
			))}
		</ul>
	);
}

В приведенном выше примере наш компонент ItemsList отвечает только за отображение данных, переданных в качестве props , в пользовательском интерфейсе. Презентационные компоненты также называют функциональными компонентами без состояния, но они могут быть написаны как компоненты классов и могут содержать состояние, относящееся к пользовательскому интерфейсу

class TextInput extends React.Component {
	constructor(props) {
		super(props);
		this.state = {
			value: '',
		};
	}

	handleChange = (event) => {
		this.setState({
			value: event.target.value,
		});
	};

	render() {
		return <input value={this.state.value} onChange={this.handleChange} />;
	}
}

В примере выше мы создали компонент класса Presentational, TextInput , отвечающий за управление его состоянием.

Компоненты-контейнеры: В отличие от презентационных компонентов, контейнерные компоненты в большей степени отвечают за то, как все работает. Обычно это компоненты класса, содержащие методы жизненного цикла и компоненты Presentational. В них также происходит выборка данных.

class TvShowsContainer extends React.Component {
	constructor(props) {
		super(props);
		this.state = {
			shows: [],
			loading: false,
			error: '',
		};
	}

	componentDidMount() {
		this.setState({
			loading: true,
			error: '',
		});

		fetch('https://api.tvmaze.com/schedule/web?date=2020-05-29')
			.then((res) => res.json())
			.then((data) =>
				this.setState({
					loading: false,
					shows: data,
				}),
			)
			.catch((error) =>
				this.setState({
					loading: false,
					error: error.message || error,
				}),
			);
	}

	render() {
		const { loading, error, shows } = this.state;

		return (
			<div>
				<h1> Tv Shows </h1> {loading && <p> Loading... </p>}{' '}
				{!loading && shows && <ItemsList items={shows} />} {!loading && error && <p> {error} </p>}{' '}
			</div>
		);
	}
}

Мы создали компонент TvShowsContainer , который получает данные из API, когда компонент монтируется в приведенном выше примере. Он также передает эти данные презентационному компоненту ItemsList , который мы создали ранее. Преимуществом этого паттерна является разделение проблем и возможность повторного использования компонентов. Другие компоненты Container могут повторно использовать презентационный компонент ItemList для отображения данных, поскольку он не связан жестко с TvShowsListContainer . Рабочее приложение можно посмотреть здесь.

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

2. Паттерн провайдера

Одна из основных проблем, с которой сталкиваются разработчики React, - это Prop drilling. Prop drilling - это сценарий, в котором данные (props) передаются в различные компоненты, пока не попадут в тот компонент, где они нужны. Хотя prop-drilling не так уж плох, он становится проблемой, когда несвязанные компоненты совместно используют данные, что приводит нас к паттерну Provider. Паттерн Provider позволяет нам хранить данные в центральном месте, например, в объекте React Context или в хранилище Redux. Затем поставщик/хранилище контекста может передавать эти данные любому компоненту, которому они нужны, напрямую, без использования реквизитов.

Представьте, что в веб-приложении реализован темный режим, а несвязанные компоненты реагируют на смену темы, вызванную другим компонентом. Мы можем добиться этого, используя паттерн Provider. Мы создаем объект React context для хранения значения темы.

import { createContext } from 'react';

const ThemeContext = createContext({
	theme: 'light',
	setTheme: () => {},
});

export default ThemeContext;

В файле App.js мы оборачиваем импортируемые компоненты с помощью ThemeContext. Provider. Это дает различным компонентам и их дочерним элементам доступ к созданному объекту Context

import React, { useState, useMemo } from 'react';
import Header from './Header';
import Main from './Main';
import ThemeContext from './context';
import './styles.css';

export default function App() {
	const [theme, setTheme] = useState('');
	const value = useMemo(
		() => ({
			theme,
			setTheme,
		}),
		[theme],
	);

	return (
		<ThemeContext.Provider value={value}>
			<div className="container">
				<Header />
				<Main />
			</div>{' '}
		</ThemeContext.Provider>
	);
}

По умолчанию ThemeContext является stateless и не может быть обновлен. Чтобы решить эту проблему, мы можем подключить ThemeContext к состоянию и предоставить функцию обновления в ThemeContext для изменения состояния.

Чтобы получить доступ к ThemeContext в компонентах, мы можем воспользоваться хуком useContext, представленным в React 16.9

import {
    useContext
} from "react";
import ThemeContext from "./context";

const Header = () => {
    const {
        theme,
        setTheme
    } = useContext(ThemeContext);

    const toggleTheme = () => {
        if (theme === "dark") {
            setTheme("");
            return;
        }
        setTheme("dark");
        return;
    };

    return ( <
        header className = {
            theme === "dark" && "dark"
        } >
        <
        h1 > Тв - шоу < /h1> <
        button onClick = {
            toggleTheme
        } > Toggle Theme < /button> <
        /header>
    );
};

export default Header;

import {
    useContext
} from "react";
import ThemeContext from "./context";

const Main = () => {
    const {
        theme
    } = useContext(ThemeContext);

    return ( <
        main className = {
            theme === "dark" && "dark"
        } >
        <
        h2 > {
            ""
        } {
            theme === "dark" ? "Включена темная тема" : "Включена светлая тема"
        } <
        /h2> <
        /main>
    );
};

export default Main;

Хотя Context упрощает передачу данных между компонентами, рекомендуется использовать этот подход осторожно, поскольку он затрудняет повторное использование компонентов. Рабочее приложение приведенного выше примера можно посмотреть здесь. Шаблон Provider используется в React Router и React-Redux.

3. Паттерн составных компонентов

Составные компоненты - это компоненты, которые имеют общее состояние и работают вместе для достижения общей цели. Примером могут служить элементы HTML <select> и <option> . В сочетании они создают выпадающее меню, но сами по себе они мало чего достигают.

Шаблон Compound Components используется в популярных библиотеках React UI, например, Ant Design и Material UI. Ниже приведена реализация компонента Menu в Material UI

import * as React from 'react';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';

export default function MaterialMenu() {
	return (
		<div>
			<Button> Menu </Button>{' '}
			<Menu>
				<MenuItem> Profile </MenuItem> <MenuItem> My account </MenuItem>{' '}
				<MenuItem> Logout </MenuItem>{' '}
			</Menu>{' '}
		</div>
	);
}

Без составных компонентов нам пришлось бы передавать реквизиты родительскому компоненту, а затем родительский компонент передавал бы данные дочерним компонентам

<Элементы меню={['Профиль','Мой аккаунт','Выход']} />

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

<Пункты меню={['Профиль','Мой аккаунт','Выход']} defaultSelected={1} />

По мере увеличения количества требований компонент начинает становиться беспорядочным и непригодным для использования. Шаблон составного компонента обеспечивает более чистый способ достижения этой цели.

Существует два способа создания компонента React с использованием паттерна составного компонента:

React.cloneElement React Context

В примере ниже я буду использовать подход React Context

import React, { createContext, useState, useCallback, useMemo, useContext } from 'react';
import './styles.css';

const MenuContext = createContext();

const Menu = ({ children, defaultSelected }) => {
	const [selectedItem, setSelectedItem] = useState(defaultSelected);

	const toggleSelectedItem = useCallback(
		(item) => {
			if (item !== selectedItem) {
				setSelectedItem(item);
				return;
			}
			setSelectedItem('');
		},
		[selectedItem, setSelectedItem],
	);

	const value = useMemo(
		() => ({
			toggleSelectedItem,
			selectedItem,
		}),
		[toggleSelectedItem, selectedItem],
	);

	return (
		<MenuContext.Provider value={value}>
			<menu className="menu"> {children} </menu>{' '}
		</MenuContext.Provider>
	);
};

Мы создали объект контекста MenuContext для компонента Menu с помощью функции createContext , предоставляемой React Context API. Он будет хранить общее состояние для компонентов Menu и MenuItem . Мы также создали состояние для выбранного пункта меню. Это позволит нам обновлять контекст аналогично тому, как мы делали это в паттерне Provider, поскольку Context API по своей конструкции не имеет состояния.

Следующим шагом будет создание компонента MenuItem.

const useMenuContext = () => {
	const context = useContext(MenuContext);

	if (!context) {
		throw new Error('Компонент пункта меню не может быть использован вне компонента Menu.');
	}

	return context;
};

const MenuItem = ({ value, children }) => {
	const { toggleSelectedItem, selectedItem } = useMenuContext();

	return (
		<button
			onClick={() => toggleSelectedItem(value)}
			id={`${value}-menu-item`}
			className={`menu__item ${selectedItem === value && 'active'}`}
		>
			{children}{' '}
		</button>
	);
};

Первое, что здесь делается, это создание cuстом хук useMenuContext для проверки, не используется ли MenuItem вне компонента Menu, и выброса ошибки в этом случае. После этого мы создаем наш MenuItem, используя общее состояние с компонентом Menu, чтобы определить, какой стиль применить к выбранному MenuItem, и изменить выбранный элемент при щелчке на пункте меню.

В завершение мы соединяем эти компоненты вместе в компоненте App

export default function App() {
	return (
		<Menu defaultSelected="My account">
			<MenuItem value="Profile">Profile</MenuItem>
			<MenuItem value="My account">My account</MenuItem>
			<MenuItem value="Logout">Logout</MenuItem>
		</Menu>
	);
}

Заключение

В этой статье мы рассмотрели различные паттерны проектирования, которые можно использовать при создании расширяемых и многократно используемых компонентов React. Хотя этот список не является исчерпывающим, он применим к большинству проблем, с которыми вы, вероятно, столкнетесь при создании компонентов.