Комментарии 30
И да, в статье неточность. Слоты (slot), это render callback, а не то что описано в статье. Кроме того, не могу не заметить, что продвигаемая «фишка» в виде rander callback, жутнко нарушает предыдущий столп маркетинга реакта — декларативность jsx.
И да, чтобы кому-то не показалось что я реакто и Дэно ненавистник, могу сказать, что я много лет пишу на реакте и несмотря на то, что ещё столько же лет пишу на angular и vue, бросать как его, так и другие инструменты не собираюсь. Будет скучно. Но не критиковать нельзя. Мне кажется, что это силаспособная подтолкнуть процес и сделать разработку ещё более драйвовым.
И почему декларативность нарушается? Наоборот — в одной половине четко видно где что-то будет, а в другой видно что именно, без необходимости искать конкретное место подключения.
Слоты они и в вебкомпонентах, ангуляре и vue — слоты.
Согласен! Но Ваше представление о пользовательской логике слотов, отличается от реализвации пользовательской логики в описанных Вами же технологиях. В реакт, ближе всего по духу к слотам — render callback.
И почему декларативность нарушается?
Вам виднее…
И сравнивать юнит с интаграционными тестами и говорить что одни нужнее других, это свидетельство о нездоровом непонимании процесса разработки. Кроме того, DI и, в случаи с компонентами в качестве props, агрегация, это как небо и земля.
Специально проверил — render callback это старое имя render prop. Не слот.
> И сравнивать юнит с интаграционными тестами и говорить что одни нужнее других, это свидетельство о нездоровом непонимании процесса разработки.
Вводная часть статьи как раз про то, что одни нельзя заменить другими, и оба нужны в форме симбиоза имени Пирамиды Тестирования. Не в том плане что каждый следуйщий уровень уже и тормознее, а в том, что каждый следуйший уровень опирается на предыдущий.
Согласен! Но Ваше представление о пользовательской логике слотов, отличается от реализвации пользовательской логики в описанных Вами же технологиях. В реакт, ближе всего по духу к слотам — render callback.
В React экосистеме это слово просто не в ходу. Но, к примеру, используется во Vue экосистеме. И в React можно реализовать тоже самое за счёт:
- передачи компонента как prop
- передачи метода, который отрендерит, что надо как prop
Если вдуматься, то п2. это особая разновидность п1.
И передав компонент как пропс, функционал получится тот же, но удовольствие уже испортится. На дворе 2019 год, и мне не хочется писать множество render props, превращая шаблон в кашу. Это жуткий минут реакта. Они всюду говорят что это только вью, но по сравнению с другими вьюхами, вью реакта прошлый век. Реакт не развивается как вью. Он все больше обрастает js'ом в шаблоне.
Но я сразу не подумал, что слотов может быть сколько угодно, а render callback, только один. Поэтому в реакт единственный вариант повторить функционал слотов, это props или render props.
Нууу, нет. Дважды нет. Всё… по-другому. У react просто нет слотов на уровне библиотеки. Но сделать их можно 3 путями: компонент, vdom, renderFn. А путей доставки два: context & props. Любой из них не лимитирован одной единицей. Тут же императивщина: что хотим, то творим.
<InnerComp
var1={Comp1}
val2={<Comp2 someProp={true}/>}
var3={someRenderFn}
/>
Вариант 1. Возвращается сам компонент как таковой. Вложенный компонент может его применить как угодно, например:
const { Title } = props;
return <Title/>
Вариант 2. Это как раз vue-way. Мы передаём готовый кусок vDom-а. Вложенный компонент может применить его так:
const { innerContent } = props;
return <div>{innerContent}</div>
Вариант 3. Чаще наверное всё таки используется как event-handler, нежели для построения vDOM-а (слоты). Но и для слотов тоже используется:
const { renderFn } = this.props;
const { stateProp } = this.state;
return <div>{renderFn(stateProp)}</div>
Какого-то конкретного сертифицированного подхода для слотов нет. Добавьте к этому ещё возможность подключить контекст и мы получаем зоопарк решений.
Во Vue же слоты это часть синтаксиса шаблонов и вообще готовый к использованию механизм со своими тонкостями. В React подобные штуки вы делаете вручную согласно вашим воззрениям на архитектуру проекта.
это не декларативно
А если так?
const Tab = () =>
<div class="tab">
<h1><slot name="title"/></h1>
<article><slot name="content"/></article>
</div>;
export default Tab |> handleSlots;
+
const App = () =>
<Tab>
<template slot="title">Example</template>
<template slot="content">of slots support</template>
</Tab>;
export default App |> provideSlots;
Теперь декларативно? :) А вообще вы желаете от React & JSX странного. В JSX не завезли даже ветвлений аля if, switch, for, а вам слоты не нравятся :)
это сводит слова о преимуществах реакта на нет.
Давайте всё же без популизма. Обсуждаемая нами проблема имеет настолько опосредованное отношение к реальным проблемам React и JSX, что такие пассажи...
Контекст не предназначен для передачи компонентов
Кстати нет. Это просто механизм, не более. Его единственное предназначение — избегать props-hell. То, чем должны быть его значения нигде и никак не регламентируется.
А вообще все самые интересные вещи в React делаются через Context и React.Children.Map. Так же как во Vue они делаются путём ручной реализации render руками (без строкового шаблона).
Формально можно задействовать ещё 4-ый вариант, аля шаблоны vue:
<Comp1>
<slot>
<h1>Hello!</h1>
</slot>
</Comp1>
а внутри:
const content = getSlotContent(props.children);
return <div>{content}</div>;
где getSlotContent
будет анализировать vDom из children. Решение гхм… такое. На вкус и цвет, как говорится. Скорее костыль. Но очень похоже на vue-way (визуально) :)
По факту же, во Vue содержимое slot-а будет генерироваться там где будет применено, а не уровнем выше. И туда даже можно прокинуть какие-нибудь значения (см. scope). В react такое можно реализовать только за счёт явного применения методов (или передачи компоненты). Например:
<Comp1>
<slot>{(v1, v2) => v1 + v2}</slot>
</Comp1>
Т.к. JSX в этом плане более прямолинеен.
Лично я знаю только одного человека, который Дэна, скажем так, не любит. За то что он подорвал все устои, и все провереренные десятилетиями патерны в труху и не раз — github.com/gaperton.
Для меня Дэн всегда был больше не про «знания», а про «эмпатию». За что его все и любят. Но слепо верить одному человеку, тем более с достаточно узким (и коротким) опытом — странно.
Да, что-то вроде того. Основная масса слепо следует за Дэном и кричит те же лозунги. Однако если посмотреть на различные решения в экосистеме React, то становится понятно, что бунтарей тоже выше и крыши, и каких только решений ненапридумывали. React SPA это что-то вроде конструктора. Можно построить любую дичь, на какую способен. Я обычно так и делаю :)
А почему бы в решении со слотами не внедрять через props
определения компонентов вместо их экземпляров? А для тестирования можно использовать defaultProps
:
import Aside from "./Aside";
const Page = ({ Aside, ...props }) => (
<section>
<Aside />
<main>bla bla bla...</main>
</section>
);
Page.defaultProps = { Aside };
import Page from "./Page";
import AsideContainer from "./AsideContainer";
const PageContainer = props => <Page {...props} Aside={AsideContainer} />
Тогда презентационные компоненты действительно не зависят от контейнеров.
Как в примере автора компонент PageChrome
зависит от контейнеров? Подход с передачей React-элементов в слоты удобнее тем, что можно указывать пропы контейнеров (хоть контейнеры берут данные из стэйта, ownProps иногда требуются). При тестировании можно указать null
в качестве содержимого слота, поэтому defaultProps
не требуются.
P.S. React-элементы (например, <Aside />
) не являются экземплярами компонентов. Экземпляры компонентов появляются, когда React производит рендер.
React-элементы (например, <Aside />) не являются экземплярами компонентов.
Да, тут я запутался в терминологии. Тем более, что "экземпляр" SFC вообще не имеет смысла.
Как в примере автора компонент PageChrome зависит от контейнеров?
Оба примера решают поставленную задачу. Вопрос лишь в удобстве применения.
Подход с передачей React-элементов в слоты удобнее тем, что можно указывать пропы контейнеров.
Зато это лишает нас возможности передавать пропы из родительского dumb-компонента в дочерние. Простейший пример:
const ListView = ({ items, ListItem }) => (
<ul>
{items.map(item => (
<li key={item.id}>
<ListItem item={item} />
</li>
)}
</ul>
);
Как в данном случае мы будем передавать элемент ListItem?
Этот подход позволяет определять зависимости между компонентами опираясь на их интерфейсы, а не импортируя явно. ListView
зависит от свойства items
, описываемого типом MyItem[]
и от произвольного компонента ListItem
, имеющего тип (props: { item: MyItem }) => ReactNode
. Задать конкретную реализацию ListItem
мы должны либо в контейнере ListViewContainer
, либо в ListView.defaultProps
. Первый вариант мы можем использовать в конкретном приложении, а второй в тестах и storybook.
Так мы получаем полностью независимые, типизированные, тестируемые и переиспользуемые dumb-компоненты. С точки зрения старого доброго ООП это всего лишь Dependency Inversion и Decorator Pattern (наши контейнеры).
Согласен, что в вашем примере со списком подход со слотами не подойдёт. Но в статье нет таких примеров, список слотов везде детерминирован.
У вашего подхода есть недостаток: неудобно передавать пропы из контейнера в элементы списка. Можно использовать третий подход, чтобы передавать пропы из контейнера и dumb-компонента: функция в качестве значения пропа:
interface Props<Item> {
items: Item[];
renderItem(item: Item): React.ReactNode;
}
const ListView<Item extends { id: React.Key }> = ({ items, renderItem }: Props<Item>) => (
<ul>
{items.map(item => (
<li key={item.id}>
{renderItem(item)}
</li>
)}
</ul>
);
Он похож на предложенный вами (React-компоненты это по-сути функции), разница в том, что React не будет удалять и создавать пункты при каждом рендере при использовании такого кода:
render() {
return (
<ListView
items={/* ... */}
renderItem={item => <MyItem {...item} foo={this.props.foo} />}
/>
);
}
Разницу между подходами я вижу только в удобстве применения в тех или иных ситуациях. Все три подхода независимые, типизированные, тестируемые и переиспользуемые.
Из всех вариантов понравились только первое и последнее решения (мокать). Те которые подразумевают ровно 0 строк кода в реальной кодовой базе проекта, оставив все финты ушами в коде тестов и их окружения. Сам прибегаю к unit-тестам для react-компонент очень редко, предпочитая интеграционные. Ворочается это медленно (особенно ввиду того, что enzyme написан ногами), но даёт какую-то веру в реальную полезность этих тестов.
Юнит тесты это тестирование в изоляции от внешних зависимостей. Компонент компоненту — зависимость. shallow годная штука при наличии явной вложенности. Когда разработчику становится скучно и он начинает изобретать новые способы для связи компонентов, с тестами тоже приходится заморочиться.
shallow
отличное решение, потому что внешние зависимости, даже если и есть, просто не важны и не используются. Плохо то, что по принципам своей работы, shallow совсем не приспособлен к renderProp
, который используется все чаще и чаще.
Сам React, а точнее react-testing-tool и компания так не умеет, и всегда рендерит все от начала до конца. Тут уже вступают в дело моки (или Boundary), которые могут ограничить и изолировать тесты.
Но моки это сложная тема — мокать можно по разному и в разных местах. Для (очень) многих моки в тестах Реакта это fetch-mock
, который течет через пару слоев абстраций. Мокать можно только dependency, селекторы и ресолверы. В общем jest.mock, а еще лучше proxyquire, который позволяет мокать только прямые зависимости.
Проблема опять же в изоляции. В proxyquire
/jest
такого просто нет, в mockery
она работает как говно, и только в rewiremock
ее становится возможным использоваться, как и типизированные моки.
Избежать того, чтобы компоненты «умнели» можно с помощью прикидываемы их пропсами (явное использование) или слотами (чёрный ящик внутри чёрного ящика). При этом scope в vue slots — это злище.
Для меня неудобства с enzyme и shallow render начались когда я перешел от такого паттерна
class ItemsList extends React.Component {
renderItem(item) {
return <li>{item.name}</li>
}
render() {
return <ul>
{this.props.items.map(item => this.renderItem(item))}
</ul>
}
}
к вот такому
function Item({ item }) {
return <li>{item.name}</li>;
}
class ItemsList extends React.Component {
render() {
return (
<ul>
{this.props.items.map(item => (
<Item item={item} />
))}
</ul>
);
}
}
Разбиение на более мелкие компоненты упрощает рефакторинг, делает код более понятным, но усложняет тестирование, потому что shallow render не рендерит содержимое <Item />
. Нужно либо писать отдельные тесты для Item, либо использовать dive.
Мое ожидание от хороших тестов это то, что они не требуют к себе внимания и не ломаются после таких небольших рефакторингов. mount показывает здесь лучшие результаты.
Проблемы внутренних компонентов у меня решалсь моками: jest.mock('react-router-dom', () => ({Link: FakeLink}))
– и готово.
C проблемой "бесконечной" вложенности контейнеров не сталкивался, потому что не вижу смысла писать unit-тесты на верхнеуровневые конейнеры, типа DashboardPage. Проще протестировать блоки этой страницы по отдельности, которым нужно меньше моков, а потом написать несколько вебдрайверных тестов на страницу целиком.
Штука изолированная от внешнего мира — это компонент, а не модуль. Модуль — это по определению кусок исходного кода, изолированный от других кусков исходного кода.
Адепты Реакта предпочитают не замечать следующих косяков в его дизайне:
- Вычисляемые в момент передачи пропсы. Это не позволяет делать их вычисление ленивым и автоматически трекать зависимости компонента. В результате, чтобы обновить попсы компонента нужно перерендерить компонент уровнем выше. Ну и к прочим проблемам типа поднятия всего дерева, хотя нам в данном тесте интересны только 2 точки в нём.
- Берущиеся из локальной области видимости классы/функции компонент. Это не позволяет их подменять извне. Это нужно не столько для тестов, сколько для настройки сторонних/обобщённых компонент под себя/конкретное место использования.
Отсюда и все ваши проблемы — 100500 костылей, как сделать очередную базовую функциональность.
2. Ну это вроде как «локальность». Никогда проблем с этим не было.
А можно пример про то как можно лучше?
Ну вот пример из статьи с хромом:
$my_app $mol_ghost
Sub <= Chrome $my_chrome
Aside <= Menu $my_menu
Content <= Page $my_page
Транслируется в:
export class $my_app extends $mol_ghost {
/// Sub <= Chrome
Sub() {
return this.Chrome()
}
/// Chrome $my_chrome
/// Aside <= Menu
/// Content <= Page
@ $mol_mem
Chrome() {
return this.$.$my_chrome.make( obj => {
obj.Aside = () => this.Menu()
obj.Content = () => this.Page()
} )
}
/// Menu $my_menu
@ $mol_mem
Menu() {
return this.$.$my_menu.make()
}
/// Page $my_page
@ $mol_mem
Page() {
return this.$.$my_page.make()
}
}
Ни Aside, ни Page не будут созданы, пока Chrome к ним не обратится. Более того, когда пользователь свернёт Aside — тот будет вообще уничтожен и создан заново при разворачивании.
Ну это вроде как «локальность». Никогда проблем с этим не было.
Вся эта статья про эти проблемы, которых "никогда не было". Собственно, обратите внимание на код выше. Все классы получаются через this.$
. Это окружающий контекст, через который можно легко переопределить любой класс ниже по дереву. Например, мы зачем-то хотим протестировать вёрстку и нам нужно замочить динамические части:
$mol_test({ '$my_app renders to right html'( $ ) {
const app = $my_app.make({
$ : $.$mol_ambient({
$my_page : $my_mock ,
$my_menu : $my_mock ,
})
})
$mol_assert_like(
app.dom_tree() ,
<section id="$my_app.make().Chrome()" my_app_chrome my_chrome mol_view>
<aside id="$my_app.make().Chrome().Aside()" my_app_chrome_aside my_chrome_aside mol_view>
<my_mock id="$my_app.make().Menu()" my_app_menu my_mock mol_view />
</aside>
<my_mock id="$my_app.make().Page()" my_app_page my_mock mol_view />
</section>
)
} })
Конечные React Компоненты