Заметки про React
3.82K subscribers
34 photos
8 videos
485 links
Короткие заметки про React.js, TypeScript и все что с ним связано

Контакт: @rimlin
Download Telegram
Показ тостов через 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
👎25👍4
React Labs: View Transitions, Activity, и другое

В 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
🔥9👍6👎1
Родители и владельцы в React: провайдеры контекста

Джулс Блом в своем блоге написала про то, что понимание того, как компоненты-родители (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>
)
}


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

🔴 компонент-родитель содержит дочерние компоненты и определяет структурные отношения (вложенность) между компонентами, например в App компонент CounterProvider является родителем для вложенных в него компонентов;
🔴 компонент-владелец рендерит другие компоненты и определяет функциональные отношения, вызывает рендеринг, например это компонент CounterProvider или App.

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

https://julesblom.com/writing/parent-owners-context
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12
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
👍17🔥8👎51
Управление фокусом в React с помощью flushSync

Установить фокус на инпуте сразу в 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
👍292🔥2
Анонс TypeScript Native

Вышла превью версия 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/
👍24🔥42
Почему Error Boundary, а не просто try/catch для компонентов

В 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
🔥13👍86
Остерегайтесь скрытых проблем при работе с 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 к высокочастотному инпуту, например <input type="text"> или <input type="range">.

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

При изменении URL может происходить замена текущей истории или добавлении в историю нового URL. При добавлении URL в историю появляется возможность управлять историей через браузерные кнопки вперед/назад. Это удобно для пользователя, но добавляет сложность для разработки, т.к. появляется два источника управления историей: через UI и браузерные кнопки вперед/назад. Например, при открытии модального окна добавляется в историю ?modalOpen=true. Теперь пользователь ожидает, что при нажатии на браузерную кнопку назад закроет окно.

https://nuqs.47ng.com/blog/beware-the-url-type-safety-iceberg
👍111
Реактивность – это легко

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

Автор статьи столкнулся с этим в MUI X Data Grid: клик по одной ячейке вызывал ре-рендер всех остальных. Решение — точечная подписка через селекторы

Вместо того, чтобы хранить состояние в useState и передавать его через Context, можно использовать внешний Store и специальный хук useSelector. Идея проста:

🔴 Store — это обычный класс, который хранит состояние, умеет подписывать на обновления (subscribe) и уведомлять подписчиков, когда данные изменились (update). Он живёт вне рендера React.

🔴 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.iss.onemo.

https://romgrk.com/posts/reactivity-is-easy/
Please open Telegram to view this post
VIEW IN TELEGRAM
👎18👍51