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

Контакт: @rimlin
Download Telegram
Скрытые утечки памяти в React: как компилятор не спасет вас

Кевин Шинер недавно писал про useCallback и утечки памяти. Если кратко, то там было написано о том, что замыкания захватывают переменные из внешней области видимости, в React компонентах это означает захват пропсов, стейта или мемоизированных значений. Утечка памяти происходила, когда попеременно вызывался мемоизированный колбек, бесконечно создавая новую область видимости с ссылкой на старую.

Если запустить пример из предыдущей статьи после React Compiler, то утечки памяти не будет. Однако, React Compiler не спасет вас от утечки памяти. Автор изменил пример, чтобы добиться утечки памяти с использованием React Compiler:


export const App = () => {
const [countA, setCountA] = useState(0);
const [countB, setCountB] = useState(0);
const bigData = new BigObject(`${countA}/${countB}`); // 10MB of data

// --snip--
};


Теперь при каждом изменении стейта будет создаваться новый объект bigData. Утечка памяти будет происходить все так же при попеременном вызове мемоизированных колбеков. Как скомпилируется новый пример:


const App = () => {
const $ = compilerRuntimeExports.c(24);
const [countA, setCountA] = reactExports.useState(0);
const [countB, setCountB] = reactExports.useState(0);
const t0 = `${countA}/${countB}`;
let t1;
if ($[0] !== t0) {
t1 = new BigObject(t0);
$[0] = t0;
$[1] = t1;
} else {
t1 = $[1];
}
const bigData = t1;
// ...
};


Чтобы предотвратить утечку памяти можно отказаться от мемоизации колбеков. Однако с React Compiler это сделать не получится.

https://schiener.io/2024-07-07/react-closures-compiler
👍171
Изучите Suspense создавая хук useFetch

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

В результате получился хук, который работает с Suspense и ErrorBoundary: если запрос находится в статусе pending, то покажется fallback Suspense, а если в запросе произойдет ошибка, то покажется fallback ErrorBoundary.


const useFetch = (url) => {
const { fetchUrl } = useContext(fetchCacheContext)
const promise = fetchUrl(url)
const state = readPromiseState(promise)

// Throw pending promise
const isPending = state.status === "pending"
if (isPending) throw promise

// Throw rejection reason
const error = state.reason
if (error) throw error

const data = state.value

// Allow refreshing data
const reload = () => fetchUrl(url, true)

// Only return data now
return [data, reload]
}


https://www.bbss.dev/posts/react-learn-suspense/
👍18👎6
Отличия React и React Native

Хоть React и React Native имеют много общего, внутри они довольно разные. В блоге Expo вышла статья о различиях между React и React Native, которая поможет составить общую картину о разработке нативных приложений на React Native и Expo.

В нативных приложениях нельзя использовать HTML теги, вместо них используются UI примитивы:


// Блок
<div></div>
<View></View>

// Изображение
<img src="https://domain.com/static/my-image.png" />
<Image source={{ uri: "https://domain.com/static/my-image.png" }} />

// Текст
<p>Hello</p>
<Text>Hello</Text>

// Список
array.map(item => <span>>{item.name}</span>)

<FlatList
data={posts}
renderItem={({ item }) => (
<Text>{item.name}</Text>
)}
/>



В нативных приложениях нет глобальных стилей. Все стили инлайновые и передаются в элемент через тег style:


export function MyComponent() {
return (
<View style={styles.container}>
<Text style={styles.greeting}>Set Reminder</Text>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
padding: 24,
},
greeting: {
fontSize: 24
},
});


Также по умолчанию все элементы имеют стиль display: flex. Флексы работают немного иначе чем на вебе, например, flexDirection по умолчанию column (вместо row).

При начале работ на React Native новички допускают ошибки:

🔴 Использование array.map. Если нужно отрендерить список элементов, то вместо array.map нужно использовать FlatList, который умеет оптимально рендерить большие списки и поддерживает виртуализацию.

🔴Используют элемент Button из React Native. Этот элемент плохо стилизуется и не практичен. Вместо него используйте TouchableOpacity.

🔴Используют логические операторы AND (&&). Если случайно в них вернуть '', NaN или 0, то React Native не отрендерит их как текст. В этом случае произойдет крах приложения.

https://expo.dev/blog/from-web-to-native-with-react
Please open Telegram to view this post
VIEW IN TELEGRAM
👍20
react-html

В React появился отдельный модуль react-html, из которого можно импортировать функцию рендера HTML renderToMarkup. Это преемник функции renderToStaticMarkup из react-dom.

Функцию renderToMarkup можно будет использовать как в React Native, так и с React Server Components. Эта функция рендерит HTML код, который не предназначен для гидратации. Функцию renderToMarkup можно использовать для генерации e-mail в Server Action:


import { renderToMarkup } from 'react-html';
import EmailTemplate from './my-email-template-component.js'

async function action(email, name) {
"use server";
// ... in your server, e.g. a Server Action...
const htmlString = await renderToMarkup(<EmailTemplate name={name} />);
// ... send e-mail using some e-mail provider
await sendEmail({ to: email, contentType: 'text/html', body: htmlString });
}


💬 https://github.com/facebook/react/pull/30105

https://github.com/facebook/react/tree/main/packages/react-html
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12
Получение данных с помощью Server Actions в Next.js

В своем блоге Робин Вирух показал опыт использования Server Actions для получения данных в Next.js. В документации Next.js рекомендуют использовать Route Handlers для создания API, например:


import { getPosts } from "@/data";

export async function GET() {
const posts = await getPosts();
return Response.json(posts);
}



"use client";

import { Posts } from "@/posts";
import { useQuery } from "@tanstack/react-query";

const fetchPosts = async () => {
const response = await fetch("/api/posts");
return await response.json();
};

const Page = () => {
const { data, isLoading } = useQuery({
queryKey: ["posts-route-handler"],
queryFn: fetchPosts,
});

/* --snip-- */
}


У этого подхода есть свои минусы: функцию fetchPosts необходимо отдельно типизировать.

В документации Next.js написано, что Server Actions предназначены только для записи (мутация), а не для чтения (запроса) данных. Однако, возможность использовать Server Actions для чтения данных есть, пример:


"use client";

import { getPosts } from "@/data";
import { Posts } from "@/posts";
import { useQuery } from "@tanstack/react-query";

const fetchPosts = async () => {
return await getPosts();
};

const Page = () => {
const { data, isLoading } = useQuery({
queryKey: ["posts-server-action"],
queryFn: fetchPosts,
});

/* --snip-- */
}


Плюсы такого подхода:

🔴Есть возможность переиспользовать getPosts как в серверных компонентах, так и в клиентских компонентах.

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

https://www.robinwieruch.de/next-server-actions-fetch-data/
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10👎3
DRY – источник плохих абстракций

В блоге Свижец Теллер вышла статья про принцип DRY. Самый худший и сложный для сопровождения код, который я видел или писал, был написан в погоне за DRY - Don't Repeat Yourself. Это один из первых принципов проектирования, который изучают разработчики и используют повсеместно.

⭐️ Бездумное использование принципа DRY приводит к созданию плохих абстракций в коде. Из-за чего код становится запутанным и сложно поддерживаемым.

Например, нужно показать навигационное меню:


// 1
const NavigationMenu = () => {
return (
<ul>
<li>
<a href="/about">
<img src="question-icon.png" />
About
</a>
</li>
<li>
<a href="/contact">
<img src="person-icon.png" />
Contact
</a>
</li>
<li>
<a href="/buy">
<img src="cash-icon.png" />
Buy
</a>
</li>
// ...
</ul>
)
}


Если пойти в сторону принципа DRY, то можно написать функцию-фабрику создания пункта меню и использовать массив:


// 2
const NavigationMenu = () => {
const items = [
makeNavItem("/about", "question-icon.png", "About"),
makeNavItem("/contact", "person-icon.png", "Contact"),
makeNavItem("/buy", "cash-icon.png", "Buy"),
// ...
]

return (
<ul>
{items.map((item) => (
<li>
<a href={item.url}>
<img src={item.icon} />
{label}
</a>
</li>
))}
</ul>
)
}


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

Представьте, что поступила задача – подсветить кнопку Buy красным цветом. Однако в текущем виде абстракция оптимизирована на отображение всех кнопок в одинаковом стиле. Можно попытаться добавить новые параметры в функцию-фабрику или попробовать переписать всю абстракцию. Но это усложнит код и будет проще вернуться к первому варианту кода.

https://swizec.com/blog/dry-the-common-source-of-bad-abstractions/
Please open Telegram to view this post
VIEW IN TELEGRAM
👍30👎111
Фича флаги в Next.js

Для Next.js появилась библиотека @vercel/flags/next для работы с фича-флагами в коде. Пример объявления:


import { unstable_flag as flag } from "@vercel/flags/next";

export const showBanner = flag({
key: "banner",
decide: () => false,
});


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


import { showBanner } from "../flags";

export default async function Page() {
const banner = await showBanner();
return (
<div>
{banner ? <Banner /> : null}
{/* other components */}
</div>
);
}


Библиотека не зависит от источника данных для фича-флагов, функция decide может быть асинхронной. Можно использовать как env переменные, так и делать запросы в провайдер данных для фича-флага:


export const showBanner = flag({
key: "banner",
async decide () {
await featureFlagClient.init();
return featureFlagClient.get(this.key);
}
});​​​​​​​​​


https://vercel.com/blog/flags-as-code-in-next-js
👍13🔥81👎1
Гибкая предварительная загрузка данных в SPA

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

В SPA приложениях React чаще всего загрузка данных происходит после монтирования приложения. Этот подход может стать неэффективным по мере масштабирования приложения.

В своей статье автор поделился скриптом для предварительной загрузки данных. Этот скрипт вставляется в тег <head> и исполняется одним из первых. Базовая реализация:


<head>
<script>
// Упрощенная версия, чтобы показать как высокоуровнево работает загрузка
window.__userDataPromise = (async function () {
const user = await (await fetch("/api/user")).json();
const userPreferences = await (await fetch(`/api/user-preferences/${user.id}`)).json();
return { user, userPreferences };
})();
</script>
</head>



function MyApp() {
const [userData, setUserData] = useState();

async function loadUserData() {
setUserData(await window.__userDataPromise);
}

useEffect(() => {
loadUserData();
}, []);
}


В своем блоге автор поделился более низкоуровневой реализацией скрипта.

https://mmazzarolo.com/blog/2024-07-29-data-preloading-script/
👍19
Рефакторинг запутанного компонента

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

- Написать тесты. Если еще тестов нет, то перед рефакторингом надо написать тесты для компонента. В тестах должна проверяться бизнес логика. Делается это для того, чтобы знать, что в процессе рефакторинга ничего не сломается.

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

- Удалить «мертвый» код. Нужно удалить лишние комментарии, неиспользуемые переменные. В этом как раз поможет линтер.

- Сократить количество стейтов. Если в компоненте много useState, то скорее всего он делает слишком много разных вещей. В этом случае его можно разделить на подкомпоненты и связать через пропсы.

- Убрать большие условные рендеры. Если блоки похожи, то их можно объединить в один компонент. Если блоки различны, то лучше разделить их на два компонента и разделить логику между ними.


return !numberIncorrect ? (
// A lot of JSX...
) : (
// Even more JSX...
)


- Следить за ответственностью компонента, чтобы внутри него было как можно меньше действий. В дальнейшем это упростит понимание кода.

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

- Используйте для валидации форм декларативные схемы, например Zod, Yup.

- Не используйте inline стили.

https://alexkondov.com/refactoring-a-messy-react-component/
👍16🔥31👎1
Чеклист безопасности React приложения

Обзор популярных уязвимостей фронтенд приложений в контексте React:

🔴Cross-Site Scripting (XSS). Атака заключающаяся в внедрении вредоносного кода, который вызывается на странице у пользователя. Связано это с тем, что выводимые данные не обрабатывается должным образом. React хорошо защищен от XSS атак, однако все равно уязвим из-за неправильного использования функций, сторонних библиотек или нестандартных реализаций. При использовании dangerouslySetInnerHTML санитизируйте передаваемые данные, например, через библиотеку dompurify:


import DOMPurify from 'dompurify';

const comment = '<script>alert("XSS Attack!")</script>Great article!';

const UserComments = () => {
return (
<div>
<li dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(comment) }} />
</div>
);
};


🔴Content Security Policy (CSP) заголовок. Использование CSP заголовка защитит от большинства веб-атак, включая XSS. CSP заголовок позволяет указать какие ресурсы могут загружаться и исполняться браузером. Позволяет работать в Report-Only режиме, позволяя только собирать обнаруженные проблемы, не применяя правила. Пример:


header Content-Security-Policy "script-src 'self' 'unsafe-inline' https://maps.googleapis.com https://www.googletagmanager.com;"


🔴Cross-Site Request Forgery (CSRF). При CSRF атаке злоумышленник обманом заставляет пользователя отправить запрос, который он не собирался делать. Обычно это делается путем внедрения вредоносных запросов на сайт или e-mail, с которым взаимодействует пользователь. Для противодействия CSRF атаке используются токены, которые генерируются на странице и передаются вместе с запросом. Пример:


function CSRFProtectedForm() {
const [csrfToken, setCsrfToken] = useState('');

{/* Запрос CSRF токена */}

return (
<form onSubmit={submitForm}>
<input type="hidden" name="_csrf" value={csrfToken} />
{/* --snip-- */}
<button type="submit">Submit</button>
</form>
);
}


🔴Переменные окружения. Используйте env переменные и не храните их значения в коде в репозитории.

https://www.trevorlasn.com/blog/frontend-security-checklist
Please open Telegram to view this post
VIEW IN TELEGRAM
👍232👎2
Глубокое погружение в формы в современном React

В своем блоге Кент С. Доддс рассказал, как использовать новые хуки useFormStatus, useActionState и useOptimistic:

🌟 useFormStatus. Если вы знакомы с контекстом в React, то представьте, что тег form - это провайдер данных, а useFormStatus - это хук, который имеет доступ к данным провайдера. С помощью хука можно получить информацию о состоянии формы и переданных данных. Минус хука в том, что, чтобы его использовать, нужно создавать отдельный компонент, который должен быть вложен внутри form.


function JoinButton({ children }: { children: React.ReactNode }) {
const { pending, data } = useFormStatus()

return (
<button type="submit">
{pending ? `Joining as ${data.get('username')}...` : children}
</button>
)
}

function JoinEventForm() {
/** --snip */
return (
<form action={joinEvent}>
<JoinButton>Join</JoinButton>
</form>
)
}


🌟 useActionState. Этот хук позволяет обновить стейт по данным из формы. Разберем пример:


async function joinEvent(
previousState: { joined: boolean },
formData: FormData,
) {
/** --snip */

return { joined: true }
}

function JoinEventForm() {
const [state, formAction, isPending] = useActionState(
joinEvent,
{ joined: false },
JOIN_URL,
)

return (
<div>
{state.joined ? (
<p>See you there!</p>
) : (
<form action={formAction}>
<button type="submit">
{isPending ? 'Joining...' : 'Join'}
</button>
</form>
)}
</div>
)
}


Хук принимает функцию joinEvent, начальный стейт { joined: false } и опциональный параметр пермалинк JOIN_URL. Он возвращает массив с текущим стейтом, функцию триггера действия и флаг – находится ли форма в состоянии отправки. Обратите внимание, что функция joinEvent принимает первым аргументом предыдущее состояние. В каком-то смысле можно представить, что это редьюсер для useReducer.

https://www.epicreact.dev/react-forms
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14
Создание стейт менеджера за 40 строк

Пример простого стейт менеджера для React, работающего через хук useSyncExternalStore.

Хук useSyncExternalStore появился в React 18 и позволяет подписаться на изменение внешнего хранилища. Внешним хранилищем может быть что угодно, например, браузер.


useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)


Значения в внешнем хранилище могут меняться, а с помощью хука эти изменения можно отследить. Пример стейт менеджера:


import { useSyncExternalStore } from 'react';

export type Listener = () => void;

function createStore<T>({ initialState }: { initialState: T }) {
let subscribers: Listener[] = [];
let state = initialState;

const notifyStateChanged = () => {
subscribers.forEach((fn) => fn());
};

return {
subscribe(fn: Listener) {
subscribers.push(fn);
return () => {
subscribers = subscribers.filter((listener) => listener !== fn);
};
},
getSnapshot() {
return state;
},
setState(newState: T) {
state = newState;
notifyStateChanged();
},
};
}

export function createUseStore<T>(initialState: T) {
const store = createStore({ initialState });
return () => [useSyncExternalStore(store.subscribe, store.getSnapshot), store.setState] as const;
}



export const useCountStore = createUseStore(0);

function Counter() {
const [count, setCount] = useCountStore();

const increment = () => {
setCount(count + 1);
};

/* --snip-- */
}


Если хук useCountStore будет использоваться в нескольких компонентах, то его значение будет общим между всеми компонентами.

https://paripsky.github.io/blog/build-your-own-react-state-management/
👍172
Google Translate может сломать React приложение

Браузерное расширение Google Translate может сломать React приложение. Это связано с тем, что, когда расширение активируется, он ищет узлы TextNode для перевода. Эти узлы заменяются на FontElement с переведенными строками внутри. Например:


<p>There are 4 lights!</p>

// После перевода:

<p><font>Er zijn 4 lampen!</font></p>


Как видно из примера, меняется DOM структура элемента. Исходный TextNode размонтируется и заменяется новым FontElement с переведенным текстом внутри.

Какие могут возникнуть проблемы при активированном Google Translate:

🔴 Значения стейта useState на странице может не обновляться. Если расширение активировано, то оно размонтирует узел и заменяет DOM элемент. Однако размонтированный DOM узел остается в памяти приложения и изменения стейта происходят в нем.

🔴 Краш приложения:

NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.

Это может произойти из-за условного рендера строки, например: {lightsOn && 'There are 4 lights!’}

🔴 Неправильное значение в event.target. Когда Google Translate активируется, то event.target у текста становится элемент font, а не ожидаемый элемент, в котором был изначальный текст.

Кроме Google Translate приложение могут сломать и другие расширения, которые манипулируют DOM. На данный момент нет реального решения этой проблемы. Есть лишь некоторые способы обхода, но они создают дополнительные проблемы. Например, один из более-менее простых способов обхода проблемы заключается в оборачивании условного рендера строки в span.

https://martijnhols.nl/gists/everything-about-google-translate-crashing-react
Please open Telegram to view this post
VIEW IN TELEGRAM
👍281
Типобезопасный REST API вместе с Zod

При работе с REST API нельзя быть уверенным в том, что вернется в ответе запроса. Даже если используешь TypeScript и пишешь типы для ответа, то запрос все равно может вернуть другой ответ, в следствие чего может произойти runtime ошибка.

Если хотите быть уверенными в том, что возвращает REST API, то используйте валидацию ответа через Zod схемы. В этом случае, если ответ запроса не соответствует ожиданию, выбросится ошибка. Пример:


const FetchedDataSchema = z.object({
id: z.number(),
name: z.string(),
});

type FetchedData = z.infer<typeof FetchedDataSchema>;

function DataFetchingComponent() {
const fetchData = async (): Promise<FetchedData> => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return FetchedDataSchema.parse(data);
};

/* --snip-- */
}


https://noahflk.com/blog/typesafe-rest-api
👍202
Обработка ошибок в React Server Components

В серверных компонентах React могут возникать ошибки. Для их обработки можно использовать ErrorBoundary из react-error-boundary, который поддерживает восстановление ошибки и повторную попытку рендеринга.

С помощью RSC и react-error-boundary можно реализовать повторный рендеринг. Пример компонента ErrorFallback, который выполняет повторный рендеринг по клику на кнопку:


'use client'

import { startTransition, useState } from 'react'
import { useRouter } from 'next/navigation'

export default function ErrorFallback({
error,
resetErrorBoundary,
}: {
error: Error
resetErrorBoundary: () => void
}) {
const router = useRouter()

// Стейт кнопки сброса
const [isResetting, setIsResetting] = useState(false)

function retry() {
setIsResetting(true)

startTransition(() => {
router.refresh()
resetErrorBoundary()
setIsResetting(false)
})
}

return (
<button onClick={() => retry()}>
{isResetting ? <Spinner /> : null}
Retry
</button>
)
}


Как видно из примера, используется router.refresh() для повторного рендера компонента, в котором произошла ошибка. На самом деле ре-рендерится вся страница, но пользователю кажется, что ре-рендерится только компонент с ошибкой. Т.к. router.refresh() долгая операция, которая не возвращает Promise, то для отслеживания выполнения она вызывается внутри startTransition.

https://edspencer.net/2024/7/16/errors-and-retry-with-react-server-components
👎7👍41🔥1
Серверные функции в Tanstack Router

В Tanstack Router появилась возможность использовать серверные функции.


// getServerTime.ts
import { createServerFn } from '@tanstack/start'

export const getServerTime = createServerFn('GET', async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return new Date().toISOString()
})

// Time.tsx
import { useServerFn } from '@tanstack/start'
import { useQuery } from '@tanstack/react-query'
import { getServerTime } from './getServerTime'

export function Time() {
const getTime = useServerFn(getServerTime)

const timeQuery = useQuery({
queryKey: 'time',
queryFn: () => getTime(),
})
}


Серверные функции можно вызывать на сервере или на клиенте. Под капотом используется библиотека Vinxi, предназначенная для создания фулстек приложений.

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

https://tanstack.com/router/latest/docs/framework/react/start/server-functions
👍9
@svg-use для импорта SVG

Библиотека @svg-use это набор инструментов и плагинов сборки для использования SVG с помощью механизма <use href>. Автор библиотеки хочет решить проблемы подхода SVG-in-JS с помощью <use href>, но при этом оставляя возможность кастомизации иконок.

Подход SVG-in-JS популярен и удобен для вставки SVG иконок на сайт. В этом подходе с помощью библиотеки svgr SVG преобразуется в React компонент. У этого подхода есть свои плюсы: возможность кастомизации иконок и удобство доставки SVG на сайт внутри JS бандла. Однако, у этого подхода есть и свои минусы:
🔴 Каждый SVG компонент сначала парсится в JS, потом в HTML, когда вставляется в дерево, это влияет на производительность.
🔴 Раздувание HTML дерева.
🔴 Увеличение размера загружаемого JS бандла.

Библиотека @svg-use встраивается в сборщик и генерирует SVG файлы как asset. Внутри сгенерированного SVG файла будут прописаны CSS переменные. Как выглядит использование библиотеки @svg-use:


import { Component as Arrow } from './icon.svg?svgUse';

return <Arrow color="red" />;


В результате на месте использования компонента Arrow рендерится SVG:


<svg viewBox="0 0 24 24">
<use
href="https://my-site.com/assets/icon-someHash.svg#use-href-target"
style="--svg-use-href-color: red"
></use>
</svg>


В asset проекта генерируется файл icon-someHash.svg:


<svg
xmlns="https://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="var(--svg-use-href-color, #111)"
id="use-href-target"
>
<line x1="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>


https://fotis.xyz/posts/introducing-svg-use/
Please open Telegram to view this post
VIEW IN TELEGRAM
👍241
Namespace для компонентов

При организации компонентов в React используется подход, когда компонент разбивается на несколько частей и доступ к каждой части происходит через точку, например Card и Card.Header, Card.Body и Card.Footer.

Есть несколько способов реализовать такой подход, например через Function Assignment:


function Card() {}

Card.Header = function Header() {}
Card.Body = function Body() {}
Card.Footer = function Footer() {}

export default Card;


Либо через Object.assign:


export default Object.assign(Card, {
Header,
Body,
Footer,
});


У обоих этих подходов есть минусы:
🔴 Проблема с сериализацией, из-за чего этот подход не будет работать в React Server Components.
🔴 Проблема с tree shaking: все части компонента будут затянуты в бандл, даже если будет использоваться только одна из них.

Для решения этих проблем можно использовать современный способ через ES модули. Пример:


// card.tsx
export function CardRoot() {}
export function CardHeader() {}
export function CardBody() {}
export function CardFooter() {}

// namespaces.ts
export {
CardBody as Body,
CardRoot as Root,
CardFooter as Footer,
CardHeader as Header,
} from "./card";

// index.tsx
export * as Card from "./namespace";

// App.tsx
import { Card } from './src/ui/card';

// Card.Root, Card.Body и т.д.


В этом способе нет runtime вычислений и код работает в React Server Components.

Дополнено: однако из-за особенностей работы бандлера, tree shaking все равно работать не будет.

https://ivicabatinic.from.hr/posts/multipart-namespace-components-addressing-rsc-and-dot-notation-issues
Please open Telegram to view this post
VIEW IN TELEGRAM
👍404
React и FormData

Для сбора данных формы можно использовать FormData – объект, который предоставляет геттеры и сеттеры для значений формы. Его можно использовать во время сабмита формы:


function onSubmit(event: React.FormEvent) {
event.preventDefault()
const formData = new FormData(event.target)

const formValues = {
name: formData.get('name')
email: formData.get('email)
}
}


Чтобы избавиться от геттеров, можно использовать Object.fromEntries для представления данных в виде объекта:


const formValues = Object.fromEntries(formData)
console.log(formValues) { name: 'my name', email: '[email protected]' }
typeof formValues // { [k: string]: FormDataEntryValue }


Если вы работаете с TypeScript, то при использовании FormData могут возникнуть проблемы с типизацией. Эти проблемы можно решить с помощью схемы Zod и парсинга данных:


const formValues = Object.fromEntries(formData) // TYPE: { [k: string]: FormDataEntryValue }

const results = myFormSchema.safeParse(formValues)
if (results.success) {
results.data // TYPE: { email: string, quantity: number }
console.log(results.data.email) // [email protected]
console.log(results.data.quantity) // 5
} else {
// Обработка ошибки
}


React 19 способствует использованию FormData и позволяет получать данные формы при сабмите через проп action:


function MyForm() {
function formAction(formData: FormData) {
// Получаем экземпляр formData вместо event
}

return <form action={formAction}>...</form>
}


https://reacttraining.com/blog/react-and-form-data
👍15👎1
Замена кода на React на CSS :has псевдокласс

Через CSS можно стилизовать дочерние элементы на основании родительского элемента:


.content .card {
background: #f0f0f0;
}


С помощью псевдокласса :has можно сделать наоборот, снизу вверх: стилизовать родительский элемент на основании дочернего элемента. Например, карточки, в которых есть изображение, имеют другой цвет границы:


.card:has(img) {
border-top: 10px solid #fee6ec;
}


Псевдокласс :has поддерживается всеми основными браузерами, глобально 92% пользователей поддерживают его.

С помощью псевдокласса :has можно сделать многие фичи без написания кода на React. В своем блоге Надя Макаревич рассказала несколько примеров использования :has, которые заменяют код на React. Например, стилизация карточки и соседних карточек при фокусе на кнопки действий внутри карточки. Пример CSS стилей:


// Все карточки после карточки с фокусом на кнопку «открытия»
.card:has([data-action='open']:focus-visible) ~ .card,

// Все карточки перед карточкой с фокусом на кнопку «открытия»
.card:has(~ .card [data-action='open']:focus-visible) {
filter: greyscale();
background-color: #f6f7f6;
}

// Стиль карточки с фокусом на кнопку «открытия»
.card:has([data-action='open']:focus-visible) {
transform: scale(1.02);
border-top: 10px solid #c3dccf;
}


https://www.developerway.com/posts/replacing-react-with-css
👍17
TanStack Form

Стейт менеджер формы от TanStack. Особенности:

🔴 Headless архитектура
🔴 Гранулированная реактивность
🔴 Поддержка сложных валидаций и обработок ошибок на уровне формы, поля
🔴 Поддержка через адаптеры для валидаций Zod, Yup, Valibot
🔴 Небольшой размер (5.2кБ gzip)
🔴 Поддержка вложенных объектов и массивов

Пример:


import { useForm } from '@tanstack/react-form';

export default function App() {
const form = useForm({
defaultValues: {
firstName: '',
},
onSubmit: async ({ value }) => {
// Do something with form data
console.log(value);
},
});

return (
<div>
<h1>Simple Form Example</h1>
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
>
<div>
<form.Field
name="firstName"
validators={{
onChangeAsyncDebounceMs: 500,
onChangeAsync: async ({ value }) => {
await new Promise((resolve) => setTimeout(resolve, 1000));

return (
value.includes('error') && 'No "error" allowed in first name'
);
},
}}
children={(field) => {
return (
<>
<label htmlFor={field.name}>First Name:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
</>
);
}}
/>
</div>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => (
<button type="submit" disabled={!canSubmit}>
{isSubmitting ? '...' : 'Submit'}
</button>
)}
/>
</form>
</div>
);
}


https://tanstack.com/form/latest/docs/overview
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥6👍2