Заметки про React – Telegram
Заметки про React
3.78K subscribers
34 photos
8 videos
485 links
Короткие заметки про React.js, TypeScript и все что с ним связано
Download Telegram
Полное руководство по кукам в Next.js

В Next.js есть несколько способов использования куки в разных ситуациях. Например, в серверных компонентах в App Router можно прочитать куки, но нельзя их изменять. В Pages Router в getServerSideProps можно не только прочитать, но и установить куки, включая флаг HttpOnly.

В статье написаны разные способы чтения и изменения куки в Next.js. Пример изменения куки в серверных компонентах с помощью серверных действий:

import {cookies} from "next/headers";

export default function Message() {
async function markAsSeen() {
'use server'
cookies().set("viewedWelcomeMessage", "true");
}

const viewedWelcomeMessage = cookies().has("viewedWelcomeMessage")
if (viewedWelcomeMessage) {
return <div>Welcome back!</div>
}

return <form action={markAsSeen}>
<button type="submit">Mark as seen</button>
</form>
}


https://www.propelauth.com/post/cookies-in-next-js
👍9
Почему мы до сих пор используем React HOC

Если вы используете хуки для асинхронного получения данных, например информация о пользователе, то сталкивались с тем, что надо показывать индикатор загрузки информации. Если это надо сделать только в одном компоненте, то можно сделать прямо в нем. Если же это надо сделать в многих местах, то можно воспользоваться HOC, что поможет уменьшить дублирование кода. Пример:

const withHookWithLoadingState = (InnerComponent) => {
// Промежуточный компонент, который передает hookResponse в InnerComponent
const WrapperComponent = () => {
const hookResponse = useHookWithLoadingState()
if (hookResponse.loading) {
return <Loading />
}
return <InnerComponent hookResponseData={hookResponse.data} />
}

return WrapperComponent
}

// Использование HOC
const Component = withHookWithLoadingState(({hookResponseData}) => {
// Тело компонента
})


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

https://www.propelauth.com/post/why-we-have-both-react-hooks-and-hocs
👎6👍51
Самый быстрый способ передать стейт из сервера на клиент

SSR приложения передают стейт на клиент в виде объекта, который парсится на клиенте. Есть несколько способов передать стейт для клиента и важно выбрать наиболее быстрый вариант, т.к. передача больших стейтов на клиент может стать медленным процессом.

В блоге PerfPlanet провели исследование и определили самый быстрый способ передачи стейта на клиенте. Варианты передачи стейта и название способа:

// "Plain object":
window.__STATE__ = {"foo":"bar"}

// "Invalid mime type":
<noscript type="mime/invalid" id="myState">{"foo":"bar"}</noscript>
window.__STATE__ = JSON.parse(window.myState.innerHTML)

// "Just parse":
window.__STATE__ = JSON.parse("{\"foo\":\"bar\"}")


По результат бенчмарков, "Invalid mime type" парсится на 50% быстрее и занимает на 6% меньше памяти браузера. Это связано с тем, что во время парсинга страницы, браузер не парсит и не компилирует код внутри скрипта с типом "mime/invalid". В "Just parse" строка стейта является частью JS кода, поэтому перед выполнением браузер ее парсит и компилирует, что ухудшает производительность.

https://calendar.perfplanet.com/2023/fastest-way-passing-state-javanoscript-revisited/
🔥14👍7
Полиморфизм в React: 2 паттерна, о которых нужно знать

Если вы делаете UI компоненты, то должны знать о полиморфизме. Пример полиморфизма в React - это когда кнопка может выступать в качестве ссылки. При правильном использовании, полиморфизм избавляет от необходимости поддерживать множество вариантов и дает большую гибкость компонента.

В статье рассказывают про два наиболее известных способа реализации полиморфизма в React: через проп as и asChild. Пример реализации asChild на TypeScript с помощью библиотеки radix:

import { Slot } from "@radix-ui/react-slot";

export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean;
}

export function Button({ asChild, ...props }: ButtonProps) {
const Tag = asChild ? Slot : "button";
return <Tag className="button" {...props} />;
}


https://www.bekk.christmas/post/2023/1/polymorphism-in-react
👍11🔥5
Как JSX преобразуется в HTML

В блоге Дэна Абрамова появилась статья с названием «Цепная реакция». В ней описано как JSX преобразуется в HTML код, понятный для браузера. В статье приводится пример упрощенной функции, которая трансформирует JSX:

function translateForBrowser(originalJSX) {
const { type, props } = originalJSX;
if (typeof type === 'function') {
const returnedJSX = type(props);
return translateForBrowser(returnedJSX);
} else if (typeof type === 'string') {
return {
type,
props: {
...props,
children: translateForBrowser(props.children)
}
};
}
}

Пример как эта функция работает:

function Greeting({ person }) {
return (
<p className="text">
Hello, <i>{person.firstName}</i>!
</p>
);
}

const originalJSX = <Greeting person={alice} />;
console.log(originalJSX.type); // Greeting
console.log(originalJSX.props); // { firstName: 'Alice', birthYear: 1970 }

const browserJSX = translateForBrowser(originalJSX);
console.log(browserJSX.type); // 'p'
console.log(browserJSX.props); // { className: 'text', children: ['Hello', { type: 'i', props: { children: 'Alice' }, '!'] }


С помощью функции translateForBrowser можно преобразовать JSX код в HTML строку или в набор инструкций для обновления существующего DOM.

https://overreacted.io/a-chain-reaction/
👍26🔥3👎2
Хитрости React: быстро, удобно и весело

Автор React роутера Wouter поделился хитростями для React, которые улучшают производительность и уменьшают размер бандла. Автор рекомендует их применять, когда библиотека уже выпущена и начинается этап доработок, в ходе которых можно улучшить метрики производительности. О чем пишет автор:

- Используйте React.cloneElement для композиций. Если нет доступа к элементу, но нужно изменить его пропсы, то можно использовать клонирование элемента:

cloneElement(<img />, { src: "keyboard-cat.webp" }) 
// <img src="keyboard-cat.webp" />


Также с помощью клонирования можно установить свой реф и получить доступ к DOM элементу.

- Улучшайте производительность. По умолчанию компоненты в React не являются «чистыми». Используйте React.memo, чтобы предотвратить лишние ре-рендеры, и useMemo, useCallback для мемоизации пропсов.

- Используйте useSyncExternalStore для подписки на изменения внешнего состояния. Из-за конкурентного рендеринга разные части приложения могут не соответствовать глобальному состоянию. Если вы используете внешнее состояние напрямую, то могут возникнуть нежелательные сбои, поэтому используйте хук useSyncExternalStore. Например, в Wouter хуки useBrowserLocation, useHashLocation, useSearch используют useSyncExternalStore.

https://molefrog.com/notes/react-tricks
👍111
Оптимизация производительности в React

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

- Узкое место в приложении может замедлить работу всей системы, и, чтобы найти его, есть несколько инструментов: React Developer Tools, Chrome DevTools -> вкладка Performance, React Profiler API.

- Один из вариантов ускорения работы медленного компонента – мемоизация React.memo.

- Обращайте внимание на локальный стейт компонента. Минимизация изменения стейта предотвращает от лишних ре-рендеров. Убедитесь, что обновление стейта происходит только по необходимости.

- Используйте ленивую загрузку и разделение кода. Это ускорит загрузку сайта. Например, если только на одной странице используется большая библиотека, то не надо грузить код библиотеки в общий бандл, можно разделить код по страницам.

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

https://www.sitepoint.com/react-performance-optimization/
👍9
Легкий способ исправить ошибки гидратации – React Hydration Overlay

В блоге Builder.io рассказали про библиотеку React Hydration Overlay, которая помогает исправить ошибки гидратации. Это библиотека пока что работает только для Next.js.

Сейчас в Next.js при возникновении ошибки гидратации появляется не очень информативное сообщение, в котором не совсем понятно в каком месте не совпадают элементы на клиенте и в SSR. Библиотека более точно показывает в каком месте есть несовпадение.

Работает она следующим образом: скрипт сохраняет HTML полученный с сервера и когда происходит ошибка гидратации сохраняет текущий HTML. После чего происходит сравнение двух HTML строк и поиск отличий. То что отличается от серверного HTML и есть причина ошибки гидратации, которую надо исправить.

https://www.builder.io/blog/announcing-react-hydration-overlay
👍12🔥72
Релиз React Aria 1.0

React Aria – библиотека компонентов от Adobe, более 40 компонентов, поддерживающих доступность и интернационализацию, а также кастомизацию стилей.

React Aria поддерживает доступный drag and drop, управление клавиатурой, встроенная валидация формы, изменение размеров колонок таблиц и многое другое.

Компоненты оптимизированы для взаимодействия на разных устройствах и поддерживают мышь, сенсорный экран, клавиатура и screen reader.

Компоненты в React Aria декомпозированы, поэтому можно самому настраивать нужный вид и компоновать их:

<ComboBox>
<Label>Permissions</Label>
<Group>
<Input />
<Button>▼</Button>
</Group>
<Popover>
<ListBox>
<ListBoxItem>Read Only</ListBoxItem>
<ListBoxItem>Edit</ListBoxItem>
<ListBoxItem>Admin</ListBoxItem>
</ListBox>
</Popover>
</ComboBox>


https://react-spectrum.adobe.com/react-aria/index.html
👍20
Конкурентный React, внешний стор и «разрыв» стейта

В блоге InterBolt рассказали, что такое «разрыв» стейта, как это связано с библиотеками управления глобальным стейтом, используемых в режиме конкурентного рендеринга, и какие есть решения и компромиссы.

«Разрыв» стейта возникает, когда два и более компонентов рендерят разный UI от одного и того же источника данных.

Библиотеки, использующие внешний стор, дают пользователю точечную реактивность для предотвращения лишних ререндеров, но не поддерживают конкурентный рендеринг, чтобы избежать «разрыва» состояния.

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

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

Для решения проблемы «разрыва» стейта при доступе к внешнему стору можно использовать хук useSyncExternalStore. С помощью него можно получить доступ к внешнему стейту и использовать конкурентный рендеринг.

https://interbolt.org/blog/react-ui-tearing/
👍9🔥2
Puck: self-hosted drag-and-drop редактор лендинга на React

Puck - это модульный, с открытым исходным кодом, визуальный редактор лендинга на React. Аналог Tilda, компоненты которого можно настроить под себя и опубликовать на своем сервере без вендор-лока. Т.к. это React, то можно использовать Next.js для рендера на сервере.

Можно самостоятельно настроить вид компонентов, список полей и их тип. Пример:

const config = {
components: {
HeadingBlock: {
fields: {
noscript: {
type: "text",
},
},
defaultProps: {
noscript: "Hello, world",
},
render: ({ noscript }) => {
return <h1>{noscript}</h1>;
},
},
},
};


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

// Данные по умолчанию
const initialData = {
content: [],
root: {},
};

// Измененные данные в виде JSON
const save = (data) => {};

// Рендер редактора
export function Editor() {
return <Puck config={config} data={initialData} onPublish={save} />;
}


Для рендера страницы надо передать конфиг компонентов и данные:

export function Page() {
return <Render config={config} data={data} />;
}


https://puckeditor.com/
👍15🔥2
Серверные компоненты React: хороший, плохой и уродливый

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

export default async function Page() {
const stuff = await fetch(/* … */);
return <div>{stuff}</div>;
}

Еще удобно сразу обрабатывать данные формы:

<form
action={async (formData) => {
"use server";
const email = formData.get("email");
await db.emails.insert({ email });
}}
>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" />
<button>Send me spam</button>
</form>

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

"use client";
const [formState, formAction] = useFormState(saveEmailAction);

<form action={formAction}>
<!-- snip -->
</form>

Такой подход уже не так удобен, т.к. близкий код уже не располагается рядом.

Еще одним минусом серверных компонент стал манки-патчинг fetch API в Next.js:
- по умолчанию все запросы кэшируются.
- нет доступа к объекту Request вне мидлвара.

Также, с серверными компонентами вырос размер JS бандла. В Next.js 12 базовый бандл весил ~70КБ, сейчас Next.js 14 весит 85-90КБ в сжатом виде. Не в сжатом виде, бандл весит 300КБ.

https://www.mayank.co/blog/react-server-components
👍82
Углубленный взгляд на ReactDOM.flushSync и для чего он нужен

flushSync – простое API чтобы заставить React выполнить всю ожидающую работу и синхронно обновить DOM. Ожидающая работа – изменение стейта в компонентах.

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

function Demo() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);

function handleClick() {
flushSync(() => {
setCount((c) => c + 1);
});
flushSync(() => {
setFlag((f) => !f);
});
}

{/* --snip-- */}
}


При вызове колбека handleClick, каждое выполнение flushSync заставить React перерендерить компонент и синхронно обновить DOM, вместо одного раза в конце колбека.

Также, flushSync используют, когда после изменения стейта нужно сразу обратиться в DOM, например для измерения DOM узла. Еще пример использования – скролл к элементу сразу после обновления стейта.

https://julesblom.com/writing/flushsync
👍202
Селектор контекста через React.use и React.useMemo

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

Для оптимизации контекста можно использовать библиотеку use-context-selector. Библиотека предоставляет селектор для контекста и ограничивает ререндеры компонента только на изменение выбранных в селекторе значений. Пример:

import { createContext, useContextSelector } from 'use-context-selector';

const context = createContext(null);

const Counter1 = () => {
const count1 = useContextSelector(context, v => v[0].count1);
const setState = useContextSelector(context, v => v[1]);
const increment = () => setState(s => ({
...s,
count1: s.count1 + 1,
}));

{/* --snip-- */}
};

const StateProvider = ({ children }) => (
<context.Provider value={useState({ count1: 0, count2: 0 })}>
{children}
</context.Provider>
);


Если библиотека use-context-selector не подходит, то скоро появится возможность оптимизировать контекст через useMemo и функцию use. Пример:

import { createContext, useMemo, use } from 'react';

const MyContext = createContext({
foo: '',
bar: 0,
});

const useFoo = () => {
return useMemo(() => {
const { foo } = use(MyContext);
return foo;
}, []);
};

На данный момент функция use доступна в canary версии React. Как видно по примеру, в useMemo не надо ничего передавать в массив зависимостей, React сам поймет когда нужно произвести ререндер когда foo изменится.

https://interbolt.org/blog/react-use-selector-optimization/
👍17👎3
Продвинутые фичи Next.js

Подборка менее известных фич в Next.js, о которых полезно знать. Краткий список:

- Автоматическая типизация <Link />:

const nextConfig = {
experimental: {
typedRoutes: true,
},
}

В настройках можно включить типизацию ссылок в компоненте <Link />. Сгенерится d.ts файл, в котором будут типы созданных роутов, и можно будет безопасно использовать href в <Link />.

- Встроенные модули для сторонних скриптов Google. В Next.js есть встроенные компоненты для распространенных скриптов Google в @next/third-parties/google: GoogleTagManager, GoogleAnalytics, GoogleMapsEmbed, YouTubeEmbed.

- Простая генерация sitemap.xml. Нужно создать страницу sitemap.ts и вернуть в нем массив метаданных о страницах:

import { MetadataRoute } from 'next'

export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://acme.com',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 1,
}
]
}

- Если в src расположить файл instrumentation.ts, то Next.js будет запускать экспортируемую функцию register при запуске сервера. Может пригодиться, если надо запускать внешний код при запуске приложения.

- generateViewport (app router) для генерации мета-тегов в серверных компонентах:

export function generateViewport({ params }) {
return {
themeColor: 'black',
}

/*
<meta name="theme-color" content="black" />
*/
}

https://codedrivendevelopment.com/posts/rarely-known-nextjs-features
👍14👎1
Настройка отчетов о «мягкой» навигации в React

Отчет о «мягкой» навигации – это экспериментальная фича Google Web Vitals. С ее помощью можно измерять Core Web Vitals и другие метрики для динамических URL в SPA. «Мягкая» навигация происходит при изменении URL с помощью JavaScript, что типично для SPA приложений.

Что нужно сделать, чтобы подключить отчеты в приложении:

1) Включить экспериментальную фичу chrome://flags/#enable-experimental-web-platform-features.

2) Установить web-vitals с «мягкой» навигацией npm i web-vitals@3.5.0-soft-navs-10/

3) Создать функцию для отчетов в src/reportWebVitals.js и вызвать ее в src/index.js:


const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ onTTFB, onFCP, onLCP, onCLS, onFID, onINP }) => {
onTTFB(onPerfEntry, {reportSoftNavs: true});
onFCP(onPerfEntry, {reportSoftNavs: true});
onLCP(onPerfEntry, {reportSoftNavs: true});
onCLS(onPerfEntry, {reportSoftNavs: true});
onFID(onPerfEntry, {reportSoftNavs: true});
onINP(onPerfEntry, {reportSoftNavs: true});
});
}
};

export default reportWebVitals;


https://www.debugbear.com/blog/soft-navigations-react-tutorial
👍5🔥1
Релиз Glide Data Grid 6.0

Вышел релиз Glide Data Grid 6.0 – датагрида на канвасе. Основные новые фичи:
- Экспериментальная поддержка кинетического скролла на iOS.
- Улучшение производительности при обновлении большего количества данных за раз.
- Улучшение ячейки UriCell – подчеркивание ссылки при наведении.
- Показ скелетонов в ячейках.

Основное преимущество Glide Data Grid – возможность отрендерить миллион строк, используя ленивый рендеринг. Также для сохранения батареи устройства используется нативный скроллинг.

Т.к. датагрид отображается на канвасе, то есть ограничение в отображаемых данных. Из коробки библиотека поддерживает основные типы ячеек, но если понадобится сделать кастомный тип ячейки, то это будет сложнее, чем для HTML датагридов.

Базовый пример как использовать Glide Data Grid:


const columns: GridColumn[] = [
{ noscript: "First Name", width: 100 },
{ noscript: "Last Name", width: 100 }
];

function getData([col, row]: Item): GridCell {
const person = data[row];

if (col === 0) {
return {
kind: GridCellKind.Text,
data: person.firstName,
allowOverlay: false,
displayData: person.firstName
};
} else if (col === 1) {
return {
kind: GridCellKind.Text,
data: person.lastName,
allowOverlay: false,
displayData: person.lastName
};
} else {
throw new Error();
}
}

export default function App() {
return (
<DataEditor columns={columns} getCellContent={getData} rows={data.length} />
);
}


https://grid.glideapps.com/
👍9👎1
Гайд по композиции серверных компонентов

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


function Page() {
return (
<Card>
<Details city={...} />
<Tags tags={...} />
<Footer>
<AddToFavorites />
<Button />
</Footer>
</Card>
);
}


При композиции серверных и клиентских компонентов есть несколько особенностей, которые стоит учитывать:

- Возможна передача только отрендеренных компонентов при передаче серверных компонентов в клиентский:


// Так можно
<ClientCard footer={<ServerFooter />} />

// Так нельзя
<ClientCard Footer={ServerFooter} />


- Нельзя передавать пропсы через React.cloneElement в клиентских компонентах, которые принимают серверные. Связано это с тем, что серверные компоненты рендерятся перед клиентскими, поэтому передача пропсов через React.cloneElement не сработает.

- Не рекомендуется использовать серверные компоненты в render props. Пример того, как делать не надо:


<ClientCard
renderFooter={async (clientProps) => {
"use server";
return <ServerFooter {...clientProps} />;
}}
/>

function ClientCard({ renderFooter }) {
const [count, setCount] = useState(0);
return (
<div>
...
<Suspense>{renderFooter({ count })}</Suspense>
</div>
);
}


Каждый ререндер ClientCard вызовет render prop renderFooter, который спровоцирует сетевой запрос на сервер за обновленным состоянием серверного компонента <ServerFooter />.

https://frontendatscale.com/blog/donut-components/
👍81
Запуск локального React приложения в Android и iOS приложения

Гайд по запуску локального React приложения на мобильном телефоне, без доступа к интернету и запросов на сервер. В туториале по шагам описано, как забандлить React приложение на Vite в Anrdoid/iOS приложение и отобразить его через WebView.

Android: https://ditto.live/blog/run-react-locally-in-android
iOS: https://ditto.live/blog/run-react-locally-in-ios-app
👍93
Новые хуки в React 19

Помимо развития серверных компонентов в React добавляются новые хуки для получения данных и для работы с формой. Кратко о них:

- use(Promise). Хук для получения данных. Теперь при использовании fetch не надо дополнительно использовать useEffect. Достаточно хука use, пример:


import { use } from 'react';

function MessageComponent() {
const message = use(fetch('…'));
// ...
}


Вместе с хуком use можно использовать Suspense для показа состояния загрузки. Также в отличие от других хуков, хук use можно использовать внутри циклов и условных операторов.

- use(Context). Хук для чтения контекста. Также можно использовать внутри циклов и условных операторов. Можно оптимизировать получение значения контекста, используя хук useMemo:


import { use } from 'react';

function HorizontalRule({ show }) {
if (show) {
const theme = useMemo(() => use(MyContext).theme, []);
return <hr className={theme} />;
}
return false;
}


- action в форме принимает асинхронную функцию:


const AddToCartForm = () => {
const formAction = async (formData) => {
await addToCart(formData, noscript);
};

return (
<form action={formAction}>
{/* --snip-- */}
</form>
);
};


- useFormState – хук для помощи при работе с action у формы. Принимает action и дефолтный стейт формы, возвращает текущий стейт формы и action для передачи в форму:


import { useFormState } from 'react-dom';
import { action } from './action';

function MyComponent() {
const [state, formAction] = useFormState(action, null);
// ...
return <form action={formAction}>{/* ... */}</form>;
}


- useFormStatus – хук, который возвращает текущее состояние формы. Используется в дочерних компонентах внутри формы:


const { pending, data, method, action } = useFormStatus();


https://marmelab.com/blog/2024/01/23/react-19-new-hooks.html
👍22👎106🔥3
Тип кортежей

В своем блоге Кайл Шевлин описал проблему типизации кортежей, возвращаемых из функции, пример:


function useBool(initialValue = false) {
const [state, setState] = React.useState(initialValue)

const handlers = React.useMemo(
() => ({
/* --snip-- */
}),
[initialValue],
)

return [state, handlers]
}

/**
const result: (
| boolean
| { /* --snip-- */ }
)[]
*/
const result = useBool()


Если посмотреть на тип result, то можно увидеть, что это массив, состоящий из объединения boolean и типа объекта handlers. По сути, TypeScript не знает индекс наших значений в массиве.

Чтобы решить эту проблему, можно указать возвращаемый тип у функции useBool, но более правильно будет добавить as const к возвращаемому кортежу:


function useBool(initialValue = false) {
/* --snip-- */

return [state, handlers] as const
}


Это сообщит TypeScript, что возвращаемый массив будет readonly и никогда не изменится. Поэтому TypeScript корректно определит типы state и handlers в местах использования.

https://kyleshevlin.com/wrangling-tuple-types/
👍181👎1