Изучение трех новых API в React 18
Вместе с React 18 было представлено несколько новых API, которые позволяют пользователям в полной мере использовать возможности параллельного рендеринга React. Это хуки: useSyncExternalStore, useId и useInsertionEffect.
В этой статье будут рассмотрены эти три новых API, варианты их использования, какие проблемы они решают, почему они были добавлены и как они интегрируются в область параллельного рендеринга.
Примечание, прежде чем мы начнем
Содержание статьи:
Поскольку новые API связаны с параллельным рендерингом, я бы порекомендовал вам сначала ознакомиться с концепцией и понять, почему команда React уделяет ей такое внимание. Хорошим материалом для начала является аннотация React 18 от React или working group announcement. После этого следующие разделы будут иметь гораздо больше смысла.
Хук useSyncExternalStore
Одним из API-интерфейсов, представленных в React v16.14.0 для поддержки параллельного рендеринга, был useMutableSource, предназначенный для обеспечения безопасной и эффективной интеграции компонентов React с внешними изменяемыми источниками во время параллельного рендеринга.
React JS. Основы
Изучите основы ReactJS на практическом примере по созданию учебного веб-приложения
Получить курс сейчас!
Хук подключается к источнику данных, ждет изменений и соответствующим образом планирует обновления. Все это происходит таким образом, чтобы предотвратить разрывы, когда возникают визуальные несоответствия из-за нескольких значений для одного и того же состояния.
Это особенно заметная проблема с новыми функциями параллельного рендеринга, поскольку потоки состояний могут очень быстро переплетаться. Однако внедрение useMutableSource оказалось трудным по следующим причинам:
1. Хук асинхронен
Хук не знает, может ли он повторно использовать результирующее значение функции selector, если оно изменится. Единственным решением была повторная подписка на предоставленный источник данных и повторное получение снапшота, что может вызвать проблемы с производительностью, поскольку это происходит при каждом рендеринге.
Для пользователей и библиотек (таких как Redux) это означает, что они должны были запоминать каждый селектор в своем проекте и не могли определять функции селектора, потому что их ссылки не были стабильными.
2. Хук должен иметь дело с внешним состоянием
Первоначальная реализация также была ошибочной, потому что ей приходилось иметь дело с состояниями, существующими за пределами React. Это означало, что состояние могло измениться в любое время из-за его изменчивости.
Поскольку React пытался решить эту проблему асинхронно, это иногда приводило к замене видимых частей пользовательского интерфейса резервными элементами, что приводило к неоптимальному взаимодействию с пользователем. Все это сделало миграцию болезненной и неоптимальной как для разработчиков, так и для пользователей.
Решение этих проблем с помощью useSyncExternalStore
Чтобы решить эти проблемы, команда React изменила базовую реализацию и переименовала хук в useSyncExternalStore, чтобы правильно отразить его поведение. Изменения включают в себя:
Отсутствие повторной подписки на внешний источник каждый раз при изменении селектора (для снапшота) — вместо этого React будет сравнивать результирующие значения селекторов, а не функций селектора, для решения, нужно ли снова получать снимок, чтобы пользователи могли определить встроенные селекторы без негативного влияния на производительность
Всякий раз, когда изменяется внешнее хранилище, результирующие обновления теперь всегда синхронны, что предотвращает замену пользовательского интерфейса резервным. Единственное требование состоит в том, что результирующее значение аргумента хука getSnapshot должно быть ссылочно стабильным. React использует это внутренне, чтобы определить, нужно ли получить новый снимок, поэтому он должен быть либо неизменяемым значением, либо запомненным/кэшированным объектом.
Как использовать useSyncExternalStore
JavaScript // Code illustrating the usage of `useSyncExternalStore`. // Source: <https://github.com/reactwg/react-18/discussions/86> import {useSyncExternalStore} from ‘react’; // React will also publish a backwards compatible shim // It will prefer the native API, when available import {useSyncExternalStore} from ‘use-sync-external-store/shim’; // Basic usage. getSnapshot must return a cached/memoized result const state = useSyncExternalStore(store.subscribe, store.getSnapshot); // Selecting a specific field using an inline getSnapshot const selectedField = useSyncExternalStore(store.subscribe, () => store.getSnapshot().selectedField); // Code illustrating the usage of the memoized version. // Source: <https://github.com/reactwg/react-18/discussions/86> // Name of API is not final import {useSyncExternalStoreWithSelector} from ‘use-sync-external-store/with-selector’; const selection = useSyncExternalStoreWithSelector( store.subscribe, store.getSnapshot, getServerSnapshot, selector, isEqual );
12345678910111213141516171819202122232425262728 | // Code illustrating the usage of `useSyncExternalStore`.// Source: <https://github.com/reactwg/react-18/discussions/86> import {useSyncExternalStore} from ‘react’; // React will also publish a backwards compatible shim// It will prefer the native API, when availableimport {useSyncExternalStore} from ‘use-sync-external-store/shim’; // Basic usage. getSnapshot must return a cached/memoized resultconst state = useSyncExternalStore(store.subscribe, store.getSnapshot); // Selecting a specific field using an inline getSnapshotconst selectedField = useSyncExternalStore(store.subscribe, () => store.getSnapshot().selectedField); // Code illustrating the usage of the memoized version.// Source: <https://github.com/reactwg/react-18/discussions/86> // Name of API is not finalimport {useSyncExternalStoreWithSelector} from ‘use-sync-external-store/with-selector’; const selection = useSyncExternalStoreWithSelector( store.subscribe, store.getSnapshot, getServerSnapshot, selector, isEqual); |
Хук useId
Запуск React на стороне сервера
Долгое время проекты React выполнялись только на стороне клиента. Короче говоря, это означало, что весь код был отправлен в браузер пользователя (клиент), а затем браузер отвечал за рендеринг и отображение приложения пользователю.
React в целом расширяется в сторону рендеринга на стороне сервера (SSR). В SSR сервер отвечает за создание структуры HTML на основе кода React. Вместо всего кода React в браузер отправляется только HTML.
Браузер отвечает только за то, чтобы взять эту структуру и сделать ее интерактивной, отрисовывая компоненты, добавляя CSS поверх нее и присоединяя к ней JavaScript. Этот процесс называется гидратацией.
Наиболее важным требованием для гидратации является то, что структуры HTML, сгенерированные сервером и клиентом, должны совпадать. Если они этого не сделают, браузер не сможет определить, что он должен делать с этой определенной частью структуры, что приводит к неправильному отображению или неинтерактивному пользовательскому интерфейсу.
Это особенно заметно в функциях, которые зависят от идентификаторов, поскольку они должны совпадать с обеих сторон, например, при создании уникальных имен классов стилей и идентификаторов специальных возможностей.
Эволюция хука useID
Чтобы решить эту проблему, React изначально представил хук useOpaqueIdentifier, но, к сожалению, у него также были некоторые проблемы. В разных средах хуки будут давать разные результаты:
На стороне сервера: может быть создана строка
На стороне клиента: может быть создан специальный объект, который нужно будет передать непосредственно атрибуту DOM.
React JS. Основы
Изучите основы ReactJS на практическом примере по созданию учебного веб-приложения
Получить курс сейчас!
Это означает, что хук мог создавать только один идентификатор, и что было невозможно динамически генерировать новые идентификаторы, потому что они должны подчиняться правилам хуков. Таким образом, если вашему компоненту требуется X разных идентификаторов, ему придется вызывать хук X раз, что, очевидно, плохо.
JavaScript // Code illustrating the way `useOpaqueIdentifier` handles the need for N identifiers in a single component, namely calling the hook N times. // Source: <https://github.com/facebook/react/pull/17322#issuecomment-613104823> function App() { const tabIdOne = React.unstable_useOpaqueIdentifier(); const panelIdOne = React.unstable_useOpaqueIdentifier(); const tabIdTwo = React.unstable_useOpaqueIdentifier(); const panelIdTwo = React.unstable_useOpaqueIdentifier(); return ( <React.Fragment> <Tabs defaultValue=”one”> <div role=”tablist”> <Tab id={tabIdOne} panelId={panelIdOne} value=”one”> One </Tab> <Tab id={tabIdTwo} panelId={panelIdTwo} value=”one”> One </Tab> </div> <TabPanel id={panelIdOne} tabId={tabIdOne} value=”one”> Content One </TabPanel> <TabPanel id={panelIdTwo} tabId={tabIdTwo} value=”two”> Content Two </TabPanel> </Tabs> </React.Fragment> ); }
123456789101112131415161718192021222324252627282930 | // Code illustrating the way `useOpaqueIdentifier` handles the need for N identifiers in a single component, namely calling the hook N times. // Source: <https://github.com/facebook/react/pull/17322#issuecomment-613104823> function App() { const tabIdOne = React.unstable_useOpaqueIdentifier(); const panelIdOne = React.unstable_useOpaqueIdentifier(); const tabIdTwo = React.unstable_useOpaqueIdentifier(); const panelIdTwo = React.unstable_useOpaqueIdentifier(); return ( <React.Fragment> <Tabs defaultValue=”one”> <div role=”tablist”> <Tab id={tabIdOne} panelId={panelIdOne} value=”one”> One </Tab> <Tab id={tabIdTwo} panelId={panelIdTwo} value=”one”> One </Tab> </div> <TabPanel id={panelIdOne} tabId={tabIdOne} value=”one”> Content One </TabPanel> <TabPanel id={panelIdTwo} tabId={tabIdTwo} value=”two”> Content Two </TabPanel> </Tabs> </React.Fragment> );} |
Некоторые специальные API, такие как aria-labelledby, могут принимать несколько идентификаторов через список, разделенный пробелами, но поскольку выходные данные хука отформатированы как непрозрачный тип данных, их всегда нужно напрямую привязывать к атрибуту DOM. Это означало, что было невозможно правильно использовать вышеупомянутые API.
Чтобы решить эту проблему, реализация хука была изменена а сам хук переименован в useId. Этот новый хук API генерирует стабильные идентификаторы во время SSR и гидратации, чтобы избежать несоответствий. Вне контента, отображаемого сервером, он возвращается к глобальному счетчику.
Вместо создания непрозрачного типа данных (специальный объект на сервере и строка на клиенте), как это делается с помощью useOpaqueIdentifier, хук useId создает непрозрачную строку с обеих сторон.
Это означает, что если нам нужно X разных идентификаторов, больше не нужно вызывать хук X раз. Вместо этого компонент может вызвать useId один раз и использовать его в качестве основы для идентификаторов, которые необходимы в компоненте (например, с помощью суффикса), потому что это просто строка. Это решает обе проблемы, которые присутствовали в useOpaqueIdentifier.
Как использовать useID
В приведенном ниже примере кода показано, как использовать useId на основе того, что мы обсуждали выше. Поскольку идентификаторы, сгенерированные React, уникальны в глобальном масштабе, а суффиксы уникальны локально, динамически создаваемые идентификаторы также уникальны в глобальном масштабе и, следовательно, не вызовут несоответствия гидратации.
JavaScript // Code illustrating the improved way in which `useId` handles the need for N identifiers in a single component, namely calling the hook once and creating them dynamically. // Source: <https://github.com/reactwg/react-18/discussions/111> function NameFields() { const id = useId(); return ( <div> <label htmlFor={id + ‘-firstName’}>First Name</label> <div> <input id={id + ‘-firstName’} type=”text” /> </div> <label htmlFor={id + ‘-lastName’}>Last Name</label> <div> <input id={id + ‘-lastName’} type=”text” /> </div> </div> ); }
123456789101112131415161718 | // Code illustrating the improved way in which `useId` handles the need for N identifiers in a single component, namely calling the hook once and creating them dynamically. // Source: <https://github.com/reactwg/react-18/discussions/111> function NameFields() { const id = useId(); return ( <div> <label htmlFor={id + ‘-firstName’}>First Name</label> <div> <input id={id + ‘-firstName’} type=”text” /> </div> <label htmlFor={id + ‘-lastName’}>Last Name</label> <div> <input id={id + ‘-lastName’} type=”text” /> </div> </div> );} |
Хук useInsertionEffect
Проблемы с библиотеками CSS-in-JS
Последний хук, который добавлен в React 18 — и который мы здесь обсудим— это useInsertionEffect. Он немного отличается от других, поскольку его единственная цель важна для библиотек CSS-in-JS, которые генерируют новые правила на лету и вставляют их с тегами style в документ.
В некоторых сценариях теги style необходимо генерировать или редактировать на стороне клиента, что может вызвать проблемы с производительностью при параллельном рендеринге. Это связано с тем, что когда правила CSS добавляются или удаляются, браузер должен проверять, применяются ли эти правила к существующему дереву. Он должен пересчитать все правила стиля и применить их заново, а не только измененные. Если React найдет другой компонент, который также генерирует новое правило, тот же процесс повторится.
Фактически это означает, что правила CSS должны пересчитываться для всех узлов DOM для каждого кадра во время рендеринга React. Хотя есть неплохой шанс, что вы не столкнетесь с этой проблемой, это не то, что хорошо масштабируется.
Теоретически, есть способы обойти эту проблему, которые в основном связаны с таймингами. Лучшим решением синхронизации было бы создание тегов одновременно со всеми другими изменениями в DOM, например, когда это делает библиотека React. Самое главное, это должно произойти до того, как кто-либо попытается получить доступ к макету, а также до того, как все будет представлено браузеру для отображения.
Выглядит так, как будто хук useLayoutEffect может помочь, но проблема в том, что один и тот же хук будет использоваться как для чтения макета и для вставки правил стиля. Это может вызвать нежелательное поведение, например многократное вычисление макета за один проход или чтение неправильного макета.
Как useInsertionEffect решает проблемы параллельного рендеринга
Чтобы решить эту проблему, команда React представила хук useInsertionEffect. Он очень похож на хук useLayoutEffect, но не имеет доступа к ссылкам узлов DOM.
Это означает, что можно только вставлять правила стиля. Его основной вариант использования — вставка глобальных узлов DOM, таких как <style> или SVG <defs>. Поскольку это применимо только для генерации тегов на стороне клиента, хук не запускается на сервере.
JavaScript // Code illustrating the way `useInsertionEffect` is used. // Source: <https://github.com/reactwg/react-18/discussions/110> function useCSS(rule) { useInsertionEffect(() => { if (!isInserted.has(rule)) { isInserted.add(rule); document.head.appendChild(getStyleForRule(rule)); } }); return rule; } function Component() { let className = useCSS(rule); return <div className={className} />; }
1234567891011121314151617 | // Code illustrating the way `useInsertionEffect` is used.// Source: <https://github.com/reactwg/react-18/discussions/110> function useCSS(rule) { useInsertionEffect(() => { if (!isInserted.has(rule)) { isInserted.add(rule); document.head.appendChild(getStyleForRule(rule)); } }); return rule;} function Component() { let className = useCSS(rule); return <div className={className} />;} |
Заключение
Наиболее ожидаемыми функциями React 18 являются функции параллельного рендеринга. Мы получили новые API, которые позволят пользователям применять функции параллельного рендеринга в зависимости от их вариантов использования. Хотя некоторые из них являются совершенно новыми, другие представляют собой улучшенные версии предыдущих API, основанные на отзывах сообщества.
В этой статье мы рассмотрели три последних API, а именно хуки useSyncExternalStore, useId и useInsertionEffect. Мы рассмотрели их варианты использования, проблемы, которые они решают, почему были необходимы определенные изменения по сравнению с их предыдущими версиями и для каких целей они служат при параллельном рендеринге. React 18, наполненный новыми функциями, определенно оправдывает ожидания!
Автор: Chak Shun Yu
Источник: webformyself.com