Pull to refresh

Comments 50

одному мне кажется очень странным удаление хуков componentWillMount & componentWillUpdate ?
Если обновление state на основе props вынесено в getDerivedStateFromProps, то эти хуки теперь избыточны:
  • Вместо componentWillMount — для сайд-эффектов более оптимальным вариантом будет componentDidMount, а для работы со state и props можно использовать конструктор
  • Вместо componentWillUpdate — для сайд-эффектов предлагается componentDidUpdate, а для работы со state нам дают getDerivedStateFromProps
просто реакт заявляет об обратной совместимости и «мы не такие как ангулар», но при этом переход с 15+ на 16 для основной массы готовых библиотек и компонентов занял пол года, это при том что там впринципе не было ничего сверхестественного только Props вынесли и еще по мелочи (я говорю с точки зрения совместимости старого кода, а не про новый Fiber), а тут изменение API которому 100 лет в обед и на которое, я уверен, завязано достаточно много логики в старом коде, и мне кажется, обновляться они будут еще дольше + не будет возможности обновить библиотеку компонентов на последнюю версию, сохранив старую версию реакта.

Объединение componentWillMount и componentWillReceiveProps под одним getDerivedStateFromProps имеет смысл, что бы убрать дублирование кода.

Если говорить без привязки к фреймворкам наличие у компонента
хуков
— init

— willGetParams
— shouldChange?
— willMount/willUpdate
****** render
— didUpdate

— willUnmount
добавляют гибкости. Даже в ангуларе они появились.

P.S. вобщем у меня смешаные чувства по поводу грядущего обновления, и мне кажется что он принесет больше пробем чем пользы.
getDerivedStateFromProps не имеет доступа к this, соответственно не может обновлять state, он вроде для определения перерендерить компонент или отменить рендер
Вообще-то, обновлять state — его основная функция. Только делает это он возвращая значение, а не через setState.

А определением перерендерить или отменить рендер занимается shouldComponentUpdate же…

А для чего вы ими пользовались?


Для большинства операций больше пододят did-хуки, то есть componentDidMount и componentDidUpdate. Если нужно предотвратить обновление, то есть shouldComponentUpdate.
will-версии и так не рекомендовались к использованию.

Не кисло так за 4 года 16 версия, они хоть немного совместимы между собой?

Вообще-то они после версии 0.14 выпустили версию 15 вследствие перехода на semver
Вас не удивляет, что Хром за неполных десять лет добрался до версии 64?
Новый контекст теперь работает только в рендере? Если да, то это редкостная кривизна. Тот же MobX'овый Inject как теперь должен работать?

Суть в том что теперь есть обёртка в виде Consumer. А данные из контекста передаются дальше в виде props.

В виде каких пропс? Вы видели пример? Они передаются в виде аргументов функции-как-потомка. В пропс ничего не передается и из других методов класса контекст не получить

Что-то мешает взять пропсы и передать дочернему компоненту?

Изобретаем HOC? Мешает то, что получаем ещё 1 уровень вложенности, который усложняет и debugging, и несущественно, но всё же влияет на производительность. У нас и так в сложном приложении повсюду возможны матрёшки из HOC, а так матрёшками нужно покрывать компоненты, которые используют context. Добавить к этому какие-нибудь styled-components, где матрёшкой обёрнут каждый тег со стилями, и получается, что древо компонент почти целиком состоит из композитных матрёшек. С точки зрения приложения — ок, работает же. С остальных точек зрения — спорно, очень спорно.

Что значит «изобретаем»? Декоратор inject в mobx-react — это уже HOC, независимо от того нравится вам это или нет.

А я про inject в mobx-react и connect в redux-react и не заикался. Это изначально сильно impure-вещи, которые идут как большой компромисс между идеализмом и практикой. Я скорее про тот context, что разработчики по своей воле сами используют в сложных проектах, устав от props-hell. Ниже отписал подробнее.

Зато TheShock именно про него и говорил, а я ему отвечал.
Изобретаем HOC? Мешает то, что получаем ещё 1 уровень вложенности, который усложняет и debugging, и несущественно, но всё же влияет на производительность. У нас и так в сложном приложении повсюду возможны матрёшки из HOC, а так матрёшками нужно покрывать компоненты, которые используют context. Добавить к этому какие-нибудь styled-components, где матрёшкой обёрнут каждый тег со стилями, и получается, что древо компонент почти целиком состоит из композитных матрёшек. С точки зрения приложения — ок, работает же. С остальных точек зрения — спорно, очень спорно..

Используйте для HOC наследование вместо композиции и никаких новых уровней вложенности добавляться не будет

Вы знаете, я бы с радостью. Но вот не совсем понимаю, как его тут применить. Судя по тем обрывкам, что я увидел касательно нового API, он работает именно в рамках .render-метода. Ну и по сути асинхронно. Не подскажите рецепт? В голову приходят только разного рода извращения, работающие с задержкой.

Да просто использовать те же хокки но только не в виде


function hoc(TargetCmponent){
  return class extends React.Component {
   render(){
     return <TargetCmponent {...this.props}/>
   }
 }
}

а в виде


function hoc(target){
  const isTargetClass = target instanceof React.Component
  return class extends (isTargetClass ? target : React.Component) {
    render(){
        return isTargetClass ? super.render() : target(this.props) 
    }
  }
}

и все, больше никаких вложенностей и глубоких матрешек из компонентов-оберток — сколько бы хокков мы бы не навесили на компонент — да хоть тысячу — в рантайме все равно будет создан только один компонент что будет быстрее, так как вызов рендера не создаст тысячу объектов-элементов виртуального дума для этих врапперов, и реакту нужно будет выполнить diff одного компонента а не тысячи

Вы, вероятно, имели ввиду примерно это:


function hoc(Target, key)
{
    const isTargetClass = target instanceof React.Component;
    const Parent = isTargetClass ? Target : React.Component;
    return class extends Parent 
    {
        render()
        {
            return <Consumer>{val => 
            {
                const props = {...this.props, [key]: val };
                return isTargetClass 
                    ? super.render(props) 
                    : target(props);
            }}</Consumer>
        }
    };
}

Однако в данном случае мы располагаем нужным нам context-полем только внутри super.render. А сложные stateFull компоненты хотят его в других своих методах, включая, возможно, всякие хуки. В теории можно и это обойти, к примеру, задействовав setState. Но тогда мы должны писать компонент из рассчёта на двойной render при инициализации, и, возможно, при одновременной смене чего-нибудь в props и в context (но это уже редкий use case).


Идём дальше. Что если нам нужно несколько полей, а не одно? Нужно городить уже достаточно сложную обвязку. Всё выполнимо, но от этого кода сильно несёт… каким-то альтернативным react-ом.

Ну да, с новым контекстом реакта доступ возможен только внутри render-метода. То что этот контекст не будет доступен в разных хуках это проблемы нового контекста. А вообще, если нужно связывать несколько полей, то зачем нужен контекст — не лучше ли использовать внешнее состояние? Я выше привел вариант hoc-ов когда вместо композиции мы используем наследование и избегаем компонентов-оберток в рантайме что позволит улучшить производительность и каких-то минусов по сравнению композиционными hoc-ами я не вижу
А вообще, если нужно связывать несколько полей, то зачем нужен контекст — не лучше ли использовать внешнее состояние?

Вы имеете внешнее хранилище? Ну вот я использую redux. Но там данные приложения. А вещи которые я пробрасываю через context "руками" другого характера. Сложно объяснить. Ну кроме одной — i18n, по сути переводы и инструментарий к ним.


Да можно написать свой внешний context, по принципу того же redux-react-а. Но это что-то вроде из стрельбы пушки по воробьям. Серьёзное архитектурное решение.


Старый context при всей своей уродливости был в чём-то изящен. Идеологически. Предполагалось, что туда помещают либо совсем что-то impure, и тогда сами ковыряйтесь в этом как хотите (react-redux скажем), либо что-то статическое, и тогда вся машинерия по re-render-у уже не нужна, а вот от килотонн бойлерплейта избавляет.


А новый контекст гораздо больше напоминает прицельные порталы для изменяемых значений. Теперь наследник окольными путями (скажем через import-export) узнаёт что-то от родителя (react в этом совсем не помогает), и позволяет от этого асинхронным образом в render методе зависеть. Вероятно, это выглядит достаточно изящно, при решении каких-то задач. Не знаю.


По поводу внешнего хранилища. Тут ещё встаёт вопрос реализации. Скажем redux-react очень уязвим в вопросе количества подписчиков. Скажем если у вас приложение рендерит сотни или тысячи мелких компонент, то подписываясь в каждом из них на store, вы заставляете connect вычислять mapStateToProps на любой чих (коих может быть очень много). В лучшем случае это просто будет жрать батарейку, в худшем ещё и тормозить.

Ну контекст это по сути просто объект в который помечаются общие для какого-то поддерева компонентов данные. Если данные контекста не зависят от того в каком поддереве находится компонент например i18n и другие данные то мы можем просто вынести этот контекст во внешний объект в отдельном файле выполнить заимпортить этот объект и обращаться к нему напрямую. Если контекст у разных поддеревьях компонентов разный то всегда можно выразить эту сутуацию не в через компоненты а через данные.


По поводу внешнего хранилища. Тут ещё встаёт вопрос реализации. Скажем redux-react очень уязвим в вопросе количества подписчиков. Скажем если у вас приложение рендерит сотни или тысячи мелких компонент, то подписываясь в каждом из них на store, вы заставляете connect вычислять mapStateToProps на любой чих (коих может быть очень много). В лучшем случае это просто будет жрать батарейку, в худшем ещё и тормозить.

O, вы обратились по адресу — в статьях тут, тут и тут я разбирал эту проблему и ее решение в виде mobx

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

В случае MobX ― может быть. В случае redux это как-то дорого обходится.

А старая концепция context-а, предполагала, что в нём лежат статичные данные, нечто вроде констант, которые не требуется передавать явным образом. Естественно я не имею ввиду redux и прочие динамичные штуки, а скорее какой-нибудь i18n и ему подобные вещи. Это касается в первую очередь сложных приложений, где такой подход работы с контекстном позволяет избежать много-много-многочисленных пробрасываний каких-то обобщённых props по всему большой вложенности древу вниз.


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

А i18n и подобные вещи — это тоже вполне себе изменяемые данные, пусть и нечасто изменяемые.

Ок, допустим. На самом верхнем уровне, где лежит какой-нибудь <i18n-provider/> можно просто в случае "динамики" (например смены языка) проставить новый key и, хотя мы разово перетрясём всё vdom-древо, мы добьёмся нужных результатов. При том, что на кнопку хорошо если хоть кто-нибудь нажмёт. Мне такой подход показался разумным компромиссом. А теперь же для обеспечения того же функционала, нужно (в моих проектах) покрывать HOC каждый 3-й компонент. Contex позволял обойтись малыми жертвами. А случае использования наследования давался практически бесплатно.

Хм. Отдельный момент эти сами .consumer-ы. Если я ничего не напутал, то их нужно передавать по ссылке. Т.е. самым явным образом. Скажем при помощи import-export-а, а это, в свою очередь, усложняет жизнь, не просто лишним бойлерплейтом, а ещё и тем, что если у нас один parent-component, мог порождать несколько вариаций context-значений, исходя из условий, то теперь надо будет как-то самостоятельно разграничивать их своими силами, и react нам в этом больше не помощник.


Нет, я конечно ожидал, что context пересмотрят, но чтобы настолько… необычно. Этого вот я не ждал. Мне даже сложно назвать это контекстом. Скорее порталом :)

Что-то мешает взять пропсы и передать дочернему компоненту?

Вы понимаете зачем нужен контекст? Чтобы не пользоваться пропсами, для того, чтобы гонять по дочерним компонентам всякие вещи типа сторов. По сути, это единственный способ нормального DI в Реакте.

А вот вы мне предлагаете, если я хочу воспользоваться контекстом, чтобы не использовать пропсы — использовать пропсы? Тогда зачем вообще нужен такой костыльный контекст, если нужно просто передавать во все компоненты в пропсы еще и контекст вручную?

А что случилось-то? Ну делал он раньше var additionalProps = grabStoresFn(this.context.mobxStores || {}, newProps, this.context) || {}, а теперь будет делать


<MobxStores.Consumer>
    {(mobxStores) => {
        var additionalProps = grabStoresFn(mobxStores, newProps)
        // ...
        return createElement(component, newProps)
    }}
</MobxStores.Consumer>

Или вы про то, что третий аргумент неоткуда взять теперь? А его вообще кто-то использует? Всегда же он был в состоянии "недодокументирован"...

А случилось то, что раньше я мог в любом методе класса обратиться к контексту, а сейчас могу обратиться только в части метода рендер. При чем тут третий аргумент вообще?
Вы спрашивали как должен работать inject из mobx-react. Я ответил как.
Всмысле «ответили»? Вы совершенно не ответили, как inject будет работать так, чтобы я мог из всех методов пользоваться зависимостями.

В таком случае я не понимаю как вы использовали inject до этого… Документация говорит что его надо использовать вот так:


@inject("color") @observer
class Button extends React.Component {
  render() {
    return (
      <button style={{background: this.props.color}}>
        {this.props.children}
      </button>
    );
  }
}

То есть зависимости попадают в props, откуда их можно достать в любом методе.

Вот именно что «в любом методе».

То есть я могу использовать его так:

@inject("counter") @observer
class MyComponent extends Component<{ id: number, counter: CounterStore }> {
  onButtonClick() {
    const { id, counter } = this.props;
	
    counter.get(id).increaseAction();
  }

  render() {
    const { id, counter } = this.props;
  
    return (
      <CoolButton onClick={this.onButtonClick}>
        {counter.get(id).value}
      </CoolButton>
    );
  }
}


Видите? В любом методе класса, а не только в части метода рендер.
Раньше inject передавал вам в props тот counter который он достал из контекста в рендере. Теперь inject будет передавать вам в props тот counter который достал Consumer в рендере.

Что поменялось-то?

PS Вы вообще в исходники mobx-react заглядывали? Посмотрите как внутри inject устроен: github.com/mobxjs/mobx-react/blob/master/src/inject.js#L45

Туда Consumer идеально зайдет…
Посмотрите как внутри inject устроен

Хм, вероятно вы правы, а я — нет. Я заглядывал, но давно. Странно, мне казалось, что он иначе устроен. Последнее время менялся подход?
Извините, конечно, может что-то не так понял, но как достучаться до this.state в getDerivedStateFromProps?! this же недоступен…
Тупикнул, спасибо (-:

До this.state там дотягиваться и не нужно, т.к. он передаётся как параметр метода. А вот до всех остальных полей компонента уже по-нормальному никак. Довольно странное решение, если учесть, в документации открытым текстом сказано, что не стоит помещать в .state поле ничего, что не участвует в .render, мол помещайте "как есть" в this.%property%. Поместили. А теперь туда доступа нет :)

Да, уже понял про второй параметр, спасибо… хотя…
Как сравнить this.props и nextProps? Вернее, где теперь сравнить props, пришедшие, например, из redux??!
ну, для помещения чего-то в this.%property%, я так понимаю, оставили componentDidUpdate… (если это действительно так необходимо)

Кстати да. В большинстве не сильно хитрых случаев componentDidUpdate вполне сгодится. Правда вот порылся в своём коде и увидел что у меня есть методы componentWillReceiveProps, которые оперируют offsetWidth и offsetHeight от <SVG/> для формирования нового state. Теперь чтобы иметь к ним доступ нужно этот DOMElement толкать в state. В раздумьях. Насколько здравая идея толкать в .state DOM-элементы, а не данные. Логика подсказывает, что ничего криминального.

Как минимум — лишний рендер (либо WTF-логика в shouldComponentUpdate), ведь DOM-элемент можно получить только после рендера, а изменение state вызывает рендер.
ведь DOM-элемент можно получить только после рендера

Дак и componentWillReceiveProps тоже вызывается уже после первого render-а. А касательно того, что DOMElement раньше не получить ― что есть, то есть :) Так уж React устроен.

Интересный момент есть, связанный с <Provider/>-ми. Представьте, что вы maintainer redux-а. И вам нужно переехать на новый API. Вы, естественно не хотите, чтобы пользователи из-за обновления стали переписывать все свои container-а. Но что делать? Ведь если мы будем порождать новый Context.{Provider|Consumer} для каждого store-а, а внутри реализации connect-обёртки мы не будем иметь доступ к соответствующему <Consumer/>, и всё накроется медным тазом. Нужно будет пробрасывать store повсеместно. Т.е. переписывать все контейнера. Да и переиспользуемость компонент накроется медным тазом тоже. Они будут явным образом зашиты на один конкретный store.


Оказалось, что всё намного проще. redux-react может позволить себе один единственный <Provider/> на всю свою библиотеку, сколько бы store-ов у вас не было. И createContext будет вызван именно внутри библиотеки. Всё дело в том, что один и тот же <Provider/> можно вкладывать в себя же самого сколько душе угодно, и всякий раз при этом менять значение. А каждый <Consumer/> будет иметь доступ только к тому значению, которое ближайшее к нему по древу.


Т.е. при первоначальном взгляде на <Provider/> он напоминает телепорт значения сверху внизу, минуя props. На деле же каждый ReactElement порождённый <Provider/>-классом является самостоятельной сущностью со своим собственным значением. Указав во внутреннем <Provider/>-е другое значение, мы значение верхнего не изменим, и вообще ничего не сломается. Они не будут иметь с друг другом ничего общего. Хотя ссылки на <Provider/>-ы и <Cosumer/>-ы будут ===.

Ну это как бы было очевидно…

Мне нет. Даже в голову не пришло. Более того я уже столько материалов на эту тему нарыл, столько перечитал. Перерыл кучу обсуждений на github-е, RFC посмотрел и все доводы за и против, мотивы решения. Даже стал песочницу собирать. А потом случайно в комментариях на Medium наткнулся на вопрос про вложенность и даже ссылку на рабочий sandbox. И только поковыряв его до меня дошло, как на деле я буду его использовать (несколько вложенных instance от одного и того же Provider-а в одном древе).

Sign up to leave a comment.