Pull to refresh

Comments 30

Интересно, а реактоводы всегда делают то, что говорит Дэн? По крайней мере у меня возникает такое стойкое ощущение))) Это я про его правку к собственной статье.
Поверьте на слово — да! Я слежу за развитием реакта, Дэна, соощества с самого их появления. Кроме того, с самого начала я был с ними вомного не согласен. Я основывался на swing, javafx, wpf, mobile, разработка uix для игровых приложений. А все сообщество опиралось лишь на мнение Дэна. И это происходит до сих пор. При чем, если даже не обращать внимание, как он мнение меняет, то все ещё остается актуальной тема, почему он так медленно развивается. Почему он, имеющий такое влияние, до сих пор не сделал из react инструмент для разработчиков?! На фоне других инструментов и технологий, react досих пор выглядит как плацдарм для эксперементов Дэна. И самое ужасное, что все во что может переасти ui, уже давно описано и вместо того, чтобы включить мозг, все сообщество, так и полагаются на Дэна. Зная Дена, могу с уверенность сказать, что он назвал бы это «глупое или stateless сообщество». Правильных, устаявшихся названий он либо не знает, либо маркетинг fb не позволяет. И видимо на большее фантазии не хватает как у него, так и всего отдела маркетинга.

И да, в статье неточность. Слоты (slot), это render callback, а не то что описано в статье. Кроме того, не могу не заметить, что продвигаемая «фишка» в виде rander callback, жутнко нарушает предыдущий столп маркетинга реакта — декларативность jsx.

И да, чтобы кому-то не показалось что я реакто и Дэно ненавистник, могу сказать, что я много лет пишу на реакте и несмотря на то, что ещё столько же лет пишу на angular и vue, бросать как его, так и другие инструменты не собираюсь. Будет скучно. Но не критиковать нельзя. Мне кажется, что это силаспособная подтолкнуть процес и сделать разработку ещё более драйвовым.
Слоты они и в вебкомпонентах, ангуляре и vue — слоты.
И почему декларативность нарушается? Наоборот — в одной половине четко видно где что-то будет, а в другой видно что именно, без необходимости искать конкретное место подключения.
Слоты они и в вебкомпонентах, ангуляре и vue — слоты.

Согласен! Но Ваше представление о пользовательской логике слотов, отличается от реализвации пользовательской логики в описанных Вами же технологиях. В реакт, ближе всего по духу к слотам — render callback.

И почему декларативность нарушается?

Вам виднее…

И сравнивать юнит с интаграционными тестами и говорить что одни нужнее других, это свидетельство о нездоровом непонимании процесса разработки. Кроме того, DI и, в случаи с компонентами в качестве props, агрегация, это как небо и земля.
> В реакт, ближе всего по духу к слотам — render callback.

Специально проверил — render callback это старое имя render prop. Не слот.

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

Вводная часть статьи как раз про то, что одни нельзя заменить другими, и оба нужны в форме симбиоза имени Пирамиды Тестирования. Не в том плане что каждый следуйщий уровень уже и тормознее, а в том, что каждый следуйший уровень опирается на предыдущий.
Согласен! Но Ваше представление о пользовательской логике слотов, отличается от реализвации пользовательской логики в описанных Вами же технологиях. В реакт, ближе всего по духу к слотам — render callback.

В React экосистеме это слово просто не в ходу. Но, к примеру, используется во Vue экосистеме. И в React можно реализовать тоже самое за счёт:


  • передачи компонента как prop
  • передачи метода, который отрендерит, что надо как prop

Если вдуматься, то п2. это особая разновидность п1.

Да, я был не прав. Просто если подумать, то говоря о современных ui компонентах и о декларативности, в голове рисуется шаблонный синтаксис представлений (html, jsx, шаблоны angular). И первое что нарисовалось в голове при ассоциации с slot в react, это render callback. На мой взгляд, по синтаксису он наиболее схож со slot в шаблонах. Передача компонента как props вообще не похожа на слоты в других инструментах. Но я сразу не подумал, что слотов может быть сколько угодно, а render callback, только один. Поэтому в реакт единственный вариант повторить функционал слотов, это props или render props. Но это не делает props или render prop слотом. Это просто передача компонента как пропса. Понятие слот тогда вообще не применимо к реакту.
И передав компонент как пропс, функционал получится тот же, но удовольствие уже испортится. На дворе 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 подобные штуки вы делаете вручную согласно вашим воззрениям на архитектуру проекта.

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

А если так?


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} />

Тогда презентационные компоненты действительно не зависят от контейнеров.

Отличная идея. Но TypeScript, который я очень люблю, не очень любит defaultProps.

Вообще-то с версии 3.0 очень даже любит (по крайней мере для классов).
А для SFC можно сделать и такой финт ушами:


type Props = Partial<typeof Page.defaultProps> & {
  foo: number;
  bar: string;
};

const Page = ({ foo, bar, Aside }: Props) => (
  // ...
);

Page.defaultProps = { Aside }; 

Как в примере автора компонент 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 ее становится возможным использоваться, как и типизированные моки.

Тестировать юнит-тестами что-то, кроме pure-компонентов довольно странно. Второе исправление Дэна не верно в корне. Для внешнего наблюдателя «компонент» — это всегда чёрный ящик, а значит все поддерево компонентов эквивалентно ему самому. В этом смысле, если поддерево якобы «тупого» компонента имеет явную зависимость на «умный» компонент, то это автоматом делает все поддерево «умным» и для внешнего наблюдателя, корневой компонент поддерева уже не может считаться «тупым».

Избежать того, чтобы компоненты «умнели» можно с помощью прикидываемы их пропсами (явное использование) или слотами (чёрный ящик внутри чёрного ящика). При этом 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. Проще протестировать блоки этой страницы по отдельности, которым нужно меньше моков, а потом написать несколько вебдрайверных тестов на страницу целиком.

Штука изолированная от внешнего мира — это компонент, а не модуль. Модуль — это по определению кусок исходного кода, изолированный от других кусков исходного кода.


Адепты Реакта предпочитают не замечать следующих косяков в его дизайне:


  1. Вычисляемые в момент передачи пропсы. Это не позволяет делать их вычисление ленивым и автоматически трекать зависимости компонента. В результате, чтобы обновить попсы компонента нужно перерендерить компонент уровнем выше. Ну и к прочим проблемам типа поднятия всего дерева, хотя нам в данном тесте интересны только 2 точки в нём.
  2. Берущиеся из локальной области видимости классы/функции компонент. Это не позволяет их подменять извне. Это нужно не столько для тестов, сколько для настройки сторонних/обобщённых компонент под себя/конкретное место использования.

Отсюда и все ваши проблемы — 100500 костылей, как сделать очередную базовую функциональность.

1. А можно пример про то как можно лучше?
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>
    )

} })
Вы открыли ящик Пандоры, но пока не поняли этого…
Sign up to leave a comment.

Articles