Pull to refresh

Comments 72

image


Простейший всё же выглядел бы так:


@Observer
export class Counter extends Component {

    @observable counter = 0

    render() { return <>
        <h2>Counter: {this.counter}</h2>
        <button onclick={ ()=> this.counter ++ }>+1</button>
        <button onclick={ ()=> this.counter -- }>-1</button>
    </>}

}
что за за «умники» заминусовали? Наверное те, которые застряли в своем мирке и даже не знают что такое MobX

Использование дополнительной библиотеки – это уже не простейший пример по определению (минусы не мои).

Только со временем бывает понятно- это доп библиотека или будущий дизайн языка, принятый сообществом. Js как вы понимаете, эксперимент-размышление о будущем.

Декораторы еще даже не JS, это экспериментальный синтаксис, хоть и с большой перспективой войти в стандарт. И да, примеры все-таки на чистом React.

Вы, конечно же, говорите про переписывание компонент с нуля, используя на этот раз react-hooks, так как lifecycle-hooks идут в топку?

Ничего в топку не идет, не нужно перегибать. Документация так и говорит:


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

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

А вот с декораторами такой номер не пройдет. Это часть языка, просто так переключаться между API не получится.


В феврале этого года выпустили новую версию стандарта, совсем не совместимую с тем что было раньше и поддерживается в typescript или babel.


Определение декоратора в новом стандарте:


export decorator @bound {
  @initialize((instance, name) => instance[name] = instance[name].bind(instance))
}

раньше декоратор был просто функцией


function bound(instance, name, descriptor) {
  instance[name] = instance[name].bind(instance);
}

Как вы собираетесь переиспользовать существующий код в такой ситуации?

Речь про хуки, а не про классы: https://ru.reactjs.org/docs/react-component.html#unsafe_componentwillmount
Впрочем, всё идёт к тому, что и классы вскоре задепрекейтят.


Ну и по секрету вам скажу, что typescript — это не javascript, а лишь немного похожий на него язык. Ему ничего не мешает поддерживать оба синтаксиса.

Из-за одного deprecated метода переписывать все целиком нерационально. Хуки здесь не причём


Ему ничего не мешает поддерживать оба синтаксиса

Я ваш оптимизм не разделяю. В Babel уже есть переключатель между двумя ныне устаревшими версиями, новая редакция добавит ещё один режим. Как писать в таких условиях переиспользуемые библиотеки — категорически непонятно

Легко, Babel и TS будут поддерживать и тот и другой вариант. Это же очевидно.

А для меня вот совсем неочевидно. Есть конструкция:


@observer
class Example {}

Это в новый синтаксис транспилировать или в старый?

Без разницы, результат остается прежним, говоришь TS или Babel в какую версию JS код превратить и он это делает, а не вы собственноручно)

Хорошо, спрошу по-другому. Результат транспиляции зависит от того, какой версии стандарта придерживается @observer. Typescript эту информацию из типов как-то достанет, а Babel как будет? Он работает с файлами поодиночке, в импорты не смотрит

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

С 2015 года вы пользуетесь legacy реализацией, которую из стандарта выпилили. А для актуальной спецификации транспилятор пока никто не написал по причине указанных выше проблем.

Не вижу причин не использовать legacy декораторы по сей день, они ещё будут очень долго жить.

Тут в комнату входит Svelte и говорит:


<script>
  let counter = 0;
</script>

<h2>Counter: {counter}</h2>
<button on:click={() => ++counter}>+1</button>
<button on:click={() => --counter}>-1</button>

Если использовать дополнительные библиотеки, то использовать по максимуму.

UFO just landed and posted this here

Тут все-таки иллюстрируется другая проблема. Зачем усложнять код в примере?


В боевом коде будет что-то типа:


const dismounted = useDismounted();

useEffect(() => {
  entitiesApi.getById(entityId)
    .pipe(takeUntil(dismounted))
    .subscribe(setData, toasts.handleApiError('Entity load'));
}, [entityId])
UFO just landed and posted this here
UFO just landed and posted this here

Хм, да, тогда у меня будет что-то типа (это, кстати, приводит к отмене запроса через AbortController):


const dismounted = useDismounted();
const loadRef = useRef();

useEffect(() => {
   if (loadRef.current) {
      loadRef.current.unsubscribe();
   }
   loadRef.current = entitiesApi.getById(entityId)
      .pipe(takeUntil(dismounted))
      .subscribe(setData);
}, [entityId])

Но для большинства случаев такая перестраховка лишняя, entityId скорее всего придет откуда-нибудь из роута и вряд ли будет очень быстро изменятся, если вообще будет.

UFO just landed and posted this here
UFO just landed and posted this here

dismounted это Subject из RxJs, при закрытии которого произойдет автоматическая отписка. А закрывается он внутри кастомного хука useDismounted автоматически при размонтировании компонента.


Приемущество моего подхода с useDismounted в том, что он позволит отписаться от всех Observables разом, не отписываясь отдельно от каждого.

UFO just landed and posted this here

А вы используете react и rxjs в проде?

Да, rxjs удобен, позволяет делать куда больше и гибче, чем промисы или async-await. По крайней мере на мой взгляд)
Например — отменять запросы.

Плюсы мне понятны. Я скорее в том плане, что в ng он идёт из коробки. Интересно, что и в react его используют

Я перебежчик из мира ng, некоторые концепции крепко засели в голове.

Один из самых удобных вариантов использования в реакте — redux-obervable, для асинхорщины и бизнес логики, в частности.

ИМХО, такое необходимо прямо в статье приводить, хотя бы в спойлере. По примеру из статьи совершенно не очевидно, что он рафинирован до полной неприменимости в реальной жизни.

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

В руководстве react тоже все примеры очень простые. Кажется, для документации и обучающих материалов это норма.

Вообще, я планировал рассказать именно о проблемах и крутостях хуков. Но вводный ликбез занял 70% времени доклада, а сама суть/цель доклада прошла слишком лаконично, тезисно. Кто сталкивался, тот боль поймет. Кто еще нет - вряд ли оценит.

А упрощённый - без спойлера.

И самое последнее — в подходе к написанию компонентов с хуками, наверное, заложена какая-то философия.

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

useState — это скорее аналог статических переменных (не полей класса) в си-подобных языках. К DI оно мало отношения имеет.


useEffect — это скорее событие onRender, чем реактивность. Для реактивности его приспособить, конечно, можно, но смысла в этом мало.

useState — это скорее аналог статических переменных (не полей класса) в си-подобных языках.

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

С такой логикой любой стор является DI. Логически это одно и то же:


const counter = useState(0)

// object field
counter = new Store(0)

// object field
counter = 0

Собственно из-за невозможности настройки, это и никакой не DI.

С такой логикой любой стор является DI. Логически это одно и то же:

Чтобы стор (или вообще любая зависимость) стал DI — с-но, он должна быть внедрен. Т.е., как только вы этот стор не сами выбираете, а устанавливаете магией или на стороне пользователя — тогда DI и появляется. Ваш код выше совсем не эквивалентен (и не может быть эквивалентен) логически тому, что реально происходит в реакте, т.к. useState — это фабричный метод, который резолвит зависимость для конкретной компоненты. Да, способ резолва вы настроить не можете — но это и не является необходимым. DI состоит в самом факте делегирования резолва стороннему модулю.

С такой логикой new тоже резолвит зависимость для каждого компонента, а Store — фабричный метод, какой объект он вернёт вы тоже знать не можете.

С такой логикой new тоже резолвит зависимость для каждого компонента

Нет, не резолвит.


а Store — фабричный метод, какой объект он вернёт вы тоже знать не можете.

В смысле не знаю? Знаю. Но дело не в том, знаю или нет. В случае стора это один конкретный объект, который вы и создали и четко определили. Вы на стороне клиента говорите: "дай мне этот конкретный объект", и вам его дают. Это не DI, т.к. ответственность на клиенте, клиент полностью определяет результат.
А вот когда вы на стороне клиента говорите: "дай-ка мне какой-нибудь подходящий объект", и уже какая-то внешняя логика решает, какой объект будет "подходящим" в данной ситуации для данного клиента — тогда у вас DI.

Вызов useState даёт вам именно что конкретный объект.

Нет, оно решает совершенно другие задачи.


DI — это про передачу внешних зависимостей, useState — про хранение внутреннего состояния компонента.

DI — это про передачу внешних зависимостей, useState — про хранение внутреннего состояния компонента.

Не так. Хранение состояния — это задача, а DI — способ реализации. Такой задачи как "передача внешних зависимостей" никогда на практике не стоит — нам не надо передавать зависимости в принципе, нам надо какие-то конкретные. И передавать их надо за тем, чтобы решить, с-но, задачу. Т.е. передача зависимостей — не задача, это решение.


В данном случае — мы храним состояние в некотором внешнем объекте, который инкапсулирует это состояние, а так же его геттеры/сеттеры. Этот объект неявно создается и неявно менеджится (т.е. мы сами его не создаем, и получить его сами не можем, если не залезем в потроха фреймворка). Нам внутри компонента нужен этот объект, т.е. это зависимость, и магия реакта эту зависимость создает и потом инжектит на последующих вызовах render.
Задача — взаимодействие с состоянием внешнего объекта, способ реализации — внедрение зависимости (этого внешнего объекта).


Вызов useState даёт вам именно что конкретный объект.

Какой "конкретный"? Вызывая useState, вы делегируете создание и дальнейший резолв объекта реакту, уже он определяет, какой объект вам выдать, вы на это не влияете никак.
Мне кажется, вы не совсем понимаете как работает useState.

Такой задачи как "передача внешних зависимостей" никогда на практике не стоит

С точки зрения архитектуры, такая задача вполне возможна. Можете считать её мета-задачей, если просто "задача" для вас слишком привязана к чему-то конкретному.


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

Если этот объект создаётся нами по нашему же запросу и нами же используется — это не внешний объект, а внутренний.

С точки зрения архитектуры, такая задача вполне возможна. Можете считать её мета-задачей, если просто "задача" для вас слишком привязана к чему-то конкретному.

Ну так вот у вас "задача" — работа с внешним стейтом, а решение — приводит к "мета-задаче" внедрения этого стейта как зависимости.


Если этот объект создаётся нами по нашему же запросу и нами же используется — это не внешний объект, а внутренний.

С DI объекты всегда внедряются по запросу клиента и используются потом клиентом (ну, за тем клиент эти объекты и запрашивает, чтобы их использовать). При этом, заметьте — "внедряются", а не "создаются". И при вызове useState, вы стейт не создаете — вы его внедряете (т.е. просите у useState предоставить объект). А уже сам useState решает — что ему создавать, как создавать, и создавать ли вообще. useState решает, не вы.


Внутренний объект — это объект, который клиентом создается и за пределы клиента не выходит. Если же объект создается не клиентом, а сторонним сервисом, существует за пределами клиента и менеджится тоже сторонним сервисом, к клиенту никак не относящимся — это объект, определенно, внешний.


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


Но вы не можете создать этот объект руками, не можете повлиять на его конструкцию, и не можете вмешаться в процесс резолва (ну, опять же, не залезая куда-то в потроха).

Ну так вот у вас "задача" — работа с внешним стейтом, а решение — приводит к "мета-задаче" внедрения этого стейта как зависимости.

Работа с внешним стейтом возможна при помощи useContext, useSelector, useObservable или обсуждаемого выше решения для rx, причем только первые два способа внедряют стейт как зависимость.


А useState — это внутренний стейт.


С DI объекты всегда создаются по запросу клиента

Нет


Внутренний объект — это объект, который клиентом создается и за пределы клиента не выходит.

А куда выходит результат setState, кроме инфраструктурного кода?

А useState — это внутренний стейт.

Вы не понимаете, как работает useState.


Нет

Да. Исключений нет. У нас есть три общепринятых способа DI — через конструктор, через поле и через сервис-локатор/фабричный метод. Во всех трех случаях зависимость, очевидно, запрашивается именно клиентом. Вы знаете какие-то еще способы? Вообще, если клиент зависимость не запрашивает — значит, он ее не использует. Ну, т.е., вы можете, формально, что-то всунуть, что клиент не просил, но это что-то будет просто висеть и ничего не делать.


А куда выходит результат setState, кроме инфраструктурного кода?

Не понял, к чему вы это. Какая разница, куда выходит результат setState (useState, в смысле)?

Вы не понимаете, как работает useState.

А пофигу как оно работает внутри, важно лишь наблюдаемое поведение.


Рассмотрим два компонента:


function Foo() {
    const [ count, setCount ] = useState(0);

    return <div>
        <span>count = {count}</span>
        <button onClick={() => setCount(count+1)}>Increment</button>
    </div>;
}

function Bar({count, setCount}) {
    return <div>
        <span>count = {count}</span>
        <button onClick={() => setCount(count+1)}>Increment</button>
    </div>;
}

Так вот, у Foo внутреннее состояние, а у Bar — внешнее. Эти термины были придуманы специально для того, чтобы отличать Foo от Bar.


Во всех трех случаях зависимость, очевидно, запрашивается именно клиентом.

Во всех трёх случаях зависимость клиенту интересна независимо от её уникальности или времени жизни (важно лишь чтобы она не жила меньше чем сам клиент).


В случае с useState это не так.

Краткое содержание треда: Druu пытается натянуть концепции DI на useState, а потом удивляется, что ж все так неудобно работает

А пофигу как оно работает внутри, важно лишь наблюдаемое поведение.

Я про него и говорю. useState вам отдает внешнюю зависимость.


Так вот, у Foo внутреннее состояние, а у Bar — внешнее.

Что? Как раз нет. Вы просто продемонстрировали два разных способа DI. С-но, никаких различий, кроме удобства, в этих способах нет, они эквивалентны по тому самому наблюдаемому поведению. Просто внедрение по аргументу в реакте неудобно (придется этот аргумент везде таскать).


Вообще, не поделитесь своей логикой? Каким образом вы назвали внутренней зависимость, которую достали из внешнего глобального объекта?


Внутренней зависимость будет, если вы ее создадите внутри компонента. Если же зависимость создает неизвестная и неконтролируемая вами магия в произвольный (вам неизвестный) момент времени — чего тут внутреннего? За внутреннюю зависимость отвечает сам клиент. В данном же случае клиент за зависимость не отвечает и никак ей не управляет. Она существует полностью независимо от клиента и связана с ним, с-но, только через внешний сервис (useState). Вы же не станете утверждать, что сервис useState является тоже внутренним?


Во всех трёх случаях зависимость клиенту интересна независимо от её уникальности или времени жизни

В DI клиенту как раз очень часто важна и уникальность и время жизни.


В случае с useState это не так.

Не так что?

Если же зависимость создает неизвестная и неконтролируемая вами магия в произвольный (вам неизвестный) момент времени

Не в произвольный, а в момент первого вызова useState каждым экземпляром компонента.


Она существует полностью независимо от клиента

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

Нет, она создаётся при первом рендере экземпляра компонента (ведь раньше Реакт просто не знает про неё), и уничтожается вместе с ним

Это ваши домыслы о деталях реализации, которые вам никто не гарантирует. Точно так же он мог бы создаваться ДО первого рендера (при помощи статического анализа, например), создаваться во время каждого рендера и уничтожаться после его окончания и т.д..


И вы со стороны клиента никакие эти факторы никак не контролируете. Вы даже не можете быть уверены, что все на самом деле так, как вы говорите


hint: а оно в действительности и не так, вы с вашими домыслами ошиблись — стейт принадлежит файберу, а не компоненту, так что у вас на каждый компонент может быть несколько копий стейта, время жизни каждой из которых определяется временем жизни файбера.


Я вам больше скажу — перерасчет стейта, например, происходит не при вызове setSomething, а при вызове самого useState (которое на самом деле обертка над useReducer).

Точно так же он мог бы создаваться ДО первого рендера (при помощи статического анализа, например)

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


создаваться во время каждого рендера и уничтожаться после его окончания

Нет, этот вариант нарушит работу компонента, а значит в корректной реализации он невозможен.


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

Это всего лишь означает, что у каждого файбера есть свои экземпляры компонентов. И то, это верно лишь для тех компонентов, которые ещё не попали в DOM.

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

Ну как не может? Может.


Нет, этот вариант нарушит работу компонента, а значит в корректной реализации он невозможен.

Нет, не нарушит.


Это всего лишь означает, что у каждого файбера есть свои экземпляры компонентов. И то, это верно лишь для тех компонентов, которые ещё не попали в DOM.

Это верно для всего. С точки зрения работы стейта вообще нет никаких компонент. И не важно как и куда они попали. Есть только файберы и стейт хранится только в файберах.


Когда вы делаете useState, то вам возвращается dispatch, замкнутый на текущий файбер (и это лишь деталь реализации, повторяю — он может храниться в компонентах, мог бы хранится еще где-то, мог бы вообще нигде не храниться, т.к. useState будет работать и без хранения стейта в сколько-нибудь явном виде, достаточно замкнуть на конкретное значение dispatch).


Т.е., еще раз — вы стейт не создаете, и у вас нет никакого контроля над тем, как он создается. АПИ которое вам предоставляет useState — это просто вернуть текущее значение значение стейта и ф-ю диспатч, которая все что делает — добавляет экшн в очередь файбера.


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

Нет, не нарушит.

Каким образом оно не нарушит?


После нажатия на кнопку компонент Foo должен отобразить "count = 1". Если состояние будет удалено — он отобразит "count = 0". Это что, эквивалентное поведение?


Это верно для всего. С точки зрения работы стейта вообще нет никаких компонент.

А мне не важна точка зрения стейта, мне важна точка зрения компонента.

Каким образом оно не нарушит?

Ни каким не нарушит.


Если состояние будет удалено — он отобразит "count = 0".

Он отобразит нужное состояние.


А мне не важна точка зрения стейта, мне важна точка зрения компонента.

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

Он отобразит нужное состояние.

Как, если оно было удалено после первого рендера?


Вам важно, чтобы useState вам зарезолвил правильный диспатч

Вот в понятие "правильности" тут как раз и входит, в том числе, правильное время жизни.

Как, если оно было удалено после первого рендера?

Из замыкания возьмет.


Вот в понятие "правильности" тут как раз и входит, в том числе, правильное время жизни.

Так в том и дело что не входит. И в реальности время жизни стейта как раз не соответствует времени жизни компонента. Но вы этого не замечаете, т.к. это ни на что не влияет. Это детали реализации. Вам зависимость внедрили — вы с ней работаете, остальное вас не волнует.

Из замыкания возьмет.

Если оно лежит в каком-то замыкании и косвенно доступно — оно не удалено.

Если оно лежит в каком-то замыкании и косвенно доступно — оно не удалено.

Ну это уже у вас софистика, не важно что там и где косвенно.


В любом случае, мы отошли от темы (ну, вы старательно его увели от темы, раз за разом игнорируя аргументы).
У нас факты — есть множество внешних объектов, которые вы напрямую не контролируете (стейты компонент), есть некоторый сервис, апи которого предоставляет вам возможность получить способ взаимодействия с каким-то из этих объектов (при чем не вы решаете каким — сервис и решает) — useState. Вы просите у сервиса, с-но, этот способ предоставить — это классический сценарий DI, просто по всем свойствам DI. Если это — то DI банально в природе не существует. Т.к. вы не сможете привести пример, который бы более точно определению DI соответствовал, чем инжект сервиса при помощи useState.

Уже забыл, что классовые компоненты существуют :/

Спасибо, большой и подробный обзор.

Но не касаемо статьи, а самого новшества от Facebook:
— Хуки — это просто еще один способ описывать логику ваших компонентов. Он позволяет добавить к функциональным компонентам некоторые возможности, ранее присущие только компонентам на классах.

— меня одного от этого трясёт? Можно же просто использовать компоненты на классах и не изучать заново фактически ещё один новый React?

По одной из ссылок, которую вы привели:
В наблюдении facebook классы являются большим препятствием при изучении React. Вам необходимо понять как работает this, а он не работает так как в остальных языках программирования, так же следует помнить о привязке обработчиков событий. Без стабильных предложений синтаксиса код выглядит очень многословно

— ээ… ну как бы, а что за JS-программисте, который не понимает, что такое this и bind/call?

Получается, чуть облегчаем (?) одно и изобретаем новый велосипед, который будем изучать и раскатывать ближайшие три года. Впрочем, как всегда во фронт-енде…

Вот у меня такая же реакция была: все де нормально было. Зачем эта лапша?

Sign up to leave a comment.