Комментарии 52
Мне одному классы больше нравились?
Вот да. Как только неявный стейт где-то за кадром в хуках "проще" - ну хз. Может проще, пока не думаешь, как это работает под ковром.
Было проще, если бы react явно просил передавать currentNode из своего дома как аргумент.
const MyComponent = (props, node) => { useState(1, node)
currentNode есть, но спратана в кишках.
Да, так было бы максимально понятно. Ну и всякие «нельзя в циклах, нельзя в if, надо всегда в одном порядке, чтобы мы в глобальной хешмапе (ну или что там внутри) поняли че куда сувать». Понятно, что они так две переменные сэкономили, но стоило ли? Т.е. когда в редаксе предлагали шмотки копипастить - было норм, а теперь резко боремся за каждую переменную. Ну может и хрен с ним, все-равно везде ложь: рантайм спекулирует, планировщик делает че хочет, процессор конвееризирует все и вообще из кешей вместо памяти читает. Если кругом хаки и ложки нет, то может хрен с ним с неявным стейтом.
В классах это "под ковром" ещё хуже. Суть в переиспользовании логики — если в классах вы либо копипастите, либо начинаете делать HoC'и, то с хуками можете легко скомпоновать.
Грубо говоря пока вы пишете
useEffect(() => {
Axios.get('/api/user/name')
.then(response => {
setName(response.data,name)
})
}, [])
Классы реально кажутся лучше и проще, а вот когда у вас есть SomeComponent который может быть Draggable, Droppable, Transparentable и Чегоготамble, то вы городите жуткую пирамиду из НоС'ов:
class Component {...}
const MyComponent = Draggable(Droppable(Transparentable(Чегоготамble(Component))))
Ой, погодите, почему так скучно, они же конфигурируемые?!
class Component {...}
class ЧегоготамbleComponent = Чегоготамble('some config string')(Component)
const TransparentableЧегоготамbleComponent = Transparentable('50%')(ЧегоготамbleComponent)
const Component4 = Draggable(true, true)(TransparentableЧегоготамbleComponent)
const MyComponent = Droppable('.drop-zone')(Component4)
Потом вы устаете это писать руками и ставите recompose (или что там сейчас, подскажите кто знает)
Получается так:
class Component {...}
const MyComponent = compose(
Draggable(true, true),
Droppable('.drop-zone'),
Transparentable('50%'),
Чегоготамble('some config string'),
)(Component)
// Тупой вопрос для собеседований №215: В какую сторону идет композиция?
Звучит неплохо, но теперь у вас:
1) конфиги больше самих компонентов
2) Очень больно передавать данные между этими НоС'ами
Вот пример из моей игры:
export class BaseAnimal extends React.PureComponent {
render() {
// Половина пропсов не актуальна без соответствующих НоС'ов
const {classes, animal, game, children, canInteract, acceptInteraction, place} = this.props;
// бла бла логика, рендер
}
}
// Внешний компонент который мне нужен раз
export const Animal = compose(
setDisplayName('Animal')
, setPropTypes({animal: PropTypes.instanceOf(AnimalModel).isRequired})
, withStyles(styles)
)(BaseAnimal);
// Внешний компонент который мне нужен два
export const InteractiveAnimal = compose(
setDisplayName('InteractiveAnimal')
, setPropTypes({animal: PropTypes.instanceOf(AnimalModel).isRequired})
, connect(...)
, InteractionTarget([
InteractionItemType.CARD_TRAIT
, InteractionItemType.FOOD
, InteractionItemType.TRAIT
, InteractionItemType.TRAIT_SHELL
, InteractionItemType.ANIMAL_LINK
, InteractionItemType.COVER
, InteractionItemType.PLANT_ATTACK
], {
// и тут ещё конфиг на 150 строк, не шучу
})
)(Animal);
// Внешний компонент который мне нужен три
export const AnimatedAnimal = AnimatedHOC(({animal}) => `Animal#${animal.id}`)(InteractiveAnimal);
InteractionTarget и AnimatedHOC — это логика которая шарится между многими классами.
Вот и получается, что при такой сложности классы уже не тянут то что нужно и всю эту логику можно вынести в хуки, что сделает её более прозрачной:
const Animal = () => {
const classes = useStyles(); // BaseAnimal вообще не нужно
return <div.../>
}
const InteractiveAnimal = () => {
const {canInteract, acceptInteraction} = useInteractionTarget(...config);
return <Animal .../>
}
const AnimatedAnimal = () => {
useAnimation(...config);
return <InteractiveAnimal .../>
}
tl;dr
1) что классы, что хуки на простых компонентах примерно одинаковые по сложности
2) С увеличением количества реиспользования программирование на классах смещается от самих классов к HoC'ам. Или к renderProps'ам
А вот у них обоих хуки выигрывают с большим отрывом.
Ну может для игр так. У меня таких страшных HoC'ов не было никогда, но ваша точка зрения понятна.
Для игр, для библиотек и для хороших энтерпрайз проектов.
Вы вот со своими классами что будете делать, если вам нужно определить stateful-поведение отдельно от сущностей? Копипастить, как обычно?
EDIT: Отличный пример это Apollo Client
Вот, почитайте как это было раньше: https://www.apollographql.com/docs/react/api/react/hoc/ (NSFW)
Ну, если не говорить про реакт и пойти, например, в сторону игр (я их когда-то разрабатывал), то многие большие игровые проекты успешно пишутся на классах. Как бы, проблема, которая недавно решена с помощью функциональных компонентов с хуками, уже лет 20-30 как решена на классах в геймдеве.
В случае классов можно не городить кучу оберток с помощью декораторов (HOC), а выносить логику из компонента в другие классы/объекты или функции, а также воспользоваться другими паттернами композиции, например стратегией. Если интересно, можете почитать о различных подходах в моей статье: https://habr.com/ru/post/545368/
Хорошая статья, спасибо.
Правда в случае этой статьи, мы все-таки говорим про реакт и я доказывал именно преимущество хуков над классами в реакте, а не вообще по жизни
Мне кажется в любых больших проектах так. Не только в играх. Эти HoC иногда собираются в такие безумные пирамидки… К примеру у нас на проекте не compose
из redux
а самописный метод pipe
. А так как TypeScript то он ещё и типизирован:
/* prettier-ignore */
declare function pipe<A, B, C>
(
ab: (arg: A) => B,
bc: (b: B) => C
): (arg: A) => C;
/* prettier-ignore */
declare function pipe<A extends unknown[], B, C, D>
(
ab: (...args: A) => B,
bc: (b: B) => C,
cd: (c: C) => D
): (...args: A) => D;
/* prettier-ignore */
declare function pipe<A extends unknown[], B, C, D, E>
(
ab: (...args: A) => B,
bc: (b: B) => C,
cd: (c: C) => D,
de: (d: D) => E,
): (...args: A) => E;
/* prettier-ignore */
declare function pipe<A extends unknown[], B, C, D, E, F>
(
ab: (...args: A) => B,
bc: (b: B) => C,
cd: (c: C) => D,
de: (d: D) => E,
ef: (e: E) => F,
): (...args: A) => F;
/* prettier-ignore */
declare function pipe<A extends unknown[], B, C, D, E, F, G>
(
ab: (...args: A) => B,
bc: (b: B) => C,
cd: (c: C) => D,
de: (d: D) => E,
ef: (e: E) => F,
fg: (f: F) => G
): (...args: A) => G;
/* prettier-ignore */
declare function pipe<A extends unknown[], B, C, D, E, F, G, H>
(
ab: (...args: A) => B,
bc: (b: B) => C,
cd: (c: C) => D,
de: (d: D) => E,
ef: (e: E) => F,
fg: (f: F) => G,
gh: (g: G) => H
): (...args: A) => H;
/* prettier-ignore */
declare function pipe<A extends unknown[], B, C, D, E, F, G, H, K>
(
ab: (...args: A) => B,
bc: (b: B) => C,
cd: (c: C) => D,
de: (d: D) => E,
ef: (e: E) => F,
fg: (f: F) => G,
gh: (g: G) => H,
hk: (h: H) => K,
): (...args: A) => K;
/* prettier-ignore */
declare function pipe<A extends unknown[], B, C, D, E, F, G, H, K, L>
(
ab: (...args: A) => B,
bc: (b: B) => C,
cd: (c: C) => D,
de: (d: D) => E,
ef: (e: E) => F,
fg: (f: F) => G,
gh: (g: G) => H,
hk: (h: H) => K,
kl: (h: K) => L,
): (...args: A) => K;
То и дело приходится заходить и дописывать очередной уровень. С hook-ми ничего подобного не испытываем. Это просто код. Им можно жонглировать как угодно, главное не вызывать хуки опционально и в нефиксированных циклах (это кстати напрягает).
Ну redux сам по себе копипасту создает:)) Причем в ng эта проблема просто не существует из-за наличия сервисов.
А redux тут вообще не причём. Наши HoC с ним не связаны. Чаще всего это всякие context.Provider-ы разных мастей.
P.S. а причём тут Angular?
Просто сравниваю. У ng тоже много родовых травм (например, html-шаблоны), но там хорошо сделана структура приложения: feature modules, services, components и везде четко написано, что куда нужно совать, а что не нужно. И "It’s hard to reuse stateful logic between components" просто не существует в ng: делаешь сервис и ничего не hard to reuse. И главное эта логика очень простая и понятная для любого программиста с опытом в enterprise. Хуки вводят новый концепт, который надо учить. Для меня это выглядит как сущность, которую наплодили с понятной надобностью конечно... Но что мешашло IOC или подобие partial application прикрутить, чтобы внедрять зависимости? Типа с хуками проще? Ну может.
Уже не очень помню ангуляр 1, но там, кажется, нельзя делать так, чтобы сервис был на каждый инстанс компонента. Во втором это норма?
Хуки это в первую очередь способ сделать независмый, переиспользуемый кусок логики привязанный к жизненному циклу компонента. Это замена для render props и HoC's (и ранне миксинов) в первую очередь. Со старыми методами была куча проблем, связанная с тем, что все общались через общие пропсы, конфликты имен и т.д. Просто хуки используются и как замена локальному стейту компонента, как в классовых компонентах.
Для IoC используется React Context. Вообще многие библиотеки выглядят в таком виде:const client = new Client();
<ClientProvider.Provider client={client}>
{children}
</ClientProvider.Provider>
//
const client = useClient()
Есть класс отвечающий за логику. Он закидывается в компоненты через контекст. И есть хук который отвечает за привязку к жизненному циклу компонента.
Мне кажется в таком виде похоже на сервисы.
Вы предлагаете смотреть на хуки как на короткие версии render props/hoc/миксины, лишенные проблем конфликта имён в props, а неявный useState и useEffect (жизненный цикл) воспринимать как аспект реализации и не горевать, что их за кулисы убрали? + как бонус компоненты видны без обёрток в консольке. Правильно понял?
А зачем это? За 5 лет использования ts в angular не видел ничего подобного, что этот код делает?
В Angular-е ООП, а в ООП не требуется композиция функций, поэтому и не сталкивались. Вам по сути нечего композировать, т.к. большая часть ваших функций это методы классов. А когда у вас половина проекта это тысячи небольших функций, которые вы с друг другом по разному стыкуете, тогда и начинаются все эти примитивы и пайп-операторы.
Формально можно и без них, но читаемость страдает. Скажем:
// 1. naive
d(c(b(a(source))); // a => b => c => d =>
// 2. pipe
pipe(a, b, c, d)(source);
// 3. pipe operator
source |> a |> b |> c |> d;
В React компоненты это, чаще всего, просто функции. И чтобы добавить к ним какую-нибудь типовую функциональность часто используют HoC-и (хоки). Это функции-врапперы, которые принимают на входе один компонент, а возвращают обёртку над ним, которая внутри вызывает тот самый компонент, но теперь с перламутровыми пуговицами. И в клинических случаях такие матрёшки могут состоять из 10 уровней.
Есть ощущение, что это горе от ума и слишком мелко нарезанные компоненты просто
Нет. Ваши ощущения вас обманывают
- Во-первых HoC-и это экстра-функциональность поверх какого-нибудь компонента, и тут вообще не важно, насколько он там раздроблен уровнем ниже. Глубина матрёшек из HoC-ов не изменится.
- Во-вторых чем ближе вы к ФП, тем больше у вас подобных штук. Думаю про монады, функторы, каррирование и прочий зоопарк вы слышали. Да, в React они не нужны, но вот в хардкорном ФП без них вы будете как без рук и ног. Это ведь кирпичики на которых строятся сложные приложения, построенные на композиции функций. А ФП это целые оркестры\лабиринты из функций. Такая парадигма. И монады там не потому что они всё слишком мелко нарезают. И, пожалуйста, не надо думать что они там все глупые и у них "горе от ума". Это просто другой мир со своими правилами.
- В третьих тут дело не только в React. Такие штуки нужны и в повседневном коде, когда вам приходится иметь дело с большим количеством мелких функций (а многие из нас обожают иметь дело именно с ними).
Я повторю, то что вы их не используете, это не потому, что у вас архитектура правильнее (или неправильнее), а потому что в Angular ООП. А в ООП почти все функции в проекте завязаны на state своего instance-а класса. Т.е. вы в любом случае их не за pipe-ете, т.к. без this state-а ваши методы не работают. Вам просто нечего pipe-ать. У вас своя собственная вселенная с ООП-паттернами, декораторами, IoC контейнерами и пр… Можно было бы тоже сказать, что это всё "горе от ума", но нет, это просто свои инструменты, каждому из которых есть определённое назначение.
P.S. в React пожалуй нет никакой границы вроде "тут я слишком мелко нарезал". Даже однострочные компоненты — ОК. Например такие:
export const Separator = () => <hr className={css.separator}/>;
Это не имеет никаких больших penalty по производительности и отлично сказывается на качестве кода. Особенно когда таким образом достигается разделение business-логики и презентационной. Например в UI библиотеках у любого маломальски сложного компонента очень много допустимых props. И условная кнопка может быть:
<UI.Button
iconAlign="left"
theme="warning"
dir="right"
text="submit"
type="submit"
disabled={disabled}
className={css.submitButton}
blabla1...N={...}
/>
Удобно когда вся эта мишура вынесена куда-нибудь с глаз долой и в реальной форме вы видите код вида:
export const SubmitButton = ({ disabled }: { disabled: boolean })
=> <Ui.Button .../>;
...
import { SubmitButton } from './Controls';
...
<Form ...>
...
<SubmitButton disabled={formIsValid}/>
</Form>
В React нет никаких проблем с тем чтобы раздробить ваши компоненты до любого уровня. Хоть каждый тег компонентом обернуть (если на то есть объективная причина).
И, пожалуйста, не надо думать что они там все глупые и у них «горе от ума». Это просто другой мир со своими правилами.
они там все глупые и у них «горе от ума»
Ведь так оно и есть на самом деле.
Это просто другой мир со своими правилами.
Посмотрите на жителей психбольницы, у них там тоже другой мир со своими правилами.
Монады и функторы к хукам не имеют никакого отношения, если только не считать, что хуки - это a la state monad in disguise. Я же не говорю, что есть проблемы с дроблением. Я говорю, что, наоборот, не надо так мелко. Хуки задачу свою вроде как решают, просто «своим особым способом». В итоге есть накопленный опыт в других технологиях и есть особый react way, который как-бы функциональный, но на самом деле не совсем.
- Монады я привёл в пример не относительно хуков, а относительно направления мысли. Вектор.
- Мы тут не про хуки говорили, а про хоки (хукам не нужен pipe)
- Да у React есть свой react way. Всё так. Стоит правда отметить что свой way в UI фреймворках есть у всех. Просто у React он особенно странный.
- Мелконарезанные компоненты не имеют отношения ни к хукам, ни к хокам. Мелконарезанные компоненты это очень хорошо.
Мелконарезанные компоненты это очень хорошо.
Нет. Очень тонкая грань, все индивидуально, касаемо каждого конкретного компонента и случая.
Я выше привёл два примера (<Separator/>
и <SubmitButton/>
). Я нарушил там эту "грань"? Если да, то почему? Если нет, то можно пример где "грань" нарушена? Ну т.е. что такого нужно сделать чтобы было слишком "мелко нарезано"? И почему?
Я понимаю почему:
const isItemActive = (item: Item): boolean =>
item.isActive
Это перебор, в большинстве случаев. Но подобного со стороны React не встречал.
Если вы конкретно про перегруженную функцию pipe - то в angular вы наверняка используете Observable#pipe по сто раз на дню. Загляните под капот - увидите ту же самую простыню из перегруженных сигнатур: https://github.com/ReactiveX/rxjs/blob/master/src/internal/Observable.ts#L349
Хоть сам статью пиши, потому что эта убогая что капец. Хуки у нас появились оттого что лайвсайкла нет в функциональных компонентах бть, ага.
Документация реакта, ПЕРВЫЙ пункт в мотивации:
Хоть бы один пункт из неё взяли >..<
Заворачивайте в декораторы и не парьтесь
Вы в примере с классами вписали конфиги, а с хуками нет, что-бы код был короче)
Классы реально кажутся лучше и проще, а вот когда у вас есть SomeComponent который может быть Draggable, Droppable, Transparentable и Чегоготамble, то вы городите жуткую пирамиду из НоС'ов:
В angular на эти все вещи создается по директиве, которая независима от класса и может работать с любым (или по желанию определенным) тегом/компонентом.
В таком случа если у нас есть компонент А которому нужно добавить draggable - нам не нужно менять компонент, вместо этого мы навешиваем директиву при использовании компонента.
<my-component draggable></my-component>
Директив при этом можно навесить сколько угодно.
Может просто в react не умеют в классы?
Я сначала в штыки воспринял хуки, и в старых проектах заблокировал их внедрение ради консистентности подхода
А в новых попробовал писать, в итоге пришел к тому, что хуки офигенны, и прозрение случилось в тот момент, когда я начал писать свои кастомные хуки.
Что-то вроде
const { handleSubmit, handleListItemToggle, listItems } = useMyCustomScreenHook();
В итоге
Функциональные компоненты получились гораздо чище классов, ведь из своего хука я экспортирую только нужные методы, абстрагируясь от организации state и того как именно это работает в React.
Хуки можно разделить, если они отвечают за разные логические моменты экрана, получить более читаемый код
Некоторые хуки можно сделать системными. Например - в одном проекте понадобилось пулить несколько сторонних REST API для получения статусов, и был написал хук для этого, который оч красиво встал во все места где это нужно
P.S. Пишу про экраны, потому что сейчас делаю проекты на React Native.
Все смешалось люди, кони.
Особенно понравилась логическая цепочка: появление хуков сделало реакт более понятным, поэтому кол-во вопросов на SO возросло )
Старый добрый реактовский Context API был ужасен. useContext - одно из самых крутых улучшений. Но не единственное. Как уже говорилось, проще декомпозировать логику (которую можно реализовывать на классах и хранить экземпляры в рефах).
Дважды прочитал статью, но так и не понял чем хуки лучше классов.
У меня вот вопрос, уже сто раз видел такие примеры. Хоть кто-то пишет код запроса в компонентах? А не создается какая то оберточка для екшена?
Экшн - это если мы данные хотим в глобальный стор поместить. Для переиспользования в других местах приложения, к примеру. Но вполне себе есть случаи, когда данные одноразовые и нужны только в одном компоненте. Общий стор для этого избыточен.
Кроме того, вполне себе есть паттерн без thunks - компонент реализует асинхронную логику, и уже диспатчит в стор готовые результаты. (опять же для случая, когда эту логику переиспользовать нигде не требуется)
Мне в хуках не хватает второго аргумента в setState - setState(updater, [callback])
Приходится колхозить.
Ещё нет forceUpdate
Этот график доказывает то, что разработчики стали чаще пользоваться библиотекой React после появления хуков.
График также "доказывает" увеличение популярности библиотеки с момента появления COVID-19.
Ужасно пустая статья. Ну и useRef Отношения к DOM не имеет.
Хуки — это лучшее, что случилось с React