Заметки про React – Telegram
Заметки про React
3.78K subscribers
34 photos
8 videos
485 links
Короткие заметки про React.js, TypeScript и все что с ним связано
Download Telegram
Гайд по композиции серверных компонентов

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


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
Как начинать проект на React в 2024 году

В своем блоге Робин Вирух сделал ежегодный обзор как начинать новый проект на React в 2024 году. Вот его предложения:

- React и Vite. Если вы раньше использовали Create React App, то сейчас лучше перейти на Vite. Он быстрее, более активно поддерживается и схож с CRA. В нем также нет какого-то фреймворка, на основе которого строится приложение.

- Next.js или Remix. Если нужен готовый фреймворк с SSR/SSG. Под капотом много готовых решений для облегчения разработки, и можно легко добиться высоких показателей производительности, если все правильно делать.

- Astro. Фреймворк для создания MPA сайтов. Этот случай подходит, если делаете сайт с упором на контент: блог, портфолио, лендинги. Ориентирован на рендер на сервере и по умолчанию не поставляет JS в браузер.

https://www.robinwieruch.de/react-starter/
👍12👎41
Мысли про Remix

Возможности React растут, а технические подходы к созданию UI стали более сложными, появляются новые «мета-фреймворки», которые позволяют быстрее создавать более производительные и масштабируемые приложения, прилагая меньшие усилия к разработке. На данный момент есть два популярных «мета-фреймворка» – Next.js и Remix.

Соломон Хоук поделился своими мыслями про Remix. Что нравится в Remix:

- Хорошая документация, в которой легко ориентироваться, есть гайды и темам и справочник API.

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

- В Remix уделяется внимание стандартам веб API, например используется Response и FormData.

- Для большинства рабочих кейсов не надо устанавливать стейт-менеджер и библиотеку для получения данных (типа react-query). Хватает встроенного API, например loader для получения данных.

Также автор описал вещи, которые не нравятся в Remix:

- Remix использует esbuild, но не дает доступа к его конфигурации.

- Получение данных связано с роутом через loader. Это может стать проблемой, если хотите получать данные на уровне компонента. Как вариант, использовать react-query.

- Проблема с ESM модулями. Remix бандлит сервер в формате CommonJS и по умолчанию не бандлит зависимые модули.

https://www.viget.com/articles/thoughts-on-remix/
👍9
Улучшаем типизацию роутинга в Next.js

В Next.js есть экспериментальная фича для генерации типов роутов:

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


Однако, этот способ не всегда применим, он позволяет только проверить правильность написания пути роута в href у Link:

// Валидно
<Link href="/about" />

// Ошибка
<Link href="/aboot" />


У typedRoutes есть несколько ограничений:

- нет валидации типа query params и проверок в runtime.
- нет подсказок для динамических роутов.
- нет валидации типа при передаче href как объекта <Link href={{href: '/about', query: {ref: 'hello'}}} />
- нет валидации типа для useParams() и useSearchParams()

Чтобы обеспечить валидацию типа и проверок в runtime, автор статьи предлагает использовать Zod и создал утилитарную функцию makeRoute. Пример как используется makeRoute:

Динамические роуты:

export const OrgParams = z.object({orgId: z.string()})

export const Routes = {
home: makeRoute(({orgId}) => `/org/${orgId}`, OrgParams)
}

<Link href={Routes.home({orgId: 'g4eion3e3'})} />


useParams с типизацией:

// type = {orgId: string}
const params = Routes.home.useParams()


Query параметры:

export const SignupSearchParams = z.object({
invitationId: z.string().optional().nullable(),
})

export const Routes = {
signup: makeRoute(() => "/signup", z.object({}), SignupSearchParams),
}

<Link href={Routes.signup({}, {search: {invitationId: '8haf3dx'}})} />


https://www.flightcontrol.dev/blog/fix-nextjs-routing-to-have-full-type-safety
👍91
Использование Intersection Observer в React

react-intersection-observer – библиотека для использования Intersection Observer в React. Можно использовать через хук useInView и через компонент InView:


const Component = () => {
const { ref, inView, entry } = useInView({
/* Optional options */
threshold: 0,
});

return (
<div ref={ref}>
<h2>{`Header inside viewport ${inView}.`}</h2>
</div>
);
};


Поддерживаются параметры Intersection Observer v2: trackVisibility и delay.

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

https://github.com/thebuilder/react-intersection-observer
👍18
Релиз Storybook 8 Beta

Вышел релиз Storybook 8 Beta. Какие он принес изменения:

- Значительное улучшение производительности. Поменяли компилятор, вместо Babel теперь SWC. Для проектов на React автоматически генерируются контролы с помощью react-docgen, это ускорило время запуска на 20%.

- Обновили UI на мобильных устройствах. Теперь на мобильных устройствах боковая панель будет появляться снизу.

- Улучшили утилиты для тестирования. Объединили @storybook/jest и @storybook/testing-library в один модуль @storybook/test, который основан на Vitest.

- Поддержка React Server Components. Пока что в историях можно использовать RSC только в Next.js проектах.

- Обновили версии основных зависимостей. Для Storybook 8 потребуется Node.js 18+, Vite 5 и Lit 3 версии.

https://storybook.js.org/blog/storybook-8-beta/
👍15👎1
LLRT – среда выполнения от AWS

LLRT – облегченная среда выполнения JS от AWS. LLRT обеспечивает в 10 раз более быстрый запуск приложения и в 2 раза более низкую общую стоимость по сравнению с другими средами, работающими в AWS Lambda. По времени запуска LLRT быстрее Node.js, Bun или Deno.

LLRT написан на Rust и использует QuickJS в качестве JS движка. Хоть LLRT и не позиционирует себя в качестве замены Node.js, его API частично совместимо с API Node.js.

На данный момент вместе с LLRT есть 14 известных мне сред выполнения JS:

- Node.js (V8)
- Deno (V8)
- Bun (JavaScript core - WebKit)
- Workerd (Cloudflare, V8)
- LLRT (AWS, V8)
- WinterJS (Wasmer, Spider Money)
- Edge Runtime (Vercel, V8)
- Lagon (Vercel Acquired, V8)
- Compute @​edge (Fastly, Spider Monkey)
- Edge Compute (Alibaba V8)
- Hermes (Facebook)
- Bloomberg (v8, Internal)
- Bytedance (v8, Internal)
- txiki (QuickJS)

https://github.com/awslabs/llrt
👍11🔥21
Релиз Million.js 3

Вышел релиз Million.js 3. Подробнее что это написано здесь.

Какие изменения в новой версии:

- Улучшенная производительность. Для SSR приложений основной проблемой производительности была гидратация, ее сложность была O(n), где n – количество DOM узлов. В Million.js 3 уменьшили сложность гидратации до O(d), где d – количество динамических DOM узлов, d ≤ n:


// Million.js 3 гидратирует только handleClick и count
<div>
<h1>Hello, world!</h1>
<button onClick={handleClick}>{count}</button>
</div>


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

https://million.dev/blog/million-3
👍7👎2
Над чем работают в React Labs

В блоге React вышел новый пост о статусе работы над текущими проектами. Кратко, о чем написали:

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

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

- Actions. Раньше предполагалось делать Actions, которые выполняют код на сервере – Server Actions. Сейчас API расширили и он стал называться просто Actions, поддерживая выполнение действия и на клиенте:


<form action={search}>
<input name="query" />
<button type="submit">Search</button>
</form>


Функцию в action можно будет определить как на сервере, используя директиву 'use server', так и на клиенте. При работе с action будут доступны хуки отслеживания состояния формы useFormStatus и useFormState.

- Директивы "use client" и "use server" будут определять «точки разделения» между окружениями. Директива "use client" будет сообщать бандлеру о необходимости генерации <noscript> (как в Astro Islands), а "use server" сообщит бандлеру о необходимости генерации POST endpoint (как в tRPC Mutations).

- Document Metadata. Появится встроенная поддержка для рендера <noscript>, <meta>, <link> в любом месте дерева компонентов, как в React Helmet.

- Загрузка ресурсов. Интеграция Suspense с жизненным циклом загрузки ресурсов, таких как стили, шрифты и скрипты.

https://react.dev/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024
👍171👎1
Анимация с учетом направления в Framer Motion

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

Чтобы сделать такую анимацию можно использовать Framer Motion и создать переиспользуемый компонент, который можно использовать для разных ситуаций. Пример использования:


export const DirectionAwareAnimationsReusableSolution = () => {
const [product, setProduct] = useState<Product | null>(null);
const direction: Direction = product ? 'forward' : 'back';

return (<div>
<MotionConfig transition={{ type: 'spring', duration: 0.55 }}>
<AnimatePresenceWithDirection initial={false} mode="sync" direction={direction}>
{product
? <ProductDetails product={product} onBack={() => setProduct(null)} key="details" />
: <ProductsList openProduct={setProduct} key="list" />
}
</AnimatePresenceWithDirection>
</MotionConfig>
</div>);
};

export const ProductDetails = () => {
const animationProps = useDirectionAnimation();

return <motion.div {...animationProps}>
{/* ... */}
</motion.div>
});


https://sinja.io/blog/direction-aware-animations-in-framer-motion
👍5
Как использовать forwardRef с generic компонентами

Одно из ограничений forwardRef в том, что он отключает выведение типа для generic компонентов. Например:


const Table = <T,>(
props: {
data: T[];
renderRow: (row: T) => React.ReactNode;
},
ref: React.ForwardedRef<HTMLTableElement>
) => {
/** --snip-- */
};

const ForwardReffedTable = React.forwardRef(Table);

<Table
data={["a", "b"]}
renderRow={(row) => { // Тип выводится: row: string
return <tr>{row}</tr>;
}}
/>;

<ForwardReffedTable
data={["a", "b"]}
renderRow={(row) => { // Тип не выводится: row: unknown
return <tr>{row}</tr>;
}}
/>;


Чтобы исправить выведение типа для generic компонентов, можно создать утилитарную функцию для вызова forwardRef с правильными типами:


function fixedForwardRef<T, P = {}>(
render: (props: P, ref: React.Ref<T>) => React.ReactNode
): (props: P & React.RefAttributes<T>) => React.ReactNode {
return React.forwardRef(render) as any;
}

// Вызов forwardRef с правильным выведением типа:
const ForwardReffedTable = fixedForwardRef(Table);


https://www.totaltypenoscript.com/forwardref-with-generic-components
👍8🔥3
React будет компилируемым

В блоге React рассказали, что React 19 будет компилируемым. В React Training объяснили, что это значит.

При использовании функциональных компонентов и хуков нужно самостоятельно мемоизировать компоненты, колбеки и переменные для предотвращения ненужных ререндеров. При использовании useCallback или useMemo нужно указывать массив зависимостей в deps, чтобы React пересоздавал функцию при изменении зависимостей.

Если использовать линтер для определения зависимостей exhaustive-deps, то он решает большинство проблем. Однако, зависимости, которые кладутся в массив, должны быть стабильными. Стабильная переменная – это переменная, которая не изменяется, если вы этого не захотите. Для стабилизации массивов, объектов и функций используется useMemo.

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

https://reacttraining.com/blog/react-19-will-be-compiled
👍40👎3
Чему мы научились при переходе на Next.js 14 с серверными компонентами

Medusa.js – open source проект для создания e-commerce приложений. В своем блоге они рассказали, чему научились при переходе стартового шаблона на Next.js 14 с серверными компонентами.

Одна из новых особенностей, которая требует привыкания – механизм получения данных и кэширования. Кэширование работает автоматически для fetch запросов и каждый запрос кэшируется и хранится до ревалидации. Это избавляет от проп-дриллинга и передачи значений через контекст, данные можно сразу получить через fetch из кэша.

Серверные действия – асинхронные функции, которые вызываются на клиенте и выполняются на сервере. Под капотом происходит POST запрос на сервер. Используя серверные действия, не раскрываются API ключи и не надо беспокоиться о CORS.

Серверные компоненты позволяют создать UI, который полностью рендерится на сервере. Серверные компоненты имеют доступ к БД и API, что позволяет сократить количество запросов между клиентом и сервером. Например, по умолчанию в Medusa.js на странице карточки товара все компоненты серверные, за исключением кнопки добавления корзины.

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

https://medusajs.com/blog/client-server-transition-learnings-nextjs-14-server-components/
👍10👎5🔥2
Избегаем ошибки гидратации с помощью 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