React, я люблю тебя, но ты сводишь меня с ума

от автора

Привет, друзья!

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

Дорогой React, мы встречаемся уже почти 10 лет. Мы прошли долгий путь вместе. Но ситуация вышла из-под контроля. Нам нужно поговорить.

Ты был единственным

Ты у меня не первый. До тебя у меня были длительные отношения с jQuery, Backbone и Angular. Я знал, чего следует ожидать от фреймворка: лучший пользовательский интерфейс, продуктивность и опыт разработки, но также фрустрация от необходимости изменения способа написания кода в угоду парадигме фреймворка.

Когда я тебя встретил, я находился в длительных отношениях с Angular. Я был измотан watch и digest, не говоря уже о scope. Я искал то, что не заставляло бы меня чувствовать себя несчастным.

Это была любовь с первого взгляда. Твое однонаправленное связывание данных было таким освежающим по сравнению с тем, что я знал. Целая категория проблем с синхронизацией данных и производительностью для тебя просто не существовала. Ты был чистым JavaScript, а не его жалким подобием, представленным строкой в элементе HTML. У тебя была вещь под названием «декларативный компонент», которая была настолько прекрасной, что все на тебя засматривались.

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

Герои новых форм

Странности начались, когда я попросил тебя обработать отправку формы. На чистом JS с формами и инпутами работать сложно, на React — еще сложнее.

Сначала разработчик должен выбрать между управляемыми и неуправляемыми инпутами. Оба подхода имеют свои недостатки и баги в пограничных ситуациях. Но почему я должен делать такой выбор? Два различных подхода — это слишком много.

«Рекомендуемым» подходом являются управляемые компоненты, которые очень многословны. Для того, чтобы отправить форму, требуется написать такой код:

import React, { useState } from 'react';  export default () => {     const [a, setA] = useState(1);     const [b, setB] = useState(2);      function handleChangeA(event) {         setA(+event.target.value);     }      function handleChangeB(event) {         setB(+event.target.value);     }      return (         <div>             <input type="number" value={a} onChange={handleChangeA} />             <input type="number" value={b} onChange={handleChangeB} />              <p>                 {a} + {b} = {a + b}             </p>         </div>     ); };

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

  • Redux-form была естественным выбором при использовании Redux, но однажды ее ведущий разработчик покинул проект, чтобы заняться разработкой
  • React-final-form, которая содержала большое количество багов и… ведущий разработчик снова ушел. После этого я стал использовать
  • Formik, популярное, но тяжелое решение, медленное для больших форм с ограниченным функционалом. Поэтому я решил использовать
  • React-hook-form, которая является быстрой, но содержит скрытые баги и имеет документацию, похожую на лабиринт.

После многих лет работы с формами в React, я все еще не знаю, как делать это правильно. Когда я смотрю на то, как работает с формами Svelte, я начинаю чувствовать, что в React применяется неправильная абстракция. Взгляните на этот код:

<script>     let a = 1;     let b = 2; </script>  <input type="number" bind:value={a}> <input type="number" bind:value={b}>  <p>{a} + {b} = {a + b}</p>

Ты слишком чувствителен к контексту

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

Ты тоже это заметил и решил избавиться от Redux в пользу собственного useContext. Но в useContext отсутствует критически важная особенность Redux — возможность реагировать на изменения только части контекста. Приведенные ниже строки кода не эквиваленты с точки зрения производительности:

// Redux const name = useSelector(state => state.user.name); // контекст React const { name } = useContext(UserContext);

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

// Это безумие, но по-другому никак export const CoreAdminContext = (props) => {     const {         authProvider,         basename,         dataProvider,         i18nProvider,         store,         children,         history,         queryClient,     } = props;      return (         <AuthContext.Provider value={authProvider}>             <DataProviderContext.Provider value={dataProvider}>                 <StoreContextProvider value={store}>                     <QueryClientProvider client={queryClient}>                         <AdminRouter history={history} basename={basename}>                             <I18nContextProvider value={i18nProvider}>                                 <NotificationContextProvider>                                     <ResourceDefinitionContextProvider>                                         {children}                                     </ResourceDefinitionContextProvider>                                 </NotificationContextProvider>                             </I18nContextProvider>                         </AdminRouter>                     </QueryClientProvider>                 </StoreContextProvider>             </DataProviderContext.Provider>         </AuthContext.Provider>     ); };

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

Я не хочу использовать useMemo() и useCallback(). Лишние ререндеринги — это твоя проблема, а не моя. Но ты заставляешь меня делать это. Посмотри, как мне приходится разрабатывать простую форму, чтобы она работала достаточно быстро:

// Источник: https://react-hook-form.com/advanced-usage/#FormProviderPerformance const NestedInput = memo(     ({ register, formState: { isDirty } }) => (         <div>             <input {...register('test')} />             {isDirty && <p>This field is dirty</p>}         </div>     ),     (prevProps, nextProps) =>         prevProps.formState.isDirty === nextProps.formState.isDirty, );  export const NestedInputContainer = ({ children }) => {     const methods = useFormContext();      return <NestedInput {...methods} />; };

Прошло почти 10 лет, а проблема остается. Насколько сложно разработать useContextSelector()?

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

Мне не нужно ничего из этого

Ты объяснил мне, что я не должен обращаться к узлам DOM напрямую. Я никогда не считал DOM грязным (dirty), но поскольку тебя это тревожило, я перестал это делать. Теперь я использую refs (ссылки), как ты меня просил.

Но эти ссылки распространяются как вирус. В большинстве случаев при использовании ссылок компонентом, он передает их потомкам. Если потомком является компонент React, он должен перенаправить (передать) ссылку (forward ref) другому компоненту и т.д. до тех пор, пока один из компонентов, наконец, не отрендерит соответствующий элемент HTML.

Перенаправление ссылок могло бы выглядеть так:

const MyComponent = (props) => <div ref={props.ref}>Hello, {props.name}!</div>;

Но это было бы слишком просто. Вместо этого следует использовать (в оригинале abomination — мерзость) React.forwardRef:

const MyComponent = React.forwardRef((props, ref) => (     <div ref={ref}>Hello, {props.name}!</div> ));

В чем проблема? — спросишь ты. В том, что forwardRef() (в случае с TypeScript) не позволяет создавать общие (generic) компоненты:

// Как мне передать ссылку в этом случае? const MyComponent = <T>(props: <ComponentProps<T>) => (     <div ref={/* pass ref here */}>Hello, {props.name}!</div> );

Более того, ты решил, что ссылки предназначены не только для узлов DOM, но также являются эквивалентом this в функциональных компонентах. Или, другими словами, ссылка — это «состояние, изменение которого не влечет повторный рендеринг». На мой взгляд, мне приходится использовать ссылки по той причине, что твой интерфейс useEffect() является слишком странным. Другими словами, refs — это решение созданной тобой же проблемы.

Эффект бабочки (в оригинале используется игра слов — the butterfly (use) effect)

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

// колбек жизненного цикла class MyComponent {     componentWillUnmount: () => {         // ...     }; }  // useEffect const MyComponent = () => {     useEffect(() => {         return () => {             // ...         };     }, []); };

}, []); — от одной этой строчки мне становится дурно.

Я вижу эти загадочные наборы каббалистических символов по всему моему коду. Более того, ты заставляешь меня следить за зависимостями моего кода, например:

// Меняем страницу при отсутствии данных useEffect(() => {     if (         query.page <= 0 ||         (!isFetching && query.page > 1 && data?.length === 0)     ) {         // Запрашиваем страницу, которой не существует, устанавливаем `page` в значение `1`         queryModifiers.setPage(1);         return;     }     if (total == null) {         return;     }     const totalPages = Math.ceil(total / query.perPage) || 1;     if (!isFetching && query.page > totalPages) {         // Запрашиваем страницу за пределами диапазона, устанавливаем значение `page` в значение последней существующей страницы         // Выполняется при удалении последнего элемента на последней странице         queryModifiers.setPage(totalPages);     } }, [isFetching, query.page, query.perPage, data, queryModifiers, total]);

Видите последнюю строку? Я должен быть уверен, что включил все реактивные переменные в массив зависимостей. А мне казалось, что подсчет ссылок (reference counting) является встроенной возможностью всех языков программирования со сборщиком мусора. Но нет, я вынужден сам управлять зависимостями, поскольку ты не умеешь этого делать.

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

const handleClick = useCallback(     async event => {         event.persist();         const type =             typeof rowClick === 'function'                 ? await rowClick(id, resource, record)                 : rowClick;         if (type === false || type == null) {             return;         }         if (['edit', 'show'].includes(type)) {             navigate(createPath({ resource, id, type }));             return;         }         if (type === 'expand') {             handleToggleExpand(event);             return;         }         if (type === 'toggleSelection') {             handleToggleSelection(event);             return;         }         navigate(type);     },     [         // О, Боже, нет         rowClick,         id,         resource,         record,         navigate,         createPath,         handleToggleExpand,         handleToggleSelection,     ], );

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

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

function Counter() {     const [count, setCount] = useState(0);      const handleClick = useCallback(() => {         setCount(count => count + 1);     }, [setCount]);      useEffect(() => {         const id = setInterval(() => {             setCount(count => count + 1);         }, 1000);         return () => clearInterval(id);     }, [setCount]);      useEffect(() => {         console.log('Значение счетчика:', count);     }, [count]);      return <button onClick={handleClick}>Click Me</button>; }

Если бы ты знал, как управлять зависимостями, я мог бы написать что-то вроде:

function Counter() {     const [count, setCount] = createSignal(0);      const handleClick = () => setCount(count() + 1);      const timer = setInterval(() => setCount(count() + 1), 1000);      onCleanup(() => clearInterval(timer));      createEffect(() => {         console.log('Значение счетчика:', count());     });      return <button onClick={handleClick}>Click Me</button>; }

К слову, это валидный код Solid.js.

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

Прими решение, наконец

Ты пытался улучшить useEffect и представил мне useEvent, useInsertionEffect, useDeferredValue, useSyncExternalStore и другие уловки.

Они позволяют тебе выглядеть красиво:

function subscribe(callback) {     window.addEventListener('online', callback);     window.addEventListener('offline', callback);     return () => {         window.removeEventListener('online', callback);         window.removeEventListener('offline', callback);     }; }  function useOnlineStatus() {     return useSyncExternalStore(         subscribe, // React не будет выполнять повторную подписку при передаче одной и той же функции         () => navigator.onLine, // Как получать значение на клиенте         () => true, // Как получать значение на сервере     ); }

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

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

Ты слишком строгий

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

Например, у меня есть «компонент-инспектор», который является перетаскиваемым. Пользователи также могут скрывать этот компонент. В скрытом состоянии ничего не рендерится. Поэтому мне бы хотелось осуществлять «ранний выход», чтобы не регистрировать обработчики для ничего:

const Inspector = ({ isVisible }) => {     if (!isVisible) {         // ранний выход         return null;     }     useEffect(() => {         // Добавляем обработчики         return () => {             // Удаляем обработчики         };     }, []);     return <div>...</div>; };

Но нет, согласно правилам useEffect и другие хуки не могут вызываться условно. Поэтому мне приходится добавлять условие раннего выхода во все эффекты в случае, когда проп isVisible имеет значение false:

const Inspector = ({ isVisible }) => {     useEffect(() => {         if (!isVisible) {             return;         }         // Добавляем обработчики         return () => {             // Удаляем обработчики         };     }, [isVisible]);      if (!isVisible) {         // Выход не такой ранний, каким мог бы быть         return null;     }     return <div>...</div>; };

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

Правила хуков — это следствие деталей реализации — реализации, выбранной тобой для хуков. Но так быть не должно.

Ты слишком старый

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

Например, когда я ищу «Отслеживание позиции курсора в React» на «StackOverflow», первым результатом является решение, устаревшее еще 100 лет назад:

class ContextMenu extends React.Component {     state = {         visible: false,     };      render() {         return (             <canvas                 ref="canvas"                 className="DrawReflect"                 onMouseDown={this.startDrawing}             />         );     }      startDrawing(e) {         console.log(             e.clientX - e.target.offsetLeft,             e.clientY - e.target.offsetTop,         );     }      drawPen(cursorX, cursorY) {         // Для отображение информации в подписи         this.context.updateDrawInfo({             cursorX: cursorX,             cursorY: cursorY,             drawingNow: true,         });          // Рисование         const canvas = this.refs.canvas;         const canvasContext = canvas.getContext('2d');         canvasContext.beginPath();         canvasContext.arc(             cursorX,             cursorY /* начальная позиция */,             1 /* радиус */,             0 /* начальный угол */,             2 * Math.PI /* конечный угол */,         );         canvasContext.stroke();     } }

Когда я ищу npm-пакет для определенной фичи React, я, в основном, нахожу заброшенные пакеты со старым синтаксисом. Например, react-draggable. Это де-факто стандарт для реализации перетаскивания в React. У него много открытых вопросов и низкая активность разработки. Вероятно, старый синтаксис (классы) не привлекает новых участников (contributors).

Официальная документация по-прежнему рекомендует использовать componentDidMount и componentWillUnmount вместо useEffect. Команда разработчиков React работает над новой версией документации под названием Beta docs на протяжении последних двух лет. А воз и ныне там.

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

Семейное дело

Поначалу твой отец Facebook выглядел очень круто. Facebook хотел «Сблизить людей» — я в деле! Когда бы я ни приезжал к твоим родителям, я всегда встречал новых друзей.

Но затем все стало очень плохо. Твои родители участвовали в схеме манипулирования толпой. Они изобрели концепцию «Фейковых новостей». Они начали собирать обо всех информацию без их согласия. Посещать твоих родителей стало опасно — до такой степени, что несколько лет назад я удалил свой аккаунт Facebook.

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

Другие основные JS-фреймворки смогли освободиться от родителей. Они стали независимыми и присоединились к организации под названием OpenJS Foundation. Node.js, Electron, Webpack, Lodash, ESlint и даже Jest теперь финансируются коллективом компаний и частных лиц. Раз смогли они, ты тоже сможешь. Но ты этого не делаешь. Ты остаешься с родителями. Почему?

Это не я, это ты

У нас с тобой одинаковые цели в жизни — помогать разработчикам создавать лучший UI. Я делаю это с помощью React-admin. Поэтому я понимаю вызовы, с которыми ты сталкиваешься, и компромиссы, на которые ты вынужден идти. Твоя работа не из легких, и, вероятно, ты решаешь множество проблем, о которых я не имею ни малейшего понятия.

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

Я изучал другие фреймворки. Каждый из них имеет свои недостатки: Svelte — это не JavaScript, SolidJS содержит неприятные ловушки, например:

// это работает в `SolidJS` const BlueText = props => <span style="color: blue">{props.text}</span>;  // а это нет const BlueText = ({ text }) => <span style="color: blue">{text}</span>;

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

Я не могу бросить тебя, детка

Проблема в том, что я не могу тебя бросить.

Во-первых, я люблю твоих друзей. MUI, Remix, react-query, react-testing-library, react-table… Когда я с этими парнями, я всегда делаю потрясающие вещи. Они делают меня лучшим разработчиком — они делают меня лучше как человека. Я не могу бросить тебя, не бросив их.

«Это экосистема, идиот».

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

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

Я завишу от тебя.

Позвони мне

Я был предельно честен с тобой. Теперь твой черед. Собираешься ли ты решать названные мной проблемы и, если собираешься, то когда? Что ты думаешь о разработчиках библиотек, вроде меня? Должен ли я забыть о тебе? Или мы должны остаться вместе и работать над нашими отношениями?

Что дальше? Скажи мне.



ссылка на оригинал статьи https://habr.com/ru/company/timeweb/blog/693072/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *