Заметки про React – Telegram
Заметки про React
3.78K subscribers
34 photos
8 videos
485 links
Короткие заметки про React.js, TypeScript и все что с ним связано
Download Telegram
Избегаем ошибки гидратации с помощью useSyncExternalStore

При разработке SSR приложений может возникать ошибка:

Uncaught Error: Text content does not match server-rendered HTML.

Она связана с несоответствием контента между клиентом и сервером. Например, при рендере даты на сервере используется одна локаль 21/02/2024, а на клиенте другая локаль – пользовательская 2/21/2024.

Самый простой способ избавиться от ошибки - это использовать проп suppressHydrationWarning. Но как следует из документации React им нельзя злоупотреблять.

Другой способ – использовать useEffect для предотвращения рендера на сервере:


function LastUpdated() {
const [isClient, setIsClient] = React.useState(false)

React.useEffect(() => {
setIsClient(true)
}, [])

if (!isClient) {
return null
}

/** --snip-- */
}


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

Еще один способ – использовать useSyncExternalStore. Хоть он и предназначен для подписки на внешний стор, у него есть вторая особенность: он позволяет различать clientSnapshot и serverSnapshot. При навигации по сайту он будет сразу брать clientSnapshot, а на сервере использовать serverSnapshot. Вместо подписки на внешний стор можно передать заглушку – пустую функцию. Пример использования:


const emptySubscribe = () => () => {}

function LastUpdated() {
const date = React.useSyncExternalStore(
emptySubscribe,
() => lastUpdated.toLocaleDateString(),
() => lastUpdated.toLocaleDateString('en-US')
)

return <span>Last updated at: {date}</span>
}


https://tkdodo.eu/blog/avoiding-hydration-mismatches-with-use-sync-external-store
👍17
Functional UI Kit – унифицированный UI кит для Figma и React

Functional UI Kit – это UI кит для React, который содержит такие же компоненты для Figma. Для каждого компонента и его варианта есть своя история в Storybook. Между компонентами в Figma и React сохранены названия пропсов, а также сохранены одинаковые названия переменных в Figma и в CSS переменных.

При разработке библиотеки автор старался унифицировать все компоненты и использовать общие термины. Благодаря единообразию, продуктовая команда может общаться на одном языке и более быстро разрабатывать продукт.

https://functional-ui-kit.com/
👍18
Изучаем четыре ключевых примитива из Solid

Сравнение ключевых примитивов Solid и мета фреймворка Solid Start с Next.js/Remix:

- Сигналы. Детальная реактивность с помощью сигналов – суперсила Solid. Например, createAsync возвращает сигнал, который триггерит Suspense в зависимости от того, где происходит доступ к значению:


// Solid
export default function Home() {
const nums = createAsync(() => getNums());

return (
<Suspense fallback="Loading...">
{nums()}
</Suspense>
);
}


В React, чтобы сработал Suspense, нужно чтобы вложенный компонент был асинхронным.

- Неблокирующий Async. В Solid createAsync – неблокирующая рендер функция, т.к. для получения результата не требуется использовать async/await.

- Серверные функции. Преимущество серверных функций в возможности типизировать запросы между сервером и клиентом. Solid Start по умолчанию изоморфен, если не указана директива 'use server'. В Solid серверные функции можно вызывать внутри других серверных функций. Также во время SSR серверные функции можно вызывать внутри компонентов:


// Solid
export const getUser = async id => {
"use server";

return {
user: await db.findUser(id),
friends: getFriends(id),
};
};

export function UserComponent(props) {
const user = createAsync(() => getUser(props.userId));

/** --snip-- */
}


- Серверные действия. Они предназначены для изменения данных на сервере. В Solid они отличаются от Remix. Пример:


// Solid
const addNum = action(async () => {
"use server";

return 'success';
}, 'addNum');

export default function Home() {
const addNumAction = useSubmission(addNum);

return (
<main>
<form action={addNum} method='post'>
<button type="submit">Add Num</button>
</form>
<pre>
{addNumAction.pending ? 'pending' : 'not pending'}
{addNumAction.result}
</pre>
</main>
);
}


Хук useSubmission используется для вызова действия в любом компоненте приложения. Еще одно отличие от Remix в том, что можно создать несколько серверных действий и вызывать их в любых компонентах, а не только в роутах.

https://www.brenelz.com/posts/learning-four-key-primitives-in-solid/
👍131
Без внешнего margin

При создании переиспользуемых компонентов есть анти-паттерны, которые усложняют использование компонентов. Один из таких анти-паттернов – внешний margin (и иногда padding).

Внешний margin - это margin, который выходит за рамки border-box компонента. Пример:


function Card({ children }) {
return (
// Так нельзя, это внешний margin
<div style={{ marginBottom: '1rem' }}>{children}</div>
)
}

function EmployeeCard({ name, occupation }) {
return (
<Card>
// Так можно, это внутренний margin
<div style={{ marginBottom: '1rem' }}>{name}</div>
<div>{occupation}</div>
</Card>
)
}


Внешний margin плох тем, что наделяет компонент ответственностью за внешнее пространство. Компонент должен отвечать только за внутреннее пространство.

Для создания пространства между компонентами используйте Flex или Grid вместе с gap в родительском компоненте.

https://kyleshevlin.com/no-outer-margin/
👍25
"react-strict-dom" – почему это круто?

Совсем недавно вышла библиотека "react-strict-dom", цель которой улучшить и стандартизировать способ написания компонентов для Web и Native приложений.

Чтобы понять, как работает эта библиотека, сначала стоит разобраться как работает существующий подход к написанию универсальных компонентов через react-native-web. В библиотеке react-native-web используются React Native компоненты. Библиотека преобразует примитивы React Native в примитивы React Dom, позволяя отображать React Native компоненты в Web.

react-strict-dom использует другой подход, в ней используется Web API для рендера компонентов. Библиотека использует полифилы для перевода примитивов в react-native и react-dom для отображения в Native и Web соответственно.

Под капотом используется stylex, пример компонента с react-strict-dom:


import React from "react";
import { css, html } from "react-strict-dom";

export default function App() {
return (
<html.div style={styles.div}>
<html.div data-testid="testid">div</html.div>
<html.p>paragraph</html.p>

<html.div />

<html.span>
<html.a href="https://google.com">anchor</html.a>,
</html.div>
);
}

const styles = css.create({
div: {
paddingBottom: 50,
backgroundColor: "white",
},
});


https://szymonrybczak.dev/blog/react-strict-dom
👎14👍5
HTML стриминг и алгоритм сравнения DOM

В последние годы все больше браузеров поддерживают стриминг HTML и JS. Во время стриминга браузер сразу рендерит HTML и исполняет JS после получения HTML фрагмента с сервера. Для активации стриминга нужно отправить браузеру правильные заголовки:


{
"transfer-encoding": "chunked",
"vary": "Accept-Encoding",
"content-type": "text/html; charset=utf-8"
}


В сервера в ответе нужно использовать ReadableStream.

Есть несколько практических вариантов использования стриминга HTML. Один из них – замена HTML во время стриминга, например, как в React Suspense. Самая простая реализация React Suspense заключается в следующем: передать заглушку контента и JS скрипт для замены данных. При загрузке нужных данных передать шаблон нужного контента и вызвать функцию замены данных. Пример:


return new Response(
new ReadableStream({
async start(controller) {
const suspensePromises = []

controller.enqueue(encoder.encode('<html lang="en">'))

/** --snip-- */

// Передача заглушки
controller.enqueue(
encoder.encode('<div id="suspensed:1">Loading...</div>')
)

suspensePromises.push(
computeExpensiveChunk().then((content) => {
// Шаблон нужного контента
controller.enqueue(
encoder.encode(
`<template id="suspensed-content:1">${content}</template>`
)
)
// Передать скрипт для замены заглушки на нужный контент
controller.enqueue(encoder.encode(`<noscript>unsuspense('1')</noscript>`))
})
)

/** --snip-- */
},
})
)


Если во время стриминга HTML браузер получит JS, то он его выполнит.

Также с помощью HTML стриминга и View Transitions API можно реализовать SPA приложение, заменяя HTML без перезагрузки страницы. Если использовать алгоритмы сравнения DOM, то вместо полной замены HTML на странице можно будет менять его частично, т.е. лишь те места, которые поменялись.

https://aralroca.com/blog/html-node-streaming
👍92
Лучшие практики по написанию тестов с React Testing Library

Правильно написанные тесты не только предотвращают ошибки в коде, но и в случае React Testing Library улучшают доступность компонента. В своем блоге Алекс Хоменко собрал лучшие практики по написанию тестов с React Testing Library, кратко о них:

- Использовать *ByRole селекторы. Они наиболее надежны и работают для многих элементов. Использование *ByRole селекторов приучают к написанию доступных компонентов. Пример:


it("should submit correct form data", async () => {
/** --snip-- */

await user.type(screen.getByRole("textbox", { name: "Name" }), "Test");
await user.click(screen.getByRole("button", { name: "Sign up" }));

/** --snip-- */
});


- Использовать userEvent вместо fireEvent для симуляции событий. Хотя fireEvent работает во многих случаях, это легкая обертка над dispatchEvent. Также, в новых версиях React Testing Library рекомендуется инициализировать userEvent перед использованием:


it("should save correct data on submit", async () => {
/** --snip-- */
const user = userEvent.setup();

await user.type(screen.getByRole("textbox", { name: "Name" }), "Test");
/** --snip-- */
});


- Использовать findBy* вместо waitFor. Бывают случаи, когда элемент, который пытаемся использовать, недоступен при первоначальном рендеринге и появится после получения данных из API. Для таких сценариев можно использовать асинхронный селектор findBy*, который внутри себя использует waitFor. Пример:


it("renders without breaking", async () => {
render(<ListPage />);
expect(
await screen.findByRole("heading", { name: "List of items" }),
).toBeInTheDocument();
});


- Исправление ошибки Fixing the "not wrapped in act(...)" warnings. Одной из причин ошибки может быть использование getBy* вместо findBy*, поскольку рассматриваемый элемент или компонент обновляется после асинхронного действия.
Еще одна из причин данной ошибки может быть неправильная работа с таймерами. Если компонент использует таймеры, то используйте моки для таймеров (useFakeTimers).

https://claritydev.net/blog/improving-react-testing-library-tests
👍16🔥1
This media is not supported in your browser
VIEW IN TELEGRAM
Million Lint вышел в публичную бету

Million Lint – это VSCode расширение, которое выявляет медленный код и предлагает способы его оптимизации. Это как ESLint, но для производительности.

С помощью линтера можно узнать сколько времени в среднем занимает рендер компонента, как в React Devtools, но прямо в коде редактора. Также линтер дает возможность отследить размер бандла, сетевые запросы, пропсы, вызовы хуков.

Для того чтобы использовать линтер, надо установить VSCode расширение и добавить MillionCompiler, который выполняет динамический анализ отдельных компонентов. Пример настройки для Vite приложения:


import MillionCompiler from "@million/lint";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
plugins: [MillionCompiler.vite(), react()],
});


https://million.dev/blog/lint
👍24🔥7
В React 19 добавят diff предупреждения гидратации

В React 19 планируют писать в консоле diff предупреждения гидратации. На данный момент, в React 18, если во время гидратации клиентская и серверная разметка отличаются, то React просто предупреждает о том, что есть несоответствие UI между клиентом и сервером.

В React 19 при определении diff будет учитываться работа хука useSyncExternalStore, который позволяет возвращать разный результат для клиента и сервера.

Вместо нескольких предупреждений в консоле будет одно общее, пример:


Warning: A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:

--snip--

<Mismatch isClient={true}>
<div className="parent">
<main
+ className="child client"
- className="child server"
+ dir="ltr"
- dir="rtl"
>


https://github.com/facebook/react/pull/28512
👍185
Встроенная база данных Astro DB

В Astro появилась встроенная локальная база данных Astro DB. В ней можно хранить статьи, комментарии и пользователей, создавая разные таблицы.

БД сделана на основе libSQL. Изначально планировалось использовать SQLite, но из-за того, что SQLite нельзя использовать в браузере, выбрали libSQL. Его можно запустить в StackBlitz через WASM.

Astro DB устроена таким образом, что при каждом запуске дев-сервера БД (data.db) создается заново. При запуске команды astro dev происходит следующее:

- Создается пустая БД .astro/data.db
- Создается схема БД из db/config.ts
- Заполняется БД из db/seed.ts

Это дает вам простую, воспроизводимую БД, которая в дальнейшем служит единственным источником данных для приложения. Пример файла конфигурации БД db/config.ts:


import { defineDb, defineTable, column } from 'astro:db';

const Comment = defineTable({
columns: {
author: column.text(),
body: column.text(),
}
})

export default defineDb({
tables: { Comment },
})


При работе с БД доступны основные операции записи, чтения, фильтрации, сравнения и объединения. Пример запроса:


import { db, eq, Comment, Author } from 'astro:db';

const comments = await db.select()
.from(Comment)
.innerJoin(Author, eq(Comment.authorId, Author.id));


https://astro.build/blog/astro-db-deep-dive/
👍13👎1🔥1
React Data Grid: быстрый датагрид как Excel

react-data-grid – компонент для отображения данных в датагриде. Компонент поддерживает виртуализацию, tree-shaking, используется только одна внешняя зависимость, а gzip размер пакета 14.5 КБ.

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


import 'react-data-grid/lib/styles.css';

import DataGrid from 'react-data-grid';

const columns = [
{ key: 'id', name: 'ID' },
{ key: 'noscript', name: 'Title' }
];

const rows = [
{ id: 0, noscript: 'Example' },
{ id: 1, noscript: 'Demo' }
];

function App() {
return <DataGrid columns={columns} rows={rows} />;
}


https://adazzle.github.io/react-data-grid/
👍28
В React нельзя сделать типобезопасным children

При работе с TypeScript и JSX иногда появляются ситуации, когда надо ограничить дочерние элементы определенным типом. Например, есть компонент Select, который принимает компоненты Option как дочерние. Вы хотите убедиться, что компоненты Option единственные дочерние компоненты, которые передаются в Select:


// Хорошо
<Select>
<Option value="1">One</Option>
<Option value="2">Two</Option>
<Option value="3">Three</Option>
</Select>

// Плохо
<Select>
<div>One</div>
<div>Two</div>
<div>Three</div>
</Select>


Ограничить тип компонента, передаваемого как дочерний, через TypeScript невозможно. Любой <Component /> или HTML тег это JSX.Element.

В качестве заключения стоит отметить, что главная магия React в композиции. Можно компоновать компоненты любым удобным способом. Ограничивая children у компонента, вы нарушаете эту возможность компоновки.

Возможно вместо потенциального ограничения типа children можно использовать другой проп:


<Select
options={[
{ value: "1", label: "One" },
{ value: "2", label: "Two" },
{ value: "3", label: "Three" },
]}
/>


https://www.totaltypenoscript.com/type-safe-children-in-react-and-typenoscript
👍22👎1
Оптимизация JavaScript для развлечения и ради прибыли

Обзор распространенных методов оптимизации JavaScript, которые могут быть полезны и для React приложений. Для каждого метода автор приводит бенчмарк для сравнения скорости выполнения. Кратко, о чем пишет автор:

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

- Избегайте сравнение строк. Для сравнения строк используется функция strcmp(a, b), сложность которой O(n). Если создаете enum, то лучше не использовать строки:


// Плохо
enum Position {
TOP = 'TOP',
BOTTOM = 'BOTTOM',
}

// Лучше
enum Position {
TOP, // = 0
BOTTOM, // = 1
}


- Избегайте разных форм. Используйте одинаковую форму объекта в одном массиве.

- Избегайте методов объекта/массива. Писать в функциональном стиле через цепочку методов дороже чем в императивном. Пример:


const result =
[1.5, 3.5, 5.0]
.map(n => Math.round(n))
.filter(n => n % 2 === 0)
.reduce((a, n) => a + n, 0)


Для каждой операции будет создаваться копия массива. Цикл запускается 3 раза для разных операций, когда через цикл for запустится один раз.

- Используйте правильные структуры данных. Неправильно подобранная структура данных может ухудшить оптимизацию. Надо знать про существующие Map и Set, а также узнать о связанных списках, приоритетной очереди и деревьях. Например, использование Set.has гораздо быстрее чем Array.includes.

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

https://romgrk.com/posts/optimizing-javanoscript
👎18👍11🔥3
Новый хук useActionState

В React появится новый хук useActionState, который заменит существующий ReactDOM хук useFormState. Этот хук призван исправить некоторую путаницу и ограничения хука useFormState.

Хук useFormState импортировался из ReactDOM и был предназначен только для отслеживания стейта action у формы. Однако не обязательно было использовать его для формы, его можно было использовать для отслеживания любой асинхронной функции.

Команда React переименовала хук useFormState в useActionState, добавила стейт pending в возвращаемый результат и переместила хук в пакет react.

Пример использования хука:


import { useActionState, useRef } from "react";

function Form({ someAction }) {
const ref = useRef(null);
const [state, action, isPending] = useActionState(someAction);

async function handleSubmit() {
await action({ email: ref.current.value });
}

return (
<div>
<input ref={ref} type="email" name="email" disabled={isPending} />
<button onClick={handleSubmit} disabled={isPending}>
Submit
</button>
{state.errorMessage && <p>{state.errorMessage}</p>}
</div>
);
}


Под капотом вызов action вызывает под собой startTransition, код обработчика можно заменить на такой:


const [state, setState] = useState(null);
const [isPending, setIsPending] = useTransition();

function handleSubmit() {
startTransition(async () => {
const response = await someAction({ email: ref.current.value });
setState(response);
});
}


https://github.com/facebook/react/pull/28491
👍9🔥3
Нестабильные тесты в React: обнаружение, предотвращение и инструменты

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

Вот некоторые причины, по которым тесты могут быть нестабильными:

- Внешние зависимости. Например, компонент получает данные по API и отображает ответ. Если в API вернет не то, что ожидал компонент, то тест сломается.

- Проблемы со временем. Например, если тесту надо проверять UI элемент до или после анимации или CSS перехода, то такой тест нестабильный.

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

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

Для предотвращения нестабильных тестов, используйте следующие правила:

- Хорошо структурируйте тесты. Они должны быть независимыми, иметь осмысленное название, должны создавать собственный экземпляр компонента при запуске.

- Используйте моки. Заменяйте внешние зависимости на контролируемые моки. Это придает тесту более предсказуемое поведение.

- Используйте beforeEach и afterEach, чтобы каждый тест начинался с чистого листа.

- Если есть возможность проводить тестирование при помощи снимков, то используйте это.


test("Modal opens", async () => {
render(<Modal />);
fireEvent.click(screen.getByText("Open Modal"));

await waitFor(() => expect(screen.getByTestId("modal")).toBeInTheDocument());
});


https://semaphoreci.com/blog/flaky-react
👍10
Более быстрый React.memo()

В React.memo используется общая функция shallowCompare, которую можно значительно оптимизировать с помощью специальной функции сравнения, которая предполагает, что объекты для сравнения – это React пропсы.

Автор блога romgrk сделал две версии функции проверки пропсов для React.memo(): безопасную и небезопасную. Небезопасная быстрее общей shallowCompare в два раза, вот ее код:


function fastCompareUnsafe(a, b) {
var aLength = 0;
var bLength = 0;
for (var key in a) {
aLength += 1;
if (!Object.is(a[key], b[key])) {
return false;
}
}
for (var _ in b) {
bLength += 1;
}
return aLength === bLength;
}


При сравнении объектов { a: 1, b: undefined }, { a: 1, c: undefined } эта функция вернет true, т.е. скажет что они равны. Для общего сравнения двух объектов данная функция не подходит, но для сравнения пропсов React такая функция будет работать.

https://romgrk.com/posts/react-fast-memo/
👍11
Создаем прогресс загрузки страницы с помощью Transition API

В Build UI рассказали, как можно создать прогресс загрузки страницы с помощью Transition API. В результате получился хук useProgress и компоненты прогресса с Framer Motion:


const { start, done, reset, state, value } = useProgress();


Чтобы показать прогресс загрузки страницы в Next.js, то необходимо использовать Transition API. Пример:


const router = useRouter();
const { start, done } = useProgress();

return (
<Link
onClick={(e) => {
e.preventDefault();
start();

startTransition(() => {
router.push(href);
done();
});
}}
href={href}
>
{children}
</Link>
);


В Next.js router.push не возвращает промис, но использует Transition API. React вызывает все обновления стейта внутри startTransition одновременно в фоне. Если какое-то обновление будет приостановлено, в нашем случае так делает router.push, то React отложит отрисовку UI, пока все обновления не будут завершены.

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

https://buildui.com/posts/global-progress-in-nextjs
👍132
Предоставляйте API платформы вместо обертки

В блоге Джима Нильсена вышла критика в сторону Metadata API в Next.js.

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

Metadata API в Next.js – это обертка для генерации meta-тегов. Под капотом у нее много логики, поэтому ее не так просто дебажить:


export const metadata = {
authors: [
{ name: 'Seb' },
{ name: 'Josh', url: 'https://nextjs.org' }
],
}

// Результат:

<meta name="author" content="Seb" />
<meta name="author" content="Josh" />
<link rel="author" href="https://nextjs.org" />


Как видно из примера, Metadata API не только генерирует мета-теги, но и генерирует link-теги.

Еще одно противоречие в том, что для генерации мета-тега viewport используется свое API:


export const viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
userScalable: false,
}

// Результат

<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>


https://blog.jim-nielsen.com/2024/expose-platform-apis-over-wrapping-them/
👍91
В Waku появился pages router

В Waku появился файловый роутер страниц. Можно создавать новые страницы просто создав файл в ./src/pages. Поддерживаются сегментированные параметры blog/[slug].tsx и шаблоны _layout.tsx для оборачивания дочерних роутов.

Новая страница создается через создание файла с двумя экспортами: дефолтный для компонента и именованный getConfig для функции, которая возвращает объект настройки. В getConfig можно определить метод рендера страницы: динамический SSR и статический SSG.

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


// ./src/pages/blog/[slug].tsx

export default async function BlogArticlePage({ slug }) {
// --snip--
}

const getData = async (slug) => {
// --snip--
};

export const getConfig = async () => {
return {
render: 'static',
staticPaths: ['introducing-waku', 'introducing-pages-router'],
};
};


Статический рендер происходит во время сборки и его можно использовать в шаблонных роутах_layout.tsx, а дочерние роуты рендерить динамически.

https://waku.gg/blog/introducing-pages-router
👍3👎1
onCaughtError и onUncaughtError для обработки ошибок

В React 19 добавятся новые способы обработки ошибок. Это касается новых колбеков для React Root (createRoot и hydrateRoot):

- onCaughtError сообщает об ошибках, обнаруженных Error Boundary
- onUncaughtError сообщает о необнаруженных ошибках
- onRecoverableError теперь использует ES Error Cause, чтобы сообщить об исходной причине ошибок

В документацию добавили много примеров с использованием новых колбеков, например, колбек для необработанных ошибок:


const root = createRoot(container, {
onUncaughtError: (error, errorInfo) => {
if (error.message !== 'Known error') {
reportUncaughtError({
error,
componentStack: errorInfo.componentStack
});
}
}
});
root.render(<App />);


Если использовать onRecoverableError в hydrateRoot, то можно узнать причину ошибки гидратации, пример такого сообщения:


Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:



<App>
<span>
+ Client
- Server


https://github.com/reactjs/react.dev/pull/6742
👍11🔥3👎1
Использование Suspense с React Query

Хук useQuery в React Query возвращает опциональный объект data, он может быть или не быть, зависит от результата загрузки. Если использовать хук useSuspenseQuery, который работает с Suspense, то data будет всегда. Пример использования:


function Example() {
const { data } = useSuspenseQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
});

return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.noscript}</li>
))}
</ul>
);
}

import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";

function Parent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ErrorBoundary fallback={<div>Something went wrong!</div>}>
<Example />
</ErrorBoundary>
</Suspense>
);
}


В примере Suspense ответственен за отображение индикатора загрузки. Если запрос закончится неуспешно, то ErrorBoundary отобразит ошибку.

В этом подходе есть ограничения. Например, если изменится queryKey, то Suspense заново отобразит индикатор загрузки. Если надо чтобы компонент Example продолжал отображаться во время повторной загрузки, то придется дополнительно использовать useTransition или useDeferredValue.

Пример с useTransition:


import { useTransition, useState } from "react";
import { useSuspenseQuery } from "@tanstack/react-query";

function Example() {
const [page, setPage] = useState(1);
const [isPending, startTransition] = useTransition();

const { data } = useSuspenseQuery({
queryKey: ["todos", page],
queryFn: () => fetchTodos({ page }),
});

function handleNextPage() {
startTransition(() => {
setPage((prev) => prev + 1);
});
}

return (
<div>
{isPending && <p>Fetching...</p>}

<ul>{/* ...render todos... */}</ul>
<button onClick={handleNextPage}>Next</button>
</div>
);
}


https://www.teemutaskula.com/blog/exploring-query-suspense
👍8👎4