ar
Feedback
Заметки про React

Заметки про React

الذهاب إلى القناة على Telegram

Короткие заметки про React.js, TypeScript и все что с ним связано Контакт: @rimlin

إظهار المزيد
3 950
المشتركون
لا توجد بيانات24 ساعات
-77 أيام
-4930 أيام
أرشيف المشاركات
Great article!';\n\nconst UserComments = () => {\n return (\n
\n
  • \n
  • \n );\n}; \n\n\n🔴Content Security Policy (CSP) заголовок. Использование CSP заголовка защитит от большинства веб-атак, включая XSS. CSP заголовок позволяет указать какие ресурсы могут загружаться и исполняться браузером. Позволяет работать в Report-Only режиме, позволяя только собирать обнаруженные проблемы, не применяя правила. Пример:\n\n\nheader Content-Security-Policy \"script-src 'self' 'unsafe-inline' https://maps.googleapis.com https://www.googletagmanager.com;\" \n\n\n🔴Cross-Site Request Forgery (CSRF). При CSRF атаке злоумышленник обманом заставляет пользователя отправить запрос, который он не собирался делать. Обычно это делается путем внедрения вредоносных запросов на сайт или e-mail, с которым взаимодействует пользователь. Для противодействия CSRF атаке используются токены, которые генерируются на странице и передаются вместе с запросом. Пример:\n\n\nfunction CSRFProtectedForm() {\n const [csrfToken, setCsrfToken] = useState('');\n\n {/* Запрос CSRF токена */}\n\n return (\n
    \n \n {/* --snip-- */}\n \n
    \n );\n}\n\n\n🔴Переменные окружения. Используйте env переменные и не храните их значения в коде в репозитории. \n\nhttps://www.trevorlasn.com/blog/frontend-security-checklist","datePublished":"2024-08-13T10:30:37Z","dateModified":"2024-08-13T10:30:37Z","author":{"@type":"Organization","name":"Заметки про React","url":"https://telemetr.io/ar/channels/1196185438-ru_react_notes","image":"https://img.tlmtr.io/c/1iX4tw/5442745934208415613?ty=x"},"publisher":{"@type":"Organization","name":"Заметки про React","url":"https://telemetr.io/ar/channels/1196185438-ru_react_notes","image":"https://img.tlmtr.io/c/1iX4tw/5442745934208415613?ty=x"},"commentCount":0,"interactionStatistic":[{"@type":"InteractionCounter","interactionType":"https://schema.org/ViewAction","userInteractionCount":2378},{"@type":"InteractionCounter","interactionType":"https://schema.org/LikeAction","userInteractionCount":25},{"@type":"InteractionCounter","interactionType":"https://schema.org/ShareAction","userInteractionCount":77}]}},{"@type":"ListItem","position":19,"item":{"@type":"SocialMediaPosting","@id":"https://telemetr.io/ar/channels/1196185438-ru_react_notes/posts/474","url":"https://telemetr.io/ar/channels/1196185438-ru_react_notes/posts/474","mainEntityOfPage":"https://telemetr.io/ar/channels/1196185438-ru_react_notes/posts/474","headline":"Рефакторинг запутанного компонента В своем блоге Алекс Кондов рассказал, как бы он проводил рефакторинг запут…","articleBody":"Рефакторинг запутанного компонента\n\nВ своем блоге Алекс Кондов рассказал, как бы он проводил рефакторинг запутанного компонента формы. Кратко, о чем он пишет:\n\n- Написать тесты. Если еще тестов нет, то перед рефакторингом надо написать тесты для компонента. В тестах должна проверяться бизнес логика. Делается это для того, чтобы знать, что в процессе рефакторинга ничего не сломается.\n\n- Использовать линтер. Нужно добавить автоматические проверки, которые подсветят проблемный код. Например, неиспользуемые импорты, значения стейта и т.д. \n\n- Удалить «мертвый» код. Нужно удалить лишние комментарии, неиспользуемые переменные. В этом как раз поможет линтер.\n\n- Сократить количество стейтов. Если в компоненте много useState, то скорее всего он делает слишком много разных вещей. В этом случае его можно разделить на подкомпоненты и связать через пропсы. \n\n- Убрать большие условные рендеры. Если блоки похожи, то их можно объединить в один компонент. Если блоки различны, то лучше разделить их на два компонента и разделить логику между ними.\n\n\nreturn !numberIncorrect ? (\n // A lot of JSX...\n) : (\n // Even more JSX...\n)\n\n\n- Следить за ответственностью компонента, чтобы внутри него было как можно меньше действий. В дальнейшем это упростит понимание кода.\n\n- Вынести утилитарные функции за пределы компонент. Функция может находиться в теле компонента, только если ей требуется доступ к стейту или другому значению внутри компонента. \n\n- Используйте для валидации форм декларативные схемы, например Zod, Yup.\n\n- Не используйте inline стили. \n\nhttps://alexkondov.com/refactoring-a-messy-react-component/","datePublished":"2024-08-09T10:10:21Z","dateModified":"2024-08-09T10:10:21Z","author":{"@type":"Organization","name":"Заметки про React","url":"https://telemetr.io/ar/channels/1196185438-ru_react_notes","image":"https://img.tlmtr.io/c/1iX4tw/5442745934208415613?ty=x"},"publisher":{"@type":"Organization","name":"Заметки про React","url":"https://telemetr.io/ar/channels/1196185438-ru_react_notes","image":"https://img.tlmtr.io/c/1iX4tw/5442745934208415613?ty=x"},"commentCount":0,"interactionStatistic":[{"@type":"InteractionCounter","interactionType":"https://schema.org/ViewAction","userInteractionCount":2498},{"@type":"InteractionCounter","interactionType":"https://schema.org/LikeAction","userInteractionCount":21},{"@type":"InteractionCounter","interactionType":"https://schema.org/ShareAction","userInteractionCount":43}]}},{"@type":"ListItem","position":20,"item":{"@type":"SocialMediaPosting","@id":"https://telemetr.io/ar/channels/1196185438-ru_react_notes/posts/473","url":"https://telemetr.io/ar/channels/1196185438-ru_react_notes/posts/473","mainEntityOfPage":"https://telemetr.io/ar/channels/1196185438-ru_react_notes/posts/473","headline":"Гибкая предварительная загрузка данных в SPA При создании большого SPA одним из важных пунктов оптимизации ст…","articleBody":"Гибкая предварительная загрузка данных в SPA\n\nПри создании большого SPA одним из важных пунктов оптимизации становится предварительная загрузка данных. Например, предварительная загрузка пользовательских данных, конфига и т.д. поможет быстрее отобразить страницу. \n\nВ SPA приложениях React чаще всего загрузка данных происходит после монтирования приложения. Этот подход может стать неэффективным по мере масштабирования приложения. \n\nВ своей статье автор поделился скриптом для предварительной загрузки данных. Этот скрипт вставляется в тег и исполняется одним из первых. Базовая реализация:\n\n\n\n \n\n\n\n\nfunction MyApp() {\n const [userData, setUserData] = useState();\n \n async function loadUserData() {\n setUserData(await window.__userDataPromise);\n }\n \n useEffect(() => {\n loadUserData();\n }, []);\n}\n\n\nВ своем блоге автор поделился более низкоуровневой реализацией скрипта.\n\nhttps://mmazzarolo.com/blog/2024-07-29-data-preloading-script/","datePublished":"2024-08-06T10:10:09Z","dateModified":"2024-08-06T10:10:09Z","author":{"@type":"Organization","name":"Заметки про React","url":"https://telemetr.io/ar/channels/1196185438-ru_react_notes","image":"https://img.tlmtr.io/c/1iX4tw/5442745934208415613?ty=x"},"publisher":{"@type":"Organization","name":"Заметки про React","url":"https://telemetr.io/ar/channels/1196185438-ru_react_notes","image":"https://img.tlmtr.io/c/1iX4tw/5442745934208415613?ty=x"},"commentCount":2,"interactionStatistic":[{"@type":"InteractionCounter","interactionType":"https://schema.org/ViewAction","userInteractionCount":2649},{"@type":"InteractionCounter","interactionType":"https://schema.org/LikeAction","userInteractionCount":17},{"@type":"InteractionCounter","interactionType":"https://schema.org/ShareAction","userInteractionCount":36},{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":2}]}}]}
    Оптимизация загрузки 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/

    Негласные правила 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

    Релиз React DevTools 6.0 Вышел релиз React DevTools 6.0. Основные изменения были связаны с поддержкой серверных компонентов.
    Релиз 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

    Контроль компонентов через 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

    Композиция компонентов в 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

    Чистый код 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

    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

    Замена кода на 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

    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: 'name@someemail.com' }
    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) // name@someemail.com
      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

    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

    @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="http://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/

    Серверные функции в 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

    Обработка ошибок в 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

    Типобезопасный 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

    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

    Создание стейт менеджера за 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/

    Глубокое погружение в формы в современном 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

    Чеклист безопасности 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

    Рефакторинг запутанного компонента В своем блоге Алекс Кондов рассказал, как бы он проводил рефакторинг запутанного компонента формы. Кратко, о чем он пишет: - Написать тесты. Если еще тестов нет, то перед рефакторингом надо написать тесты для компонента. В тестах должна проверяться бизнес логика. Делается это для того, чтобы знать, что в процессе рефакторинга ничего не сломается. - Использовать линтер. Нужно добавить автоматические проверки, которые подсветят проблемный код. Например, неиспользуемые импорты, значения стейта и т.д. - Удалить «мертвый» код. Нужно удалить лишние комментарии, неиспользуемые переменные. В этом как раз поможет линтер. - Сократить количество стейтов. Если в компоненте много useState, то скорее всего он делает слишком много разных вещей. В этом случае его можно разделить на подкомпоненты и связать через пропсы. - Убрать большие условные рендеры. Если блоки похожи, то их можно объединить в один компонент. Если блоки различны, то лучше разделить их на два компонента и разделить логику между ними.
    
    return !numberIncorrect ? (
        // A lot of JSX...
    ) : (
        // Even more JSX...
    )
    
    - Следить за ответственностью компонента, чтобы внутри него было как можно меньше действий. В дальнейшем это упростит понимание кода. - Вынести утилитарные функции за пределы компонент. Функция может находиться в теле компонента, только если ей требуется доступ к стейту или другому значению внутри компонента. - Используйте для валидации форм декларативные схемы, например Zod, Yup. - Не используйте inline стили. https://alexkondov.com/refactoring-a-messy-react-component/

    Гибкая предварительная загрузка данных в 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/