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

Контакт: @rimlin
Download Telegram
Изучите 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
Чистый код React с TypeScript

В своей статье Робин Везер объясняет, как правильно типизировать компоненты React, использовать встроенные типы React, обрабатывать пропсы и дочерние элементы, управлять стейтом с помощью хуков, работать с рефами и обрабатывать события.

Несколько примеров, которые привел автор статьи:

Передача компонентов в пропсах. Можно указать требуемые пропсы у передаваемых компонентов, а определить компонент через генерик тип ComponentType:


import { ComponentType } from 'react'

type ProductTileProps = {
title: string
description?: string
}

type Props = {
render: ComponentType<ProductTileProps>
}

function ProductTile({ render }: Props) {
const props = {…}

return render(props)
}


Для определения рефов через пропсы используйте генерик тип RefObject:


import { RefObject } from 'react'

type Props = {
anchor: RefObject<HTMLElement>
}

function Popover({ anchor }: Props) {
// position component according to the anchor ref
}


MouseEvent и React.MouseEvent. MouseEvent - это нативное JavaScript событие, а React.MouseEvent - это синтетическое событие, которое используется в колбеках React. У них много общего, и их часто можно использовать как взаимозаменяемые, но версия React также учитывает несовместимость браузеров и включает некоторые специфичные для React методы, такие как persist().

https://weser.io/blog/clean-react-with-typescript
Please open Telegram to view this post
VIEW IN TELEGRAM
👎11👍91