Pull to refresh

Comments 50

А сколько лет статье? Уже даже PureComponent уходит в прошлое, уступая мемоизированным функциональным компонентам на хуках.
А в чём от них польза? Всё то же самое, и даже больше, можно делать и на классах-компонентах.
Во всем хуки лучше. Но хуков раньше не было и есть legacy, который нужно поддерживать. Нужно учить что-то новое.
Из плюсов, например, удобно работать с контекстом и рефами. Проще разбивать на логические блоки. Проще типизировать(TS). Меньше кода, как на этапе разработки, так и после Babel/Terser. Выше скорость исполнения кода(спорный момент), многое зависит от прямоты рук, но если идеальный случай, то функциональные компоненты работают быстрее. И еще куча всего описано в доке.
Из плюсов, например, удобно работать с контекстом и рефами. Проще разбивать на логические блоки. Проще типизировать(TS). Меньше кода, как на этапе разработки, так и после Babel/Terser.

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

Но:
1) Не запилили улучшения для работы с классовыми компонентами
2) Со скоростью работы тоже вряд-ли что-то смогут сделать
3) С типизацией что можно сделать?
4) И что же такого грандиозного и архитектурно правильного можно сделать на классах, чего нельзя сделать на функциональных компонентах?

А что не так со скоростью работы классов?

1) Не запилили улучшения для работы с классовыми компонентами

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


2) Со скоростью работы тоже вряд-ли что-то смогут сделать

А можно увидеть какой-то кейз, в котором классовые компоненты тормозят, а прямая замена на функциональные — тормоза убирает?


3) С типизацией что можно сделать?

С типизацией как раз у функциональных компонент проблемы. В классовых то что не так?


4) И что же такого грандиозного и архитектурно правильного можно сделать на классах, чего нельзя сделать на функциональных компонентах?

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


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


Ну и в-третьих, у вас самого не возникает чувство некоего противоречия, когда вы в функциональный компонент добавляете состояние? Эта концепция же сама по себе, просто by design, отдает одеванием штанов через голову. Если вам в компоненте нужно состояние — то значит компонент просто не должен быть функциональным. Если же вы пытаетесь смешать ужа с ежом то и получаете эту недоделанную солянку из костылей в виде "хуков".

Спасибо за конструктивную критику.

С типизацией как раз у функциональных компонент проблемы. В классовых то что не так?

Я не сталкивался именно с проблемами типизации ни FC(функциональных компонентов), ни PC(компонентах на классах), но хуки в FC умеют в выведение типов
const [count, setCount] = useState(0);

а также тип и значение находяться в непосредственной близости без загромождения кода:
const [someObject, setSomeObject] = useState<ISomeInterface>(props.someObject);


могли и запилить

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

А можно увидеть какой-то кейз, в котором классовые компоненты тормозят, а прямая замена на функциональные — тормоза убирает?

Да, тут доказать будет сложно, постараюсь на выходных сделать замеры и доказать истинность(ложность) моего утверждения. Без тестов разговор не сложиться.

имеющейся языковой инфраструктурой

А что из жизненного цикла PC или FC не относиться(относиться) к языковой инфраструктуре? Не совсем понятен аргумент вообще.

в случае классовых компонент аналог хуков был бы значительно более гибок

А можно пример желаемого синтаксиса, что-то не приходит мыслей о том как это может выглядеть.

функциональный компонент добавляете состояние


Не вижу противоречий, загуглил с целью найти тайный смысл, но все забито ссылками на FC в React.

Если вам в компоненте нужно состояние — то значит компонент просто не должен быть функциональным.

Почему?
Я не сталкивался именно с проблемами типизации ни FC(функциональных компонентов), ни PC(компонентах на классах), но хуки в FC умеют в выведение типов

Этот тип же в useState прописан. Если вы сделаете что-то типа private someState = new UseState(selector, 0) внутри класса, то он так же выведет


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

Это как раз в итоге загромождает код рендер-функции.


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

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


А что из жизненного цикла PC или FC не относиться(относиться) к языковой инфраструктуре? Не совсем понятен аргумент вообще.

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


А можно пример желаемого синтаксиса, что-то не приходит мыслей о том как это может выглядеть.

Сахар можно придумать разный, но концепт будет в итоге один — просто создаете в компоненте объект, который замыкается на класс и кусок его стейта. У этого объекта можно дергать геттеры и сеттеры. Это и будет ваш хук. И можно делать с ним все, что можно делать с обычными объектами — это не будет специальной магической сущностью со специальным поведением и сторонним менеджментом, как хуки в ф-компонентах.


Почему?

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

При добавление состояния функциональный компонент перестаёт быть чистой функцией, тогда уж.
Нет. Прочитайте что такое: чистая функция.
Прочитал. Всё равно нет, так как хук useState убивает детерминированность функции. А хуки useEffect могут добавить побочные эффекты.

Теперь предлагаю пойти почитать вам.
Окей, начну с тезиса.
"— Каждый раз функция возвращает одинаковый результат, когда она вызывается с тем же набором аргументов

— Нет побочных эффектов"
habr.com/ru/post/437512

В комментариях почти тоже самое, поэтому будем отталкиваться от этого.
Подключение dispatch к любому компоненту уже противоречит понятию чистая функция, поэтому я не рассматривал это. И вообще компонент это уже про работу с DOM, поэтому вообще абсурд. Трактую вашу фразу, хуки загрязняют FC.

Вызов функционального компонента, сводиться к созданию экземпляра компонента. И сколько раз не вызывайте, будет один результат.

Усложняем, добавляем useState(). Вызывая функцию мы получаем… новый экземпляр компонента. Мы можем множество раз его создать, результат один, init state компонента. Затем делаем Synthetic event, например, клик. То есть, мы создадим некое событие, если это событие произвести с любым экземпляром компонента будет один результат, который не повлияет на внешний скоуп, я только вернёт новой состояние, не будем углубляться в VDOM, но это не повлияет ни на что. Например, каррированная функция суммы, тоже является чистой(в идеальном мире).

useEffect уже сложнее, ведь это даёт возможность сделать запрос на сервер, что ДЕЙСТВИТЕЛЬНО УБИРАЕТ СТАТУС ЧИСТОЙ, но не обязывает этого делать, и тогда ситуация также самая что и с useState.

IMHO: это уже перебор и разговор зашёл в непродуктивное русло.

P.s. комментарии больше статьи, спасибо Druu, хорошая дискуссия.

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


Если же подходить к вопросу более прямолинейно, то:


const A = () => <>I am pure</>; // pure

const B = () => {
  const onClick = () => { kill_all_humans(); }
  return <button onClick={onClick}>kill</button>
}; // B itself is pure, but <B/> is "impure" somehow

const C = () => {
  useAnyKindOfHook(); // impure
  return null;
}; // impure

В случае А мы имеем дело с чистой функцией, т.к. она детерминирована и не имеет сайд-эфектов. Про компонент который из неё можно получить я молчу, об этом позже.


В случае Б мы снова имеем с чистой функцией, хотя если попытаться воспользоваться продуктом её результата как компонентом, то такой компонент получится impure. Тут уже отдельная песня что считать чистым компонентом, а что нет, т.к. на ФП это натянуть уже не получится, это ведь императивный реакт как IoC вступает в дело и творит что хочет. Стало быть можно придумать какие угодно правила чистоты рас… компонент, либо считать всё грязным всегда вообще (логично, чо).


Метод С разумеется сразу impure, т.е. любой хук хранит стейт вне вызывающего его метода. Ни о какой чистоте тут и речи быть не может.


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

Вызов функционального компонента, сводиться к созданию экземпляра компонента. И сколько раз не вызывайте, будет один результат.

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

Что вы имели ввиду под "нет"? Любой хук делает вашу функцию impure.

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

Одно из ключевых преимуществ хуков перед классами это возможность создавать переиспользуемые и комбинируемые куски логики. Хуки являются однозначно более удобным решением чем Render Props, и нередко более удобны, чем HOC'и. Именно возможность удобно создавать кастомные хуки является самой мощной фичей хуков. Если честно, я совсем не против API классов, но там была проблемы именно с выделением кусков логики и привязки к конкретному компоненту. Я не совсем представляю как можно решить именно эту проблему через API классов.

Druu может вам легко возразить на это тем, что концепция классов никак не запрещает группирование единых кусков кода во что-то одно. Это trait-ы, mixin-ы и как их там ещё только не называли. По сути примеси. Было бы желание у создателей React остаться в концепии классов, вернули бы mixin-ы (они когда-то были) и вообще добавили бы много всякого удобного сахара. Но там совсем другой курс партии и работа в рамках ООП им вообще не интересна.


Т.е. если сказать более корректно, то это не преимущество хуков перед классами, а преимущество хуков перед конкретной классовой моделью React.

Ну у миксинов были проблемы с конфликтами имен и неявными зависимостями между миксинами, поэтому от них отказались в пользу HOC'ов. Еще из плюсов для хуков это возможность подключить один хук несколько раз. Вообще говоря классы в React тоже имеют мало общего с ООП, а о наследовании компонентов вообще упоминать не принято.
Не буду спорить с тем, что принципиально возможно сделать нормальный API и для классов, но в текущей реализации хуки удобнее классов.

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


Всего-то нужно было хранить в каждом компоненте список всех его примесей, а при вызове событий — обходить дерево примесей рекурсивно. Это было бы куда удобнее хуков.

Ну если уж совсем правильно в случае, когда надо обновить стейт на основании пердыдущего, то лучше вот так:
this.setState((prevState, props)=>({data: [...prevState.data, item]}))
Благодарю за верное замечание. Дополнил статью.
Интересно, про первый пункт. Видимо автор не знает, что если нужно обновить поле одного объекта в массиве, придется переписывать весь стейт и все перерисовывать. В данном случае, использование setState — антипатерн. Когда я столкнулся с редактированием динамически сгенерированных карточек, которые содержат по полей пять, setState ложил мое приложение…

похоже у вас проблемы с функциональным программированием

Не весь и не всё. "Всё сложно". Когда в игру вступает иммутабельность туда же вступают всякие shallow comparison проверки и разная хитрая ссылочная мемоизация. И получается что некоторый оверхед всё же остаётся, но если приложение построено правильно, то обычно он незначительный.

Дело в том, что никто не гарантирует, что за время прошедшее между получением переменной data из state и записью ее нового значения в state, само состояние не изменится.

ЕМНИП, Javascript в браузере — синхронный, и ничего внезапно, во время выполнения участка кода, поменяться на может.

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

Однако, это не всегда правда. В данном коде стейт будет установлен синхронно, более того, дважды вызовет рендер компонента за время работы функции:


const onClick = () => setTimeout(() => {
  setState(1);
  setState(2);
}, 0);

return (
  &lt;button onClick={ onClick } /&gt;
);

(пишу с мобильного, поэтому возможны небольшие ошибки, но общий смысл должен быть понятен)

В данном коде стейт будет установлен синхронно

Нет. Хук setState просто кладет редьюсер в список. Исполняется этот список уже на другом этапе. Т.е. редьюсер setState(2) будет помещен в список до того, как будет применен редьюсер setState(1).


А количество рендеров в хуках вообще штука сложная — т.к. исполнение хуков может добавлять хуки :) то у них там просто while-цикл который рендерит компонент до тех пор, пока хуков не останется :)
Ну и еще вкорячено ограничение на максимальное количество рендеров после которого цикл обрывается. 20 штук, что ли.

Так setTimeout уберите, и будет у вас один рендер в конце.

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

Посмотрите, пожалуйста, в консоль еще раз внимательно.


before update     // запустился обработчик нажатия
rendered          // компонент перерендерился сразу после вызова
                  // setState(), не смотря на то, что обработчик
                  // еще не закончил работу, т.е. синхронно
after 1st update  // обработчик клика продолжает выполняться
rendered          // упс, render-метод снова был вызван
                  // (опять синхронно)
after 2nd update  // а обработчик еще не закончил выполняться

Обработчик клика у вас — setTimeout, компонент и не думает перерендериваться после возврата из него. Ну и насколько я понимаю, ваше "Это не всегда так, если обьернуть в setTimeout" — недокументированное поведение, которое дажи патчем может сломаться без всяких предупреждений

Да, но он перерендеривается после вызова setState(), который, как видно из логов, вызывается синхронно.

// setState(), не смотря на то, что обработчик
// еще не закончил работу, т.е. синхронно

Это не синхронно. Опуская подробности, setState ставит команду в очередь, с временем, когда ее надо будет выполнить. Это время может быть на "прямо сейчас". В этом случае вам кажется, что код выполняется синхронно.
Наличие синхронности, вообще говоря, показать на примере невозможно — какой бы порядок выполнения ни был, он может быть при асинхронном выполнении. Факт асинхронного исполнения не говорит ничего о том, что выполнение будет обязательно задержано. Лишь о том, что возможно.


В данном конкретном случае, кстати, даже если просто в промис обернуть вместо setTime — то уже выполнение будет задержано.

Это не синхронно. Опуская подробности, setState ставит команду в очередь, с временем, когда ее надо будет выполнить.

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


Наличие синхронности, вообще говоря, показать на примере невозможно

Вам следует перечитать доку по event loop в JS, чтобы понять, что в его контексте такое синхронность и асинхронность. Если коротко, из-за его однопоточности между двумя последовательными вызовами console.log() в рамках одной синхронной функции не может вклиниться что-то еще, т.е. как раз пример выше и демонстрирует синхронность. Еще раз призываю Вас обратить внимание на логи — между выводами из callback'а были выведены логи из функции рендера, что намекает на то, что рендер был вызван внутри setState() синхронно, без очереди.
Если очевидные вещи (логи) не доказывают Вам того, что существуют исключительные случаи — предлагаю просто отладить данные вызовы setState(). Для этого просто добавьте debugger в начало функции рендера и прямо перед вызовом setState(). Затем обновите результат в codesandbox и откройте инструменты разработчика. Когда Вы нажмете на кнопку Click ME, процесс выполнения остановится прямо перед вызовом setState() — нажмите два раза Step over (F10 в Chrome) и Вы, к своему удивлению, очутитесь в функции рендера, вместо того, чтобы продолжить выполнение обработчика. Можете проверить call-stack — это не сон, примерно на 14-м уровне вложенности Вы все еще внутри нашего коллбека.
Если и это Вас не убедит, то дальше можно будет попробовать обратиться лично к разработчиками за разъяснениями, но это уже без меня )

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

Я знаю как она работает. В смысле — ее актуальную реализацию, как оно по факту сделано в реакте. А что там "можно реализовать" — дело совершенно несущественное. Конечно, можно, но какое кому дело, если по факту оно работает вполне определенным образом?


Вам следует перечитать доку по event loop в JS, чтобы понять, что в его контексте такое синхронность и асинхронность.

Я прекрасно знаю, не надо объяснять. Так вот — синхронность исполнения доказать нельзя. Потому что асинхронное исполнение может ничем не отличаться по видимому результату от синхронного.


Еще раз призываю Вас обратить внимание на логи — между выводами из callback'а были выведены логи из функции рендера, что намекает на то, что рендер был вызван внутри setState() синхронно, без очереди.

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


Синхронно = обязательно в том же тике (или микротике)
Асинхронно = не обязательно в том же тике (может в следующем, может через 10 тиков, может через 10 секунд

Синхронно = обязательно в том же тике (или микротике)

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

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


Я придерживаюсь того же определения синхронности, поэтому делаю вывод, что рендер все же выполняется синхронно (т.е. сразу после вызова setState() и до выполнения следующей операции в рамках одного тика). В данном случае в упрощенном виде мы имеем:


function onClick() {
  setState();
}

function onState() {
  someIntermediateFunction();
}

function someIntermediateFunction() {
  if (...) {
    ...
  } else {
    render();
  }
}

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

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


Давайте представим функцию fn(), которая должна вызывать переданный ей коллбек. Вот Вам утилитарная функция для проверки асинхронного выполнения переданного коллбека:


function isSyncCheck(fn) {
  let isSync = false;
  fn(() => {
    isSync = true;
  })

  return isSync;
}

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

Вы сами себе противоречите или не упоминаете еще какое-то отличие Вашего понимания "асинхронности".

Вы пропустили слово "обязательно". Если оно не может выполниться в тике отличном от текущего — это синхронно. Если может (но при этом пусть и выполняется в текущем в данном случае) — это асинхронно. Конкретно в рассматриваемом случае, да, рендер выполняется в текущем тике. Но может выполняться и в другом — достаточно минимально код изменить.


Вот Вам утилитарная функция для проверки асинхронного выполнения переданного коллбека:

Еще раз — можно доказать, что ф-я работает асинхронно, просто предъявив асинхронное поведение. Но нельзя доказать, что она выполняется синхронно.


Для вашей "утилитарной ф-и": const f = (callback) => Math.random() > 0.5? callback(): setTimeout(callback, 0) ;


f вызывает коллбек асинхронно, но с вероятностью 0.5 ваша ф-я вернет true

seState() не изменяет состояние компонента мгновенно после вызова, а отправляет запрос на изменение состояния в очередь. Для повышения производительности React может откладывать на некоторое время обновление состояния, а затем выполнить несколько изменений за раз. Поэтому существует вероятность попадания в очередь двух и более запросов на обновление значения одной и той же переменной. Соответственно, после выполнения первого запроса состояние изменится, но второй запрос никак на это не отреагирует и обновит состояние без учета изменений.

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

Осталось понять как обрабатывать это самое "пользовательское событие".

обработчики popstate event и т. п.

Это событие не возникнет если в истории не записаны перемещения пользователя между субстраницами.

мы же псевдо MPA делаем, значит должны быть записаны

Ну так история-то частью стейта не является (в понимании react и redux)

Поинт в том, чтобы сделать её таковой. Вернее браузерную историю синхронизировать со стейтом (redux,mobx,react, ...), где источник правды для приложения в стейте, изменяется история через принятые способы изменения, а вся работа с браузерным API локализована в одном месте, подписанном на изменения стейта и генерящим actions для него в случае действий пользователя

Sign up to leave a comment.

Articles