Основы доступности, которые должен знать каждый фронтенд-разработчик
Мартин Холс в своем блоге рассказал о ключевых принципах доступности, которые должен знать каждый фронтенд-разработчик при разработке компонентов. Часть того, о чем пишет автор:
🔴 Семантический HTML. Используйте правильные элементы для интерактивности и нативные элементы. Если нужно показать кнопку, то используйте <button>, а для ссылок <a>. Не надо вместо этого использовать onClick на div элементе. Такие нативные элементы как <select>, <input> и <textarea> доступны из коробки, старайтесь использовать их. Но если запланируете делать кастомный элемент на подобие <select>, то лучше использовать готовые решения, например, react-select.
🔴 Формы. Каждое поле формы должно быть внутри элемента <form> с onSubmit и кнопкой отправки. Это позволяет браузерам определять связанные поля формы и, например, на мобильных устройствах позволяет переключаться между полями без закрытия клавиатуры.
🔴 Навигация с помощью клавиатуры. Проверьте что в приложении можно переключаться между элементами по нажатию Tab и работает Enter. Используйте CSS индикаторы фокуса :focus-visible и :focus.
🔴 Модалки. Сделать их доступными может быть не просто. Важно при открытии модалки переключать фокус внутри него, а при закрытии возвращать фокус на кнопку открытия модалки.
🔴 Стилизация. Улучшайте доступность интерфейсов через стилизацию, например:
- проверьте, что ссылки выглядят как ссылки, а кнопки как кнопки;
- сделайте отчетливые состояния элемента при наведении, активном и выключенном состоянии;
- используйте достаточный контраст цветов для различения элементов;
- поддержите пользовательские размеры шрифтов и зуминг;
- поддержите настройку отключения анимации;
https://martijnhols.nl/blog/accessibility-essentials-every-front-end-developer-should-know
Мартин Холс в своем блоге рассказал о ключевых принципах доступности, которые должен знать каждый фронтенд-разработчик при разработке компонентов. Часть того, о чем пишет автор:
- проверьте, что ссылки выглядят как ссылки, а кнопки как кнопки;
- сделайте отчетливые состояния элемента при наведении, активном и выключенном состоянии;
- используйте достаточный контраст цветов для различения элементов;
- поддержите пользовательские размеры шрифтов и зуминг;
- поддержите настройку отключения анимации;
https://martijnhols.nl/blog/accessibility-essentials-every-front-end-developer-should-know
Please open Telegram to view this post
VIEW IN TELEGRAM
Martijn Hols
Accessibility essentials every front-end developer should know by Martijn Hols
Essential accessibility practices for front-end developers, including semantic HTML, alt texts, ARIA, and keyboard navigation tips to build inclusive components.
🔥12👍8
Делаем использование FormData типобезопасным
Новый хук useActionState в React 19 позволяет обрабатывать отправку данных в формах, пример:
При отправке формы в колбек хука приходит объект типа FormData. В примере formData.get может принимать любой string и нет уверенности что этот string совпадает с именем поля на форме. Для решения этой проблемы, автор предлагает создать компонент поля, которое принимает дженерик с названиями полей и создать функцию получения значения из FormData, которое также принимает дженерик. Пример:
Теперь тип FormName связывает поля на форме и FormData, так что больше нет риска опечаток.
https://www.typeonce.dev/article/make-form-data-and-input-names-type-safe-in-react
Новый хук useActionState в React 19 позволяет обрабатывать отправку данных в формах, пример:
export default function Page() {
const [_, action, pending] = useActionState((_: unknown, formData: FormData) => {
const firstName = formData.get("firstName");
const age = formData.get("age");
}, null);
return (
<form action={action}>
<input type="text" name="firstName" />
<input type="number" name="age" />
<button type="submit" disabled={pending}>Submit</button>
</form>
);
}
При отправке формы в колбек хука приходит объект типа FormData. В примере formData.get может принимать любой string и нет уверенности что этот string совпадает с именем поля на форме. Для решения этой проблемы, автор предлагает создать компонент поля, которое принимает дженерик с названиями полей и создать функцию получения значения из FormData, которое также принимает дженерик. Пример:
type FormName = "firstName" | "age";
const get = <Name extends string = never>(
formData: FormData,
name: NoInfer<Name>
) => formData.get(name);
export default function Page() {
const [_, action] = useActionState((_: unknown, formData: FormData) => {
const firstName = get<FormName>(formData, "firstName");
const age = get<FormName>(formData, "age");
}, null);
return (
<form action={action}>
<SaveInput<FormName> type="text" name="firstName" />
<SaveInput<FormName> type="number" name="age" />
<button type="submit">Submit</button>
</form>
);
}
Теперь тип FormName связывает поля на форме и FormData, так что больше нет риска опечаток.
https://www.typeonce.dev/article/make-form-data-and-input-names-type-safe-in-react
typeonce.dev
Make FormData and input names type-safe in React | Typeonce
Professional training for Full-Stack software development teams
👎12👍3❤1
This media is not supported in your browser
VIEW IN TELEGRAM
react-scan для поиска проблем производительности
Вышла библиотека react-scan для поиска проблем производительности и устранения медленного рендера приложения.
В отличии от других похожих библиотек, для установки react-scan не требуется изменений в коде приложения, достаточно вставить тег <script> или можно установить как модуль. Библиотека react-scan выделяет только те компоненты, которые надо оптимизировать.
Разработчики обещают в будущем выпустить расширение для браузера.
https://github.com/aidenybai/react-scan
Вышла библиотека react-scan для поиска проблем производительности и устранения медленного рендера приложения.
В отличии от других похожих библиотек, для установки react-scan не требуется изменений в коде приложения, достаточно вставить тег <script> или можно установить как модуль. Библиотека react-scan выделяет только те компоненты, которые надо оптимизировать.
Разработчики обещают в будущем выпустить расширение для браузера.
https://github.com/aidenybai/react-scan
👍38❤1
UX++ и DX++ с движками синхронизации
В своем блоге Карл Ассманн объясняет почему сложно инвалидировать запросы и реализовать оптимистичный UI с React Query.
Сложность в инвалидации запросов заключается в том, что надо знать какие запросы нужно инвалидировать и когда. Если инвалидировать лишние ключи, то будут выполняться лишние запросы в сети. Также нужно знать все ключи, которые надо инвалидировать, иначе часть UI будет продолжать показывать устаревшие данные.
Для эффективной реализации оптимистичного UI нужно знать все связанные ключи запроса и влияние каждой мутации на них. Пример реализации оптимистичного UI:
В оптимистичном UI также возникают сложности при добавлении нового запроса. Нужно связать существующие мутации, убедиться, что они инвалидируют этот запрос или вставляют правильные данные для оптимистичного UI.
По мнению автора, основная проблема клиентских приложений в том, что они работают с состоянием на сервере. Бывает нужно выполнить слишком много запросов, из-за которых пользователям приходится ждать, прежде чем они смогут продолжить.
В качестве решения проблемы автор предлагает обратить внимание на движки синхронизации, например Replicache.
Основная идея заключается в том, чтобы иметь локальную копию данных на клиенте. Компоненты подписываются на локальные данные или изменяют их, в то время как движок синхронизирует изменения с сервером в фоновом режиме.
Replicache работает только на клиенте, предоставляет методы подписки и мутации и взаимодействует с конечными точками API, позволяя вам контролировать ваш сервер, вашу базу данных и вашу бизнес-логику.
Пример подписки:
https://www.carlassmann.com/blog/improve-ux-dx-with-sync-engines
В своем блоге Карл Ассманн объясняет почему сложно инвалидировать запросы и реализовать оптимистичный UI с React Query.
Сложность в инвалидации запросов заключается в том, что надо знать какие запросы нужно инвалидировать и когда. Если инвалидировать лишние ключи, то будут выполняться лишние запросы в сети. Также нужно знать все ключи, которые надо инвалидировать, иначе часть UI будет продолжать показывать устаревшие данные.
Для эффективной реализации оптимистичного UI нужно знать все связанные ключи запроса и влияние каждой мутации на них. Пример реализации оптимистичного UI:
const mutation = useMutation(addTodo, {
onMutate: async (newTodo) => {
const previousTodos = queryClient.getQueryData(...);
queryClient.setQueryData([newTodo, ...previousTodos]);
return { previousTodos };
},
onError: (_, _, context) => {
queryClient.setQueryData(["todos"], context.previousTodos);
},
onSettled: () => {
queryClient.invalidateQueries(["todos"]);
},
});
В оптимистичном UI также возникают сложности при добавлении нового запроса. Нужно связать существующие мутации, убедиться, что они инвалидируют этот запрос или вставляют правильные данные для оптимистичного UI.
По мнению автора, основная проблема клиентских приложений в том, что они работают с состоянием на сервере. Бывает нужно выполнить слишком много запросов, из-за которых пользователям приходится ждать, прежде чем они смогут продолжить.
В качестве решения проблемы автор предлагает обратить внимание на движки синхронизации, например Replicache.
Основная идея заключается в том, чтобы иметь локальную копию данных на клиенте. Компоненты подписываются на локальные данные или изменяют их, в то время как движок синхронизирует изменения с сервером в фоновом режиме.
Replicache работает только на клиенте, предоставляет методы подписки и мутации и взаимодействует с конечными точками API, позволяя вам контролировать ваш сервер, вашу базу данных и вашу бизнес-логику.
Пример подписки:
function Todos() {
const todos = useSubscribe(rep, async (tx) => {
return await tx.scan({prefix: "todo/"}).toArray();
});
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>✅ {todo.title}</li>
))}
</ul>
);
}
https://www.carlassmann.com/blog/improve-ux-dx-with-sync-engines
Carlassmann
UX++ and DX++ with Sync Engines
I share what I learn, do and think
👍14
Create React App устарел
Create React App был одним из основных инструментов для старта приложения в 2017-2021 году. Сейчас он официально устарел и в Readme рекомендуют перейти на другой React фреймворк.
Если вы захотите использовать CRA с React 19, то столкнетесь с множеством NPM ошибок при настройке проекта. Это обусловлено сочетанием нескольких факторов. CRA всегда пытается установить последнюю версию React по умолчанию, но 19 версии не указана в шаблонах пакета. Шаблон по умолчанию использует
https://github.com/facebook/create-react-app/issues/17004
Create React App был одним из основных инструментов для старта приложения в 2017-2021 году. Сейчас он официально устарел и в Readme рекомендуют перейти на другой React фреймворк.
Если вы захотите использовать CRA с React 19, то столкнетесь с множеством NPM ошибок при настройке проекта. Это обусловлено сочетанием нескольких факторов. CRA всегда пытается установить последнюю версию React по умолчанию, но 19 версии не указана в шаблонах пакета. Шаблон по умолчанию использует
@testing-library/react@13.0, который зависит от react@18. Это значит, что установка 19 версии приведет к несоответствию версий. https://github.com/facebook/create-react-app/issues/17004
GitHub
Umbrella: CRA breaks with React 19, and CRA needs deprecation notices · Issue #17004 · facebook/create-react-app
Per request from @rickhanlonii : Background / Primary Problem Starting with the release of React 19, users running create-react-app my-app began experiencing hard errors from NPM during project set...
👍12❤6🔥2
Почему я не буду использовать JSDOM
Артем Захарченко предлагает перестать пользоваться библиотекой JSDOM при написании тестов компонентов. JSDOM эмулирует браузер в Node.js, но со своими особенностями. Например, событие Event использует класс dom.window.Event из JSDOM. Это событие не является браузерным или даже Node.js событием. Поэтому произойдет ошибка, если это событие будет обработано где-то еще, кроме JSDOM, например:
Для реализации API браузера JSDOM использует полифиллы, даже для таких Node.js API как fetch, Event, MessageChannel. Однако JSDOM не полностью поддерживает весь функционал браузера, например не поддерживает structuredClone.
По мнению автора статьи, для прогона тестов лучше использовать Vitest Browser Mode и Playwright, которые запускают тесты в настоящем браузере.
https://www.epicweb.dev/why-i-won-t-use-jsdom
Артем Захарченко предлагает перестать пользоваться библиотекой JSDOM при написании тестов компонентов. JSDOM эмулирует браузер в Node.js, но со своими особенностями. Например, событие Event использует класс dom.window.Event из JSDOM. Это событие не является браузерным или даже Node.js событием. Поэтому произойдет ошибка, если это событие будет обработано где-то еще, кроме JSDOM, например:
import { JSDOM } from 'jsdom'
const dom = new JSDOM()
const clickEvent = new dom.window.Event('click')
console.log(clickEvent instanceof globalThis.Event)
// false
const target = new EventTarget()
target.dispatchEvent(clickEvent) // Выкинет исключение
Для реализации API браузера JSDOM использует полифиллы, даже для таких Node.js API как fetch, Event, MessageChannel. Однако JSDOM не полностью поддерживает весь функционал браузера, например не поддерживает structuredClone.
По мнению автора статьи, для прогона тестов лучше использовать Vitest Browser Mode и Playwright, которые запускают тесты в настоящем браузере.
https://www.epicweb.dev/why-i-won-t-use-jsdom
Epic Web Dev
Why I Won’t Use JSDOM
Explore how JSDOM's browser simulation works, and learn front-end testing approaches using Vitest Browser Mode for direct browser testing and native APIs
👍9❤1
Сохранение данных форм с помощью Nuqs
Во время заполнения формы может потребоваться промежуточное сохранение данных, чтобы при обновлении страницы пользователь не потерял введенные данные, либо поделиться данными формой с другими пользователями.
Вариант с localStorage не самый подходящий, с его помощью нельзя поделиться ссылкой на форму с другими пользователями. Автор статьи предлагает использовать библиотеку Nuqs, с помощью которой можно сохранять данные формы в URL в query params. Его можно легко интегрировать с Zod и useForm от react-hook-form и у него встроенная поддержка типизации. Пример формы:
https://armand-salle.fr/post/persisting-form-data-in-react-a-modern-approach-with-nuqs/
Во время заполнения формы может потребоваться промежуточное сохранение данных, чтобы при обновлении страницы пользователь не потерял введенные данные, либо поделиться данными формой с другими пользователями.
Вариант с localStorage не самый подходящий, с его помощью нельзя поделиться ссылкой на форму с другими пользователями. Автор статьи предлагает использовать библиотеку Nuqs, с помощью которой можно сохранять данные формы в URL в query params. Его можно легко интегрировать с Zod и useForm от react-hook-form и у него встроенная поддержка типизации. Пример формы:
import { useQueryState, parseAsJson } from "nuqs"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
// Define our schema
const statusEnum = ["working", "chilling", "cooking"] as const
const formSchema = z.object({
firstName: z.string(),
lastName: z.string(),
status: z.enum(statusEnum),
})
type FormSchemaType = z.infer<typeof formSchema>
function MyForm() {
// Initialize Nuqs state
const [jsonData, setJsonData] = useQueryState(
"json",
parseAsJson(formSchema.parse),
)
// Set up form with React Hook Form
const form = useForm<FormSchemaType>({
resolver: zodResolver(formSchema),
values: jsonData, // Pre-fill form with URL data
})
const handleSubmit = form.handleSubmit((data) => {
console.log(data)
setJsonData(data) // Update URL state
})
return (
<form onSubmit={handleSubmit}>
<TextInput label="First name" {...form.register("firstName")} />
<TextInput label="Last name" {...form.register("lastName")} />
<SelectInput
label="Status"
options={statusEnum}
{...form.register("status")}
/>
<Button type="submit">Submit</Button>
</form>
)
}
https://armand-salle.fr/post/persisting-form-data-in-react-a-modern-approach-with-nuqs/
armand-salle.fr
Persisting Form Data in React: A Modern Approach with Nuqs
Discover how to persist and share form data in React using Nuqs, a modern type-safe URL state manager. Move beyond localStorage limitations to create shareable, type-safe forms with zero backend requirements.
👍15❤2👎1
State of React 2024
Вышла статистика по React за 2024. В ней собрана информация о наиболее/наименее популярных фичах в React, библиотеках, инструментах. Вот наиболее интересные факты:
⭐️ forwardRef — самый нелюбимый API в React, который, кстати, в 19 версии стал устаревшим и можно прокидывать ref напрямую в пропсы.
⭐️ React Compiler и React Server Components наиболее популярные из новых фич React, при этом RSC наиболее сложная и непонятная.
⭐️ Taint API – самый неизвестный API, но это и понятно, т.к. он является экспериментальным.
⭐️ Zod – наиболее популярная библиотека для валидации, а Jest – для тестирования.
⭐️ Next.js, TanStack Query, Redux, Axios – наиболее популярные библиотеки, а TanStack Query и Zustand – самые любимые.
https://2024.stateofreact.com/ru-RU/
Вышла статистика по React за 2024. В ней собрана информация о наиболее/наименее популярных фичах в React, библиотеках, инструментах. Вот наиболее интересные факты:
https://2024.stateofreact.com/ru-RU/
Please open Telegram to view this post
VIEW IN TELEGRAM
Stateofreact
State of React 2024
The 2024 edition of the annual survey about the latest trends in the React ecosystem.
👍24❤6👎4
API captureOwnerStack
Недавно вышедший экспериментальный API React теперь официально задокументирован и без флага. Этот API особенно полезно библиотекам и фреймворкам, поскольку позволяет дополнить вывод ошибки пользователю.
API доступно только в development режиме и считывает текущий Owner Stack и возвращает его в виде строки. Пример:
https://react.dev/reference/react/captureOwnerStack
Недавно вышедший экспериментальный API React теперь официально задокументирован и без флага. Этот API особенно полезно библиотекам и фреймворкам, поскольку позволяет дополнить вывод ошибки пользователю.
API доступно только в development режиме и считывает текущий Owner Stack и возвращает его в виде строки. Пример:
import * as React from 'react';
function Component() {
if (process.env.NODE_ENV !== 'production') {
const ownerStack = React.captureOwnerStack();
console.log(ownerStack);
}
}
https://react.dev/reference/react/captureOwnerStack
react.dev
captureOwnerStack – React
The library for web and native user interfaces
👍6❤1
TypeScript перепишут на Go
Новую версию TypeScript перепишут на Go. Это связано в первую очередь со скоростью работы. TypeScript медленно работает на больших кодовых базах. Особенно это замечается при рефакторинге кода в VS Code, например когда переименовываешь переменную или ищешь все ссылки на интерфейс.
По заверениям Андерса Хейлсберга, при использовании компилятора на Go, скорость работы команды tsc увеличивается в 10 раз. Например, запуск tsc на кодовой базе Playwright вместо 11 секунд занимает 1.1 секунду. Также использование компилятора на Go значительно ускоряет работу редактора VS Code. По замерам автора, скорость загрузки проекта выросла в 8 раз.
Текущая версия TypeScript 5.8. Следующая мажорная версия TypeScript 6 останется написана на TS, но появятся пометки deprecated и критические изменения для поддержки 7ой версии.
Когда в кодовой базе на Go станет достаточно функционала, который покрывает версию на TS, то выпустят TypeScript 7 на Go.
https://devblogs.microsoft.com/typescript/typescript-native-port/
Новую версию TypeScript перепишут на Go. Это связано в первую очередь со скоростью работы. TypeScript медленно работает на больших кодовых базах. Особенно это замечается при рефакторинге кода в VS Code, например когда переименовываешь переменную или ищешь все ссылки на интерфейс.
По заверениям Андерса Хейлсберга, при использовании компилятора на Go, скорость работы команды tsc увеличивается в 10 раз. Например, запуск tsc на кодовой базе Playwright вместо 11 секунд занимает 1.1 секунду. Также использование компилятора на Go значительно ускоряет работу редактора VS Code. По замерам автора, скорость загрузки проекта выросла в 8 раз.
Текущая версия TypeScript 5.8. Следующая мажорная версия TypeScript 6 останется написана на TS, но появятся пометки deprecated и критические изменения для поддержки 7ой версии.
Когда в кодовой базе на Go станет достаточно функционала, который покрывает версию на TS, то выпустят TypeScript 7 на Go.
https://devblogs.microsoft.com/typescript/typescript-native-port/
Microsoft News
A 10x Faster TypeScript
Embarking on a native port of the existing TypeScript compiler and toolset to achieve a 10x performance speed-up.
👍32🔥7👎3
Кейсы оптимизации React
Обзор примеров, показывающих, как пять разных команд разработчиков оптимизировали React. Например, у одной из команд на проекте был плохой показатель метрики INP, которая отслеживает скорость отклика сайта. Хорошим считается отклик менее 200мс, на проекте был отклик 380мс. Какие были найдены причины:
- Большой бандл. Первоначально грузился большой бандл с кодом, который не был нужен. Помог code splitting.
- Определенные колбеки у обработчиков событий вызывались слишком часто, из-за чего блокировался основной поток браузера. Помог debounce на скролл и resize, а также делегирование событий.
- Ре-рендерелись больше компонентов чем нужно при изменении стейта. Помогло оборачивание компонентов в
- Долгий рендеринг компонентов. Помогло обновление до React 18. Основным преимуществом этого обновления стал параллельный рендеринг, поскольку высокоприоритетные задачи, такие как пользовательский клик, могут прерывать рендеринг. Это обновление улучшило метрику INP на 46% на десктопе.
Помимо это помогли следующие исправления для улучшения производительности рендеринга:
- Разбиение сложных хуков на более мелкие. На графике перфоманса было видно, что сложные хуки вызываются несколько раз и перерисовывают компонент без необходимости.
- Замена React Router на window.location там, где это возможно.
- Улучшение мемоизации Redux селекторов.
https://largeapps.dev/case-studies/advanced/
Обзор примеров, показывающих, как пять разных команд разработчиков оптимизировали React. Например, у одной из команд на проекте был плохой показатель метрики INP, которая отслеживает скорость отклика сайта. Хорошим считается отклик менее 200мс, на проекте был отклик 380мс. Какие были найдены причины:
- Большой бандл. Первоначально грузился большой бандл с кодом, который не был нужен. Помог code splitting.
- Определенные колбеки у обработчиков событий вызывались слишком часто, из-за чего блокировался основной поток браузера. Помог debounce на скролл и resize, а также делегирование событий.
- Ре-рендерелись больше компонентов чем нужно при изменении стейта. Помогло оборачивание компонентов в
React.memo() и использование useMemo для дорогих вычислений.- Долгий рендеринг компонентов. Помогло обновление до React 18. Основным преимуществом этого обновления стал параллельный рендеринг, поскольку высокоприоритетные задачи, такие как пользовательский клик, могут прерывать рендеринг. Это обновление улучшило метрику INP на 46% на десктопе.
Помимо это помогли следующие исправления для улучшения производительности рендеринга:
- Разбиение сложных хуков на более мелкие. На графике перфоманса было видно, что сложные хуки вызываются несколько раз и перерисовывают компонент без необходимости.
- Замена React Router на window.location там, где это возможно.
- Улучшение мемоизации Redux селекторов.
https://largeapps.dev/case-studies/advanced/
👍20
Показ тостов через React Server Components
В блоге Build UI показали пример, как, используя React Server Components, показывать всплывающие сообщения (тосты) на клиенте. В примере используются серверные функции, куки и useOptimistic. По итогу, чтобы вызвать тост на клиенте, достаточно в любой серверной функции сделать вызов функции
Функция toast выглядит следующим образом:
Используется случайный ID в названии куки для того, чтобы гарантировать уникальность названия куки.
Чтобы отображать куки на клиенте, надо считать куки, отфильтровать по префиксу
Как видно из кода выше, для удаления куки используется серверный API Next.js. Почему не использовать браузерное API для удаления? По мнению автора, работать с браузерным
Одна из особенностей подхода хранения тостов в куках заключается в том, что тосты будут отображаться при редиректах, в новых вкладках и перезагрузках страницы.
https://buildui.com/posts/toast-messages-in-react-server-components
В блоге Build UI показали пример, как, используя React Server Components, показывать всплывающие сообщения (тосты) на клиенте. В примере используются серверные функции, куки и useOptimistic. По итогу, чтобы вызвать тост на клиенте, достаточно в любой серверной функции сделать вызов функции
toast, пример:
"use server";
export async function save() {
await toast("Blog post successfully saved!");
}
Функция toast выглядит следующим образом:
"use server";
import { cookies } from "next/headers";
async function toast(message: string) {
const cookieStore = await cookies();
const id = crypto.randomUUID();
cookieStore.set(`toast-${id}`, message, {
path: "/",
maxAge: 60 * 60 * 24, // 1 day
});
}
Используется случайный ID в названии куки для того, чтобы гарантировать уникальность названия куки.
Чтобы отображать куки на клиенте, надо считать куки, отфильтровать по префиксу
toast:
import { cookies } from "next/headers";
import { ClientToasts } from "./client-toasts";
export async function Toaster() {
const cookieStore = await cookies();
const toasts = cookieStore
.getAll()
.filter((cookie) => cookie.name.startsWith("toast-") && cookie.value)
.map((cookie) => ({
id: cookie.name,
message: cookie.value,
dismiss: async () => {
"use server";
const cookieStore = await cookies();
cookieStore.delete(cookie.name);
},
}));
return <ClientToasts toasts={toasts} />;
}
Как видно из кода выше, для удаления куки используется серверный API Next.js. Почему не использовать браузерное API для удаления? По мнению автора, работать с браузерным
document.cookie менее приятно, чем с API от Next.js. В ClientToasts автор использует хук useOptimistic, чтобы реализовать немедленное удаление тоста с экрана пользователя и отправить запрос на удаление куки на сервер:
export function ClientToasts({ toasts }: { toasts: Toast[] }) {
const [optimisticToasts, remove] = useOptimistic(toasts, (current, id) =>
current.filter((toast) => toast.id !== id),
);
const localToasts = optimisticToasts.map((toast) => ({
...toast,
dismiss: async () => {
remove(toast.id);
await toast.dismiss();
},
}));
return …
}
Одна из особенностей подхода хранения тостов в куках заключается в том, что тосты будут отображаться при редиректах, в новых вкладках и перезагрузках страницы.
https://buildui.com/posts/toast-messages-in-react-server-components
Build UI
Toast messages in React Server Components
👎25👍4
React Labs: View Transitions, Activity, и другое
В React Labs представили два новых экспериментальных API: View Transitions и Activity. Подробнее о каждой из них:
View Transitions предназначен для анимации переходов в приложении. С ее помощью можно декларативно объявить, что анимировать, когда и как. Пример:
Чтобы определить «когда» анимировать, можно использовать один из трех триггеров: startTransition, useDeferredValue или Suspense.
Для стилизации анимации используйте CSS:
Также есть возможность указывать причину анимации через API addTransitionType, пример:
Activity это новое API, которое позволяет визуально скрывать компонент с UI, снижать его приоритет при рендере, при этом сохраняя его стейт. Использование этого API дешевле в плане производительности по сравнению с размонтированием или скрытием через CSS. Пример компонента <Activity>:
Когда у Activity мод 'visible', то компонент рендерится как обычно, а когда 'hidden', то размонтируется, но сохранит стейт и продолжит рендерится с более низким приоритетом чем видимые компоненты.
Acitivity и View Transition можно использовать вместе, пример:
https://react.dev/blog/2025/04/23/react-labs-view-transitions-activity-and-more
В React Labs представили два новых экспериментальных API: View Transitions и Activity. Подробнее о каждой из них:
View Transitions предназначен для анимации переходов в приложении. С ее помощью можно декларативно объявить, что анимировать, когда и как. Пример:
// «Что» анимировать
<ViewTransition>
<div>animate me</div>
</ViewTransition>
Чтобы определить «когда» анимировать, можно использовать один из трех триггеров: startTransition, useDeferredValue или Suspense.
Для стилизации анимации используйте CSS:
// «Как» анимировать
::view-transition-old(*) {
animation: 300ms ease-out fade-out;
}
::view-transition-new(*) {
animation: 300ms ease-in fade-in;
}
Также есть возможность указывать причину анимации через API addTransitionType, пример:
function navigate(url) {
startTransition(() => {
addTransitionType('nav-forward');
go(url);
});
}
function navigateBack(url) {
startTransition(() => {
addTransitionType('nav-back');
go(url);
});
}
<ViewTransition
name="nav"
share={{
'nav-forward': 'slide-forward',
'nav-back': 'slide-back',
}}>
{heading}
</ViewTransition>
::view-transition-old(.slide-forward) {
animation: ...
}
::view-transition-new(.slide-forward) {
animation: ...
}
Activity это новое API, которое позволяет визуально скрывать компонент с UI, снижать его приоритет при рендере, при этом сохраняя его стейт. Использование этого API дешевле в плане производительности по сравнению с размонтированием или скрытием через CSS. Пример компонента <Activity>:
<Activity mode={isVisible ? 'visible' : 'hidden'}>
<Page />
</Activity>
Когда у Activity мод 'visible', то компонент рендерится как обычно, а когда 'hidden', то размонтируется, но сохранит стейт и продолжит рендерится с более низким приоритетом чем видимые компоненты.
Acitivity и View Transition можно использовать вместе, пример:
<ViewTransition>
<Activity mode={url === '/' ? 'visible' : 'hidden'}>
<Home />
</Activity>
<Activity mode={url === '/details/1' ? 'visible' : 'hidden'}>
<Details id={id} />
</Activity>
<ViewTransition>
https://react.dev/blog/2025/04/23/react-labs-view-transitions-activity-and-more
react.dev
React Labs: View Transitions, Activity, and more – React
The library for web and native user interfaces
🔥9👍6👎1
Родители и владельцы в React: провайдеры контекста
Джулс Блом в своем блоге написала про то, что понимание того, как компоненты-родители (parent) и компоненты-владельцы (owner) влияют на обновление контекста, может помочь писать более производительные провайдеры контекста.
Чтобы провайдеры контекста со стейтом были производительными, надо чтобы провайдер с логикой изменения стейта был отдельным компонентом и принимал children. Пример:
Чтобы разобраться, почему провайдер контекста в этом примере производительный, сначала стоит разобраться что такое компоненты-родители и компоненты-владельцы:
🔴 компонент-родитель содержит дочерние компоненты и определяет структурные отношения (вложенность) между компонентами, например в App компонент CounterProvider является родителем для вложенных в него компонентов;
🔴 компонент-владелец рендерит другие компоненты и определяет функциональные отношения, вызывает рендеринг, например это компонент CounterProvider или App.
Если вспомнить причины ре-рендеринга компонента, а это изменение стейта, обновление контекста и ре-рендер компонента-владельца, то становится понятно почему провайдер контекста стал производительнее. В CounterProvider логика со стейтом и при ее изменении будет происходить ре-рендер компонента CounterProvider и компонентов, которые подписаны на изменения контекста. При этом, компонент App не будет ре-рендерится, т.к. в нем нет изменений стейта.
https://julesblom.com/writing/parent-owners-context
Джулс Блом в своем блоге написала про то, что понимание того, как компоненты-родители (parent) и компоненты-владельцы (owner) влияют на обновление контекста, может помочь писать более производительные провайдеры контекста.
Чтобы провайдеры контекста со стейтом были производительными, надо чтобы провайдер с логикой изменения стейта был отдельным компонентом и принимал children. Пример:
function CounterProvider({ children }) {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider value={count}>
{children}
</CounterContext.Provider>
);
}
function App() {
return (
<CounterProvider>
<CounterDisplay />
<UnrelatedComponent />
<Toolbar />
</CounterProvider>
)
}
Чтобы разобраться, почему провайдер контекста в этом примере производительный, сначала стоит разобраться что такое компоненты-родители и компоненты-владельцы:
Если вспомнить причины ре-рендеринга компонента, а это изменение стейта, обновление контекста и ре-рендер компонента-владельца, то становится понятно почему провайдер контекста стал производительнее. В CounterProvider логика со стейтом и при ее изменении будет происходить ре-рендер компонента CounterProvider и компонентов, которые подписаны на изменения контекста. При этом, компонент App не будет ре-рендерится, т.к. в нем нет изменений стейта.
https://julesblom.com/writing/parent-owners-context
Please open Telegram to view this post
VIEW IN TELEGRAM
JulesBlom.com
Parents & Owners in React: Context Providers | JulesBlom.com
Understanding how parent and owner components affect context updates can help you write more performant context providers
👍12
TanStack DB
TanStack представила новую библиотеку – TanStack DB – реактивный клиентский стор, с возможностью синка API запросов. Библиотека работает поверх TanStack Query и расширяет его функционал. Разработчики обещают быструю скорость работы библиотеки, даже на больших объемах данных.
Библиотека предлагает fine-grained реактивность, возможность нормализации данных и примитивы транзакций.
Примеры использования:
Синк данных коллекции с API:
Использование live query и фильтрации:
Использование транзакций и оптимистичных изменений на клиенте:
https://github.com/TanStack/db
TanStack представила новую библиотеку – TanStack DB – реактивный клиентский стор, с возможностью синка API запросов. Библиотека работает поверх TanStack Query и расширяет его функционал. Разработчики обещают быструю скорость работы библиотеки, даже на больших объемах данных.
Библиотека предлагает fine-grained реактивность, возможность нормализации данных и примитивы транзакций.
Примеры использования:
Синк данных коллекции с API:
import { createQueryCollection } from "@tanstack/db-collections"
const todoCollection = createQueryCollection<Todo>({
queryKey: ["todos"],
queryFn: async () => fetch("/api/todos"),
getId: (item) => item.id,
schema: todoSchema, // любая схема
})
Использование live query и фильтрации:
import { useLiveQuery } from "@tanstack/react-db"
const Todos = () => {
const { data: todos } = useLiveQuery((query) =>
query.from({ todoCollection }).where("@completed", "=", false)
)
return <List items={todos} />
}
Использование транзакций и оптимистичных изменений на клиенте:
import { useOptimisticMutation } from "@tanstack/react-db"
const AddTodo = () => {
const addTodo = useOptimisticMutation({
mutationFn: async ({ transaction }) => {
const { collection, modified: newTodo } = transaction.mutations[0]!
await api.todos.create(newTodo)
await collection.invalidate()
},
})
return (
<Button
onClick={() =>
addTodo.mutate(() =>
todoCollection.insert({
id: uuid(),
text: "🔥 Make app faster",
completed: false,
})
)
}
/>
)
}
https://github.com/TanStack/db
GitHub
GitHub - TanStack/db: The reactive client store for your API.
The reactive client store for your API. Contribute to TanStack/db development by creating an account on GitHub.
👍17🔥8👎5❤1
Управление фокусом в React с помощью flushSync
Установить фокус на инпуте сразу в React может оказаться не так просто, как кажется. При изменении стейта, React не сразу ре-рендерит компонент, а вместо этого он складывает в очередь все изменения состояний и выполняет их разом после завершения работы обработчика событий. Из-за такого поведения могут возникнуть непредвиденные ситуации, например:
В примере выше фокс на инпуте не появится, из-за того что изменение стейта setShow сработает только по завершению работы обработчика события onClick.
Для решения проблемы используйте функцию flushSync, которая синхронно выполняет изменение стейта в переданном колбеке. По завершению работы flushSync DOM будет обновлен и
https://www.epicreact.dev/mastering-focus-management-in-react-with-flush-sync-f5b38
Установить фокус на инпуте сразу в React может оказаться не так просто, как кажется. При изменении стейта, React не сразу ре-рендерит компонент, а вместо этого он складывает в очередь все изменения состояний и выполняет их разом после завершения работы обработчика событий. Из-за такого поведения могут возникнуть непредвиденные ситуации, например:
function MyComponent() {
const inputRef = useRef<HTMLInputElement>(null)
const [show, setShow] = useState(false)
return (
<div>
<button
onClick={() => {
setShow(true)
inputRef.current?.focus() // Фокус не будет работать
}}
>
Show
</button>
{show ? <input ref={inputRef} /> : null}
</div>
)
}
В примере выше фокс на инпуте не появится, из-за того что изменение стейта setShow сработает только по завершению работы обработчика события onClick.
Для решения проблемы используйте функцию flushSync, которая синхронно выполняет изменение стейта в переданном колбеке. По завершению работы flushSync DOM будет обновлен и
inputRef.current?.focus() сработает. Обновленный пример обработчика:
…
<button
onClick={() => {
flushSync(() => {
setShow(true)
})
inputRef.current?.focus()
}}
>
…
https://www.epicreact.dev/mastering-focus-management-in-react-with-flush-sync-f5b38
Epic React
Mastering Focus Management in React with `flushSync`
Master focus management in React with flushSync. Learn why batching matters, when to use flushSync, and how to create accessible, keyboard-friendly UIs.
👍29❤2🔥2
Анонс TypeScript Native
Вышла превью версия TypeScript написанная на Go. Ее можно установить через npm:
Этот пакет предоставляет команду tsgo – он работает аналогично команде tsc. Со временем команда tsgo переименуется в tsc и переедет в пакет typescript. Сейчас команды разделены для удобства тестирования.
Помимо команды tsgo появилось расширение в VS Code для использования TypeScript Language Service на Go в редакторе – “TypeScript (Native Preview)”. С этим расширением должны ускориться такие функции, как go-to-definition, автокомоплит подсказок, вывод ошибок, показ всплывающих подсказок и другое.
https://devblogs.microsoft.com/typescript/announcing-typescript-native-previews/
Вышла превью версия TypeScript написанная на Go. Ее можно установить через npm:
npm install -D @typescript/native-previewЭтот пакет предоставляет команду tsgo – он работает аналогично команде tsc. Со временем команда tsgo переименуется в tsc и переедет в пакет typescript. Сейчас команды разделены для удобства тестирования.
Помимо команды tsgo появилось расширение в VS Code для использования TypeScript Language Service на Go в редакторе – “TypeScript (Native Preview)”. С этим расширением должны ускориться такие функции, как go-to-definition, автокомоплит подсказок, вывод ошибок, показ всплывающих подсказок и другое.
https://devblogs.microsoft.com/typescript/announcing-typescript-native-previews/
Microsoft News
Announcing TypeScript Native Previews
Previews of the native TypeScript port are now available on npm and for VS Code through the Visual Studio Marketplace!
👍24🔥4❤2
Почему Error Boundary, а не просто try/catch для компонентов
В React нельзя использовать try/catch чтобы отловить ошибки рендера компонента. Это связано с тем, что React не вызывает функцию Calculator когда он создает элемент, он лишь создает описание того что надо отрендерить.
Поэтому если обернуть объявление выше в try/catch можно получить только ошибки во время создания этих элементов, а не ошибки рендера. Реальные ошибки происходят внутри компонента, во время рендера, эффектов и обработчиков ошибок.
По примеру выше можно обернуть в try/catch тело компонента, но это лишь обработает ошибки на уровне данного компонента.
Для обработки ошибок внутри компонента используют Error Boundary. Рекомендуется использовать библиотеку react-error-boundary, которая предоставляет готовый компонент ErrorBoundary и хук useErrorBoundary для обработки ошибок в асинхронных колбеках, эффектах, обработчиках ошибок. Пример использования хука:
https://www.epicreact.dev/why-react-error-boundaries-arent-just-try-catch-for-components-i6e2l
В React нельзя использовать try/catch чтобы отловить ошибки рендера компонента. Это связано с тем, что React не вызывает функцию Calculator когда он создает элемент, он лишь создает описание того что надо отрендерить.
const element = (
<div>
<h1>Calculator</h1>
<Calculator left={1} operator="+" right={2} />
<Calculator left={1} operator="-" right={2} />
</div>
)
Поэтому если обернуть объявление выше в try/catch можно получить только ошибки во время создания этих элементов, а не ошибки рендера. Реальные ошибки происходят внутри компонента, во время рендера, эффектов и обработчиков ошибок.
function Calculator(props) {
try {
// ...render logic
} catch (error) {
return <div>Ошибка!</div>
}
}
По примеру выше можно обернуть в try/catch тело компонента, но это лишь обработает ошибки на уровне данного компонента.
Для обработки ошибок внутри компонента используют Error Boundary. Рекомендуется использовать библиотеку react-error-boundary, которая предоставляет готовый компонент ErrorBoundary и хук useErrorBoundary для обработки ошибок в асинхронных колбеках, эффектах, обработчиках ошибок. Пример использования хука:
import { useErrorBoundary } from 'react-error-boundary'
function MyComponent() {
const { showBoundary } = useErrorBoundary()
async function handleClick() {
try {
await doSomethingAsync()
} catch (error) {
showBoundary(error)
}
}
return <button onClick={handleClick}>Do something</button>
}
https://www.epicreact.dev/why-react-error-boundaries-arent-just-try-catch-for-components-i6e2l
Epic React
Why React Error Boundaries Aren't Just Try/Catch for Components
A deep dive into how React error boundaries work, why they're different from try/catch, and how to use them effectively in your apps.
🔥13👍8❤6
Остерегайтесь скрытых проблем при работе с search params
Типо-безопасность — это только вершина айсберга, о которой вспоминают разработчики при работе с search params. Автор библиотеки nuqs предупреждает о скрытых проблемах при работе с search params.
Запись и чтение. Для получения типо-безопасного стейта search params необходимо внедрять библиотеки валидации (например Zod). Для записи в search params сложных стейтов, а потом для их чтения обратно, понадобятся функции сериализации и парсинга соответственно. В nuqs из коробки есть встроенные парсеры для основных типов данных.
Помимо типо-безопасности, при чтении search params важно иметь runtime-безопасность. Даже после парсинга в корректный тип данных, это значение может быть невалидным. Например, значение валидно если находится в диапазоне -90/+90 или -180/+180. Или email написан правильно. Поэтому после парсинга должна происходить валидация данных. Аналогично, перед сериализацией должна происходить валидация, чтобы в URL не попали невалидные значения.
Еще одна из скрытых проблем при работе с URL – частота обновления URL. У разных браузеров есть разный лимит на частоту обновлений URL (в Chrome это 50мс). Проблема может возникнуть из-за привязки URL к высокочастотному инпуту, например
Со временем схема стейта в URL может изменяться, т.е. могут переименовываться поля, удаляться или добавляться новые. Нужно поддержать возможность миграции на новую схему.
При изменении URL может происходить замена текущей истории или добавлении в историю нового URL. При добавлении URL в историю появляется возможность управлять историей через браузерные кнопки вперед/назад. Это удобно для пользователя, но добавляет сложность для разработки, т.к. появляется два источника управления историей: через UI и браузерные кнопки вперед/назад. Например, при открытии модального окна добавляется в историю
https://nuqs.47ng.com/blog/beware-the-url-type-safety-iceberg
Типо-безопасность — это только вершина айсберга, о которой вспоминают разработчики при работе с search params. Автор библиотеки nuqs предупреждает о скрытых проблемах при работе с search params.
Запись и чтение. Для получения типо-безопасного стейта search params необходимо внедрять библиотеки валидации (например Zod). Для записи в search params сложных стейтов, а потом для их чтения обратно, понадобятся функции сериализации и парсинга соответственно. В nuqs из коробки есть встроенные парсеры для основных типов данных.
Помимо типо-безопасности, при чтении search params важно иметь runtime-безопасность. Даже после парсинга в корректный тип данных, это значение может быть невалидным. Например, значение валидно если находится в диапазоне -90/+90 или -180/+180. Или email написан правильно. Поэтому после парсинга должна происходить валидация данных. Аналогично, перед сериализацией должна происходить валидация, чтобы в URL не попали невалидные значения.
Еще одна из скрытых проблем при работе с URL – частота обновления URL. У разных браузеров есть разный лимит на частоту обновлений URL (в Chrome это 50мс). Проблема может возникнуть из-за привязки URL к высокочастотному инпуту, например
<input type="text"> или <input type="range">.Со временем схема стейта в URL может изменяться, т.е. могут переименовываться поля, удаляться или добавляться новые. Нужно поддержать возможность миграции на новую схему.
При изменении URL может происходить замена текущей истории или добавлении в историю нового URL. При добавлении URL в историю появляется возможность управлять историей через браузерные кнопки вперед/назад. Это удобно для пользователя, но добавляет сложность для разработки, т.к. появляется два источника управления историей: через UI и браузерные кнопки вперед/назад. Например, при открытии модального окна добавляется в историю
?modalOpen=true. Теперь пользователь ожидает, что при нажатии на браузерную кнопку назад закроет окно.https://nuqs.47ng.com/blog/beware-the-url-type-safety-iceberg
nuqs.dev
Beware The URL Type-Safety Iceberg | nuqs
Type-safe URL state is only the visible part. There are more dangers below.
👍11❤1
Реактивность – это легко
Стандартный useContext заставляет обновляться все компоненты-потребители, даже если им не нужна изменившаяся часть состояния. Это приводит к лишним ре-рендерам, особенно в больших компонентах вроде таблиц или списков.
Автор статьи столкнулся с этим в MUI X Data Grid: клик по одной ячейке вызывал ре-рендер всех остальных. Решение — точечная подписка через селекторы
Вместо того, чтобы хранить состояние в useState и передавать его через Context, можно использовать внешний Store и специальный хук useSelector. Идея проста:
🔴 Store — это обычный класс, который хранит состояние, умеет подписывать на обновления (subscribe) и уведомлять подписчиков, когда данные изменились (update). Он живёт вне рендера React.
🔴
Хук подписывается на store и вызывает локальный ре-рендер только тогда, когда возвращаемое селектором значение изменилось.
Пример кода:
Такой подход позволяет обновлять только те компоненты, чьи данные действительно изменились, и часто избавляет от необходимости оборачивать всё в React.memo.
https://romgrk.com/posts/reactivity-is-easy/
Стандартный useContext заставляет обновляться все компоненты-потребители, даже если им не нужна изменившаяся часть состояния. Это приводит к лишним ре-рендерам, особенно в больших компонентах вроде таблиц или списков.
Автор статьи столкнулся с этим в MUI X Data Grid: клик по одной ячейке вызывал ре-рендер всех остальных. Решение — точечная подписка через селекторы
Вместо того, чтобы хранить состояние в useState и передавать его через Context, можно использовать внешний Store и специальный хук useSelector. Идея проста:
useSelector(store, selectorFn) — кастомный хук, который принимает store и функцию-селектор. Селектор — это функция, которая из всего объекта состояния достает только нужный компонентa фрагмент данных (например, `state => state.focus === index`).Хук подписывается на store и вызывает локальный ре-рендер только тогда, когда возвращаемое селектором значение изменилось.
Пример кода:
const Context = createContext();
export function Grid() {
const [store] = useState(() => new Store({ focus: 0 }));
return (
<Context.Provider value={store}>
{Array.from({ length: 50 }).map((_, i) => (
<Cell index={i} />
))}
</Context.Provider>
);
}
const selectors = {
isFocus: (state, index) => state.focus === index,
};
function Cell({ index }) {
const store = useContext(Context);
const focus = useSelector(store, selectors.isFocus, index);
return (
<button
ref={ref}
onClick={() => store.update({ ...store.state, focus: index })}
className={clsx({ focus })}
>
{index}
</button>
);
};
Такой подход позволяет обновлять только те компоненты, чьи данные действительно изменились, и часто избавляет от необходимости оборачивать всё в React.memo.
https://romgrk.com/posts/reactivity-is-easy/
Please open Telegram to view this post
VIEW IN TELEGRAM
Romgrk
Reactivity is easy
romgrk's personal blog
👎18👍5❤1