Проблемы написания библиотеки React, которая хорошо работает в различных средах и сценариях
Эта тема пришла мне в голову, когда я просмотрел код своих коллег и понял, насколько сложно создать надежную библиотеку React. Удивительно, но даже код, найденный через поиск Google, которому мы часто доверяем, может иногда вызывать такие проблемы, как ошибка несоответствия гидратации NextJS.
Эта библиотека безопасна? Этот, казалось бы, простой вопрос на удивление сложен в сообществе React.
В этой статье я поделюсь некоторыми советами и рекомендациями по написанию безопасной библиотеки React, которая безопасна для рендеринга на стороне сервера (SSR), безопасна для параллельного рендеринга и имеет оптимальные зависимости. Чтобы проиллюстрировать эти принципы в действии, мы рассмотрим наивную реализацию хука useLocalStorage
.
Отказ от ответственности: хотя я обычно идентифицирую себя как разработчик Vue, моя повседневная работа связана с поддержкой приложения Next.js. Пожалуйста, поправьте меня, если какая-либо информация в этой статье устарела или неверна.
Что такое useLocalStorage
?
useLocalStorage
— это настраиваемый хук, который позволяет вам читать и записывать в localStorage
в компонентах React. localStorage
– это API браузера, который позволяет хранить пары данных "ключ-значение" в браузере. Хук синхронизирует состояние компонента с данными, хранящимися в localStorage
. Важно отметить, что для краткости этой статьи хук не обновляет представление, если изменения в localStorage
инициируются другими компонентами или вкладками браузера.
Хук useLocalStorage
принимает два аргумента: ключ и начальное значение. Он возвращает массив из двух значений: сохраненное значение и функцию установки. Вы можете использовать сохраненное значение и функцию установки для чтения и записи в localStorage
, как и в случае с useState
.
// A naive custom hook that uses localStorage in React components function useLocalStorage(key, initialValue) { // Use useState to store the value in local state const [storedValue, setStoredValue] = useState(() => { // Get the value from localStorage const item = localStorage.getItem(key); // Parse the value as JSON return item ? JSON.parse(item) : initialValue; }); // Use useEffect to update localStorage when the value changes useEffect(() => { // Stringify the value as JSON const valueToStore = JSON.stringify(storedValue); // Set the value in localStorage localStorage.setItem(key, valueToStore); }, [key, storedValue]); // Return the value and a setter function from the hook return [storedValue, setStoredValue]; }
Приведенная выше реализация намеренно наивна. Мы рассмотрим его ограничения и улучшим его соответствующим образом.
Что означает безопасность SSR?
SSR означает рендеринг на стороне сервера, метод рендеринга компонентов React на сервере и отправки HTML в браузер. SSR может улучшить производительность, SEO и доступность вашего приложения. Однако это также требует особых соображений для авторов библиотек, таких как отказ от API-интерфейсов, специфичных для браузера, обработка гидратации и поддержка потоковой передачи.
Небезопасный код SSR может привести к ошибкам рендеринга Node.js на сервере или ошибкам несоответствия гидратации на клиенте. Эти ошибки могут привести к замедлению работы вашего приложения, прикреплению обработчиков событий к неправильным элементам или даже остановке работы вашего сервера.
Чтобы написать SSR-безопасную библиотеку, вы должны следовать этим рекомендациям:
- Избегайте использования специфичных для браузера API, таких как
window
,document
илиlocalStorage
. Эти API недоступны на сервере и могут вызывать ошибки или несоответствия. Вместо этого используйте обнаружение функций или резервные варианты для обработки различных сред. Например:
// Allow us to use window server-side const safeWindow = (typeof window === 'undefined') ? { addEventListener() {}, removeEventListener() {}, } : window;
- Избегайте рендеринга разных представлений на сервере и на клиенте. Иногда вам может понадобиться отображать различный контент на сервере и клиенте, как в случае с
useLocalStorage
. В таких ситуациях вы должны убедиться, что сервер и клиент отображают одно и то же исходное содержимое и что содержимое клиента соответствующим образом изменяется с помощьюuseEffect
. Подробнее об этом можно узнать в Документации React по отображению разного контента на сервере и клиенте.
Простая реализация, которую мы рассмотрели ранее, должна быть безопасной для SSR.
Во-первых, он напрямую использует localStorage
без каких-либо запасных вариантов, что может нарушить отрисовку сервера.
Во-вторых, если мы условно читаем из localStorage
, это может привести к тому, что сервер и клиент будут отображать разное содержимое. Нам нужно решить эти проблемы, чтобы сделать SSR безопасным.
// SSR safe version function useLocalStorage(key, initialValue) { // useState will always return initialValue consistently const [storedValue, setStoredValue] = useState(initialValue); // Get the value from localStorage in the effect handler useEffect(() => { const item = localStorage.getItem(key); if (item) { setStoredValue(JSON.parse(item)); } else { setStoredValue(initialValue); } }, [key, initialValue]); // Use useEffect to update localStorage when the value changes useEffect(() => { // Stringify the value as JSON const valueToStore = JSON.stringify(storedValue); // Set the value in localStorage localStorage.setItem(key, valueToStore); }, [key, storedValue]); // Return the value and a setter function from the hook return [storedValue, setStoredValue]; }
Обратите внимание, что с появлением React Server Components (RSC) обеспечение безопасности SSR становится еще сложнее. Однако в этой статье мы не будем углубляться в вопросы безопасности RSC.
Что означает параллельный рендеринг безопасным?
Параллельный рендеринг, ранее известный как параллельный режим, — это новая функция React, которая обеспечивает лучшее взаимодействие с пользователем за счет одновременного рендеринга нескольких компонентов. Однако параллельный рендеринг также вводит новые ограничения и потенциальные ловушки для авторов библиотек, такие как избежание побочных эффектов, изменяемые состояния и блокировка рендеринга.
Чтобы написать безопасную библиотеку параллельного рендеринга, вы должны следовать этим рекомендациям:
- Используйте функциональные компоненты и хуки вместо компонентов класса и методов жизненного цикла.
- Используйте хук
useEffect
правильно, обеспечивая:
1. Используйте хукuseEffect
для выполнения побочных эффектов.
2. Очистите все ресурсы в функции возврата хука.
3. ИспользуйтеuseLayoutEffect
при выполнении синхронного чтения DOM. - Избегайте использования глобальных или общих изменяемых состояний, таких как переменные, объекты или массивы. Вместо этого используйте локальное состояние с хуками
useState
илиuseReducer
или используйте контекст с хукомuseContext
. - Не записывайте и не читайте из
ref.current
во время рендеринга, за исключением инициализации. Это может привести к непредсказуемому поведению компонента.
Теперь давайте рассмотрим код, который мы обсуждали ранее, и определим, безопасен ли параллельный рендеринг.
Первоначальная реализация обновляет localStorage
только после обновления компонента и применения изменений к DOM, потому что обновление происходит внутри ловушки useEffect
. Как автор библиотеки, предполагать, что код ваших пользователей безопасен для параллельного рендеринга, небезопасно.
В частности, код пользователя может считываться из localStorage
во время рендеринга, что приводит к несоответствиям между кодом библиотеки и кодом пользователя. Чтобы решить эту проблему, нам нужно обновить localStorage
перед вызовом setStoredValue
.
Вторая проблема более тонкая: мы синхронно читаем значение из localStorage
, но useEffect
не гарантирует синхронных обновлений. Это может привести к неожиданному мерцанию содержимого в пользовательском коде. Поскольку одновременный рендеринг React может задерживать рендеринг в зависимости от производительности устройства и времени рендеринга, мерцание может воспроизводиться или не воспроизводиться. Автору библиотеки желательно обеспечить более надежный рендеринг.
// Concurrent rendering safe version function useLocalStorage(key, initialValue) { // useState will always return initialValue consistently const [storedValue, setStoredValue] = useState(initialValue); // Get the value from localStorage in the layout effect handler useLayoutEffect(() => { const item = localStorage.getItem(key); if (item) { setStoredValue(JSON.parse(item)); } else { setStoredValue(initialValue); } }, [key, initialValue]); // Use useEffect to update localStorage when the value changes useEffect(() => { // Stringify the value as JSON const valueToStore = JSON.stringify(storedValue); // Set the value in localStorage localStorage.setItem(key, valueToStore); }, [key]); // Use useEffect to update localStorage when the value changes const setValue = useCallback((value) => { // Stringify the value as JSON const valueToStore = JSON.stringify(value); // Set the value in localStorage localStorage.setItem(key, valueToStore); setStoredValue(value); }, [key, setStoredValue]); // Return the value and a setter function from the hook return [storedValue, setValue]; }
Наконец, давайте обсудим оптимальные зависимости.
Что такое оптимальные зависимости?
В React хуки имеют массив зависимостей, который отражает реактивные обновления значений во время рендеринга. Однако не все значения должны быть постоянно актуальными. Например, в случае useLocalStorage
мы не хотим перерисовывать, если initialValue
изменится. Это связано с тем, что initialValue
представляет резервное значение, которое будет отображаться, когда key
не существует в локальном хранилище. Если key
остается прежним, нет необходимости перерисовывать компонент.
Высокопроизводительная библиотека должна пропускать ненужные обновления представления, оптимизируя зависимости. Эта концепция может напомнить опытным пользователям React известную запись в блоге Дэна Абрамова о том, как сделать setInterval
декларативным с помощью хуков React.
Вот оптимизированная версия хука useLocalStorage
, который пропускает зависимость, используя useRef
для хранения последнего значения initialValue
:
// Concurrent rendering safe and optimized version function useLocalStorage(key, initialValue) { const [storedValue, setStoredValue] = useState(initialValue); const initialValueRef = useRef(initialValue); useLayoutEffect(() => { const item = localStorage.getItem(key); if (item) { setStoredValue(JSON.parse(item)); } else { setStoredValue(initialValueRef.current); } }, [key]); useEffect(() => { const valueToStore = JSON.stringify(storedValue); localStorage.setItem(key, valueToStore); }, [key, storedValue]); const setValue = useCallback((value) => { const valueToStore = JSON.stringify(value); localStorage.setItem(key, valueToStore); setStoredValue(value); }, [key, setStoredValue]); return [storedValue, setValue]; }
Заключение
Написание безопасной библиотеки React может быть сложной, но полезной задачей. Следуя советам и рекомендациям, описанным в этой статье, вы сможете создать библиотеку React, безопасную для SSR, параллельного рендеринга и имеющую оптимальные зависимости. Не забывайте учитывать разные среды, избегать API-интерфейсов для конкретных браузеров, правильно обрабатывать гидратацию и оптимизировать зависимости, чтобы обеспечить максимальное удобство для пользователей вашей библиотеки.
Но это не конец публикации современной библиотеки JavaScript! Мы только коснулись поверхности, обратившись к проблемам, связанным с React. Еще предстоит решить другие важные вопросы, такие как совместимость с браузером, целостность данных и, возможно, самое неприятное, модульная система.
Спасибо за чтение! Если вы найдете это полезным, следуйте за мной на среднем!