Розбираємо реліз React 19: паралельний рендеринг, серверні компоненти та нові хуки

Привіт! Я Микола, розробник з більш ніж п’ятирічним досвідом у продуктовому ІТ. Вже рік працюю як Front-End Developer на технічному SEO-напрямку в Universe Group — українській ІТ-компанії, що запускає глобальні продукти. В цій статті я розповім про новинки в 19 версії React.

Якщо ви думали, що React не може стати ще кращим, то React 19 готовий кинути вам виклик! Адже він містить важливі оновлення, які зроблять процес розробки ще досконалішим та комфортнішим. Серед ключових нововведень:

— Паралельний Рендеринг для покращення користувацького досвіду.

 Серверні Компоненти для оптимізації серверних і клієнтських ресурсів.

— Нові Хуки, що спрощують і вдосконалюють процес розробки.

— Transitions API для просунутішого управління станами.

Заявлені зміни мають на меті не лише спростити написання коду, а й зробити його більш зрозумілим і ефективним. Давайте детальніше розглянемо, що чекає на нас в цьому релізі.

Паралельний Рендеринг

У порівнянні з попередньою версією React 18, яка використовувала синхронний рендеринг, паралельний рендеринг у React 19 пропонує концепцію, що дозволяє розбивати оновлення інтерфейсу на менші асинхронні завдання. Це нововведення значно покращує інтерактивність користувача і забезпечує більш плавний користувацький досвід завдяки можливості React визначити пріоритет та планувати оновлення, а також уникати блокування інтерфейсу.

Уявіть ситуацію, коли ви відкриваєте кілька вкладок у браузері: кожна вкладка завантажується незалежно від інших, і жодна з них не блокує процес завантаження чи взаємодії з іншими вкладками. Подібним чином паралельний рендеринг у React 19 дозволяє різним частинам інтерфейсу завантажуватися та оновлюватися незалежно одна від одної, що забезпечує швидший і комфортніший користувацький досвід.

Ключові переваги

Покращена інтерактивність. Завдяки пріоритетній обробці завдань, інтерфейс швидше реагує на дії користувача, навіть при виконанні складних і обʼємних обчислень. Зменшення часу очікування на оновлення забезпечує швидшу відповідь застосунку на дії користувача, що суттєво покращує його загальне враження від використання застосунку.

Плавніші анімації. Анімації та переходи стають більш плавними та безперервними. React може рендерити кілька анімацій одночасно, що усуває затримки та пригальмовування. Це забезпечує зручніший для користувача досвід.

Оптимізація ресурсів. Паралельний рендеринг дозволяє ефективніше використовувати системні ресурси, що особливо важливо для комплексних застосунків з великою кількістю стейтів. Завдяки цьому підходу проблеми із «зависанням» інтерфейсу під час виконання важких обчислень або запитів до сервера можуть бути значно мінімізовані.

Серверні Компоненти

React Server Components (RSCs) — революційний функціонал, представлений у 19 версії React. Серверні компоненти мають потенціал змінити звичний підхід до створення вебзастосунків із використанням React. Зазвичай React-застосунки покладалися на рендеринг компонентів на стороні клієнта, але нова версія пропонує React Server Components, які можуть безпосередньо рендерити інтерфейс користувача на сервері.

Концепція рендерингу компонентів на стороні сервера попередньо вже була реалізована в таких фреймворках, як Next.js та Astro. Тепер завдяки React Server Components можна значно покращити час завантаження сторінок. Це також покращує SEO, оскільки пошукові системи можуть легше індексувати та розуміти зміст сторінок, адже браузер отримає вже готовий контент.

Нові хуки

useFormStatus

Цей хук дає нам інформацію про статус останньої відправленої форми. При надсиланні форми користувачем він дає змогу отримати такі дані: статус надсилання (успішний, неуспішний, в процесі), надіслані дані, можливість керувати інтерфейсом користувача залежно від результату дії.

const { pending, data, method, action } = useFormStatus();

useFormStatus повертає кілька значень, які дозволяють відстежувати статус форми:

  1. pending — вказує, чи перебуває форма в процесі надсилання. Якщо форма в цей момент надсилається, значення буде true, а після завершення надсилання — false.
  2. data — дані, що були надіслані у формі. Це об’єкт, який містить значення всіх полів форми, надісланих під час останнього надсилання.
  3. method — вказує HTTP-метод, який використовувався для надсилання форми, наприклад, «POST» або «GET».
  4. action — URL-адреса, на яку було надіслано форму, що дозволяє відстежувати, куди саме було здійснено запит.
import { useFormStatus } from 'react';
function Form() {
 const { pending, data, method, action } = useFormStatus();
  return ( 
   <div> 
    <form action="/submit" method="POST"> 
     <input type="text" name="username" placeholder="Enter username" required /> 
     <button type="submit" disabled={pending}>
      {pending ? 'Submitting...' : 'Submit'}
     </button>
   </form>
   {pending && <p>Форма все ще відправляється...</p>}
   {!pending && data && (
    <div>
     <h3>Деталі Відправки:</h3>
     <p><strong>Метод:</strong> {method}</p>
     <p><strong>Екшин:</strong> {action}</p>
     <p><strong>Відправлені дані:</strong> {JSON.stringify(data)}</p>
   </div> 
 )}

Якщо ви працюєте з формами та хочете покращити їхній менеджмент, а також зберігати чистий і зрозумілий код, цей хук — те, що вам потрібно.

useOptimistic

useOptimistic — хук, який допомагає керувати станом компонентів, припускаючи, що операції будуть успішними. Він дає змогу миттєво оновити інтерфейс, не чекаючи відповіді від сервера. Якщо сервер поверне нам помилку — ми повернемось до початкового інтерфейсу. Функція-хук приймає 2 параметри: 1 — початковий стан компонента (initial state) 2 — функцію, яка приймає поточний стан та оптимістичне значення і повертає оновлений стан.

const [optimisticState, addOptimistic] = useOptimistic(
   state,
   // updateFn
   (currentState, optimisticValue) => {
     // змержіть стани та поверність оновлений стан із оптимістік значенням
   }
 );

У даному прикладі ми використовуємо хук useOptimistic для динамічного оновлення інтерфейсу під час асинхронної операції збереження завдання. Коли користувач додає нову задачу через форму, вона одразу ж з’являється в списку з позначкою «Saving...», поки запит на сервер не завершиться.

import { useOptimistic, useState, useRef } from "react";

async function saveTask(task) {
 await new Promise((res) => setTimeout(res, 1000));
 return task;
}

function TaskList({ tasks, addTask }) {
 const formRef = useRef();
 async function handleSubmit(formData) {
   addOptimisticTask(formData.get("task"));
   formRef.current.reset();
   await addTask(formData);
 }
 const [optimisticTasks, addOptimisticTask] = useOptimistic(
   tasks,
   (state, newTask) => [
     ...state,
     {
       description: newTask,
       pending: true
     }
   ]
 );

 return (
   <>
     {optimisticTasks.map((task, index) => (
       <div key={index}> 
        {task.description}
        {task.pending && <small> (Збереження...)</small>}
       </div>
     ))}
     
     <form action={handleSubmit} ref={formRef}>
      <input type="text" name="task" placeholder="Додайте нову задачу..." />
      <button type="submit">Додати</button>
     </form>
   </>
 );
}

export default function TaskManager() {
 const [tasks, setTasks] = useState([
   { description: "Завершити проект", pending: false, id: 1 }
 ]);
 async function addTask(formData) {
   const savedTask = await saveTask(formData.get("task"));
   setTasks((tasks) => [...tasks, { description: savedTask, pending: false }]);
 }
 return ;
}

Ключова функція оновлення із useOptimistic полягає в тому, що нове завдання відображається одразу як додане для користувача, незалежно від того, чи було його успішно збережено на сервері. Це значно покращує взаємодію користувача з інтерфейсом. Він може миттєво бачити результат дії, не чекаючи завершення операції. Після того, як завдання успішно збережено, позначка «Saving...» зникає.

Use

Новий універсальний хук для зручнішого управління асинхронними даними в React. Він приймає лише один параметр — джерело даних, з яких ми маємо на меті зчитати дані. Наприклад, це може бути контекст чи promise.

const value = use(resource);

Якщо він приймає контекст, то працює ідентично як хук useContext, проте use буде більш гнучким. Адже на відміну від useContext, його можна викликати не лише на верхньому рівні, але й всередині блоків з умовами, циклів. Нижче наведу простий приклад використання хука use для отримання значення контексту:

import React, { createContext, useState, useEffect, use } from 'react';

// Створіть контекст із значенням за замовчуванням
const ThemeContext = createContext('light');

// Компонент-Провайдер
const ThemeProvider = ({ children }) => {
 const [currentTheme, setCurrentTheme] = useState('light');

 useEffect(() => {
   // Симулюйте виклик даних
   const fetchTheme = async () => {
     await new Promise(resolve => setTimeout(resolve, 500));
     setCurrentTheme('dark');
   };
   fetchTheme();
 }, []);

 return (
   <ThemeContext.Provider value={currentTheme}>
     {children}
   </ThemeContext.Provider>
 );
};

const ThemeDisplay = () => {
 const theme = use(ThemeContext);

 return <div>Current Theme: {theme}</div>;
};

const App = () => (
 <ThemeProvider>
   <ThemeDisplay />
 </ThemeProvider>
);

export default App;

У випадку, коли значенням є promise, ми можемо використати хук use у парі з компонентом Suspense, щоб зчитати значення promise:

import React, { Suspense, useState, useEffect, use } from 'react';

// Імітуємо отримання даних з сервера
const fetchMessage = async () => {
 // Імітуємо затримку і отримуємо дані з API
 await new Promise((resolve) => setTimeout(resolve, 2000));
 return "Hello from the server!";
};

// Компонент клієнта, який використовує отримані дані
const Message = ({ messagePromise }) => {
 // Використовуємо хук use для отримання вмісту повідомлення з Promise
 const messageContent = use(messagePromise);
 return <p>Here is the message: {messageContent}</p>;
};

// Основний компонент додатку
const App = () => {
 // Отримуємо повідомлення з сервера; повертає Promise
 const [messagePromise, setMessagePromise] = useState(null);

 useEffect(() => {
   // Ініціалізуємо Promise для повідомлення
   const initMessage = async () => {
     const promise = fetchMessage();
     setMessagePromise(promise);
   };

   initMessage();
 }, []);

 if (!messagePromise) {
   return <p>Loading...</p>; // Фолбек варіант до готовності Promise
 }

 return (
   <Suspense fallback={<p>Waiting for message...</p>}>
     <Message messagePromise={messagePromise} />
   </Suspense>
 );
};

export default App;

В такому випадку ми передаємо promise в компонент Message за допомогою props, а далі — напряму в хук use. Оскільки компонент Message обгорнутий у Suspense, спочатку буде відображатися fallback, аж доки promise не буде вирішено. Коли promise буде виконано, значення буде прочитане за допомогою API use, і компонент Message замінить fallback у Suspense.

Transition API

Transition API в React дозволяє керувати оновленнями станів, які не є терміновими, надаючи розробникам можливість покращити досвід користувача. Основна ідея полягає в тому, щоб обгортати зміни стану в функцію startTransition. Це сигналізує React, що дані зміни можуть бути відкладені, і можна надати пріоритет більш важливим операціям, як-от анімації чи інтерактивним частинам UI.

import { useState, startTransition } from 'react';

function MyProfileComponent() {
  const [isPending, setIsPending] = useState(false);
  const [profileData, setProfileData] = useState(null);

  const loadProfile = () => {
    setIsPending(true);
    startTransition(() => {
      setTimeout(() => {
        setProfileData({
          name: 'Микола Третьохвильовий',
          age: 25,
          occupation: 'Розробник'
        });
        setIsPending(false);
      }, 2000);
    });
  };

  return (
   <div>
    <button onClick={loadProfile}>Load Profile</button>
     {isPending ? (
      <p>Завантаження профілю...</p>
      ) : ( profileData && (
       <div>
        <p>Ім'я: {profileData.name}</p>
        <p>Вік: {profileData.age}</p>
        <p>Професія: {profileData.occupation}</p>
      </div> 
      )
     )}
  </div>
 );
}
export default MyProfileComponent;

В цьому випадку ми повісили на кнопку подію, яка відповідає за завантаження даних користувача. Обгортаємо функцію-оновлення стану setProfileData в startTransition для того, щоб Transition API мав доступ до нашої дії та оновлюємо наш стан із затримкою у 2 секунди. Основні переваги Transition API:

  1. Маркування нетермінових оновлень. Дозволяє позначати зміни, які не потребують негайного відображення. Наприклад, великі перетворення даних можуть бути відкладені, щоб не «заморожувати» чи сповільнювати роботу інтерфейсу.
  2. Плавніші переходи. React може забезпечити комфортну і безперервну взаємодію для користувача, оскільки критичні оновлення обробляються з вищим пріоритетом, ніж другорядні зміни.
  3. Простота використання. Даний підхід додає мінімум додаткового коду, використовуючи всього одну функцію — startTransition, що робить API зручним і простим для інтеграції.

Використання Transition API дозволить розробникам покращити продуктивність та плавність взаємодії із застосунком, відокремлюючи термінові та нетермінові оновлення стану. Це своєю чергою забезпечить більш комфортний досвід для користувачів, зменшуючи затримки та підвищуючи ефективність роботи з асинхронними операціями, особливо в складних або ресурсомістких застосунках.

SEO

З виходом React 19 управління SEO-елементами стало значно зручнішим і зрозумілішим. Тепер розробники можуть безпосередньо додавати теги заголовків та метатеги у свої компоненти React:

const HomePage = () => {
 return (
   <>
     <title>Що нового в реакт 19</title>
     <meta name="description" content="Останні новини та оновлення в React 19" />
     {/* Контент сторінки */}
   </>
 );
};

Завдяки оновленню, розробники зможуть легше покращувати доступність своїх застосунків — без використання сторонніх бібліотек чи кастомної логіки.

Висновок

Застосунки, які перейдуть на 19-ту версію React або ж будуть написані на ній із самого початку, зроблять користувачів ще щасливішими, а розробників — ще уважнішими. Не знаю, чи вже використовують у Спрингфілді останню версію React, але я б точно спробував її! А ви вже спробували нову версію? Буду радий, якщо поділитесь своїм досвідом зі мною в LinkedIn або Instagram. Бажаю всім реактивного тижня!

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось6
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Ще не розбирався з 19 реактом, але

useFormStatus

виглядає дивно. Повертає стан останної форми? якось ненадійно

я якось очікував що треба передати onSubmit в форму

Update: react.dev/...​t-dom/hooks/useFormStatus

Схоже useFormStatus треба викликати у компонентах які всередині тегу form щоб корректно працювало (у реакт 18.3.1)

To get status information, the Submit component must be rendered within a form. The Hook returns information like the pending property which tells you if the form is actively submitting.
In the above example, Submit uses this information to disable presses while the form is submitting.

і form action={...} може бути методом з ajax запитом

Дякую за ваш коментар і що долучилися до розгляду хуків! Ви праві, useFormStatus в React 19 нативніше викликати у компонентах усередині елементу form.

Приклад із dev.to/...​rmstatus-in-react-19-3khh

import { useFormStatus } from "react-dom";

const Register = () => {
  const { pending } = useFormStatus();

  return (
    <button className="border border-black">
      {pending ? "Loading" : "Register"}
    </button>
  );
};

export default function Home() {
  const formAction = async (formData: FormData) => {
    "use server";
    await new Promise((resolve) => {
      setTimeout(resolve, 1000);
    }); //intentionally delay time
    console.log(formData.get("firstName"));
  };

  return (
    <form action={formAction}>
      <div>
        <label>Name:</label>
        <input type="text" name="firstName" />
      </div>
      <Register />
    </form>
  );
}

Дякую за статтю, але мені не вистачило посилань на офиційну документацію.
До прикладу, намагався нагуглити чи пошукати на сайті реакта інфу про цей новий хук `useServerComponent` і нічого не знайшов

Vladyslav, дуже дякую вам за ваш коментар і слушне зауваження. В наступних своїх статтях буду використовувати вашу пораду та залишати посилання. Частина коду із useServerComponent — це кастомний хук, який я взяв зі свого проекту, перепрошую, якщо він міг збити вас з пантелику. Ще раз дякую вам!

Вроде несложно, можно самому написать

 

import { useEffect, useState } from 'react';

function useServerComponent(fetchFunction) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;

    const fetchData = async () => {
      try {
        setLoading(true);
        const result = await fetchFunction(signal);
        setData(result);
      } catch (err) {
        if (err.name === 'AbortError') {
          console.log('Fetch aborted');
        } else {
          setError(err);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    // Cleanup function to abort the fetch request
    return () => {
      controller.abort();
    };
  }, [fetchFunction]);

  return { data, error, loading };
}

export default useServerComponent;


//Юзаем в своем компоненте

import React from 'react';
import useServerComponent from './useServerComponent';

const MyComponent = () => {
  const fetchFunction = async (signal) => {
    const response = await fetch('https://api.example.com/data', { signal });
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.json();
  };

  const { data, error, loading } = useServerComponent(fetchFunction);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <h1>Data:</h1>
      <pre>{JSON.stringify(data, null, 2)}

);
};
export default MyComponent;

Были заюзаны AbortController: Этот объект используется для отмены асинхронных запросов. В случае, если компонент размонтируется до завершения запроса, мы можем вызвать controller.abort(), чтобы предотвратить выполнение ненужного кода.
Сигнал передается в функцию фетчинга и используется для отмены запроса при необходимости.
Почему такое? А вот почему, т.к. этот подход дает возможность отмены запросов помогает избежать утечек памяти и ненужных операций. Также этот кастомный хук позволяет легко работать с асинхронными операциями и управлять состоянием загрузки и ошибок.
Шё скажите хлопцы?

Дякую, що поділилися своїм напрацюванням кастомного хука. Вцілому використання `AbortController` для уникнення «витоків» запитів, які лишаються активними після того як компонент пропадає з сторінки — це, на мій погляд, гарна практика.

Для покращення оптимізації я би ще додатково огорнув би fetchFunction у хук useCallback.

 
const fetchFunction = useCallback(async (signal) => {
  const response = await fetch('https://api.example.com/data', { signal });
  if (!response.ok) {
    throw new Error('Network response was not ok');
  }
  return response.json();
}, []);

Благодарю за критический комментарий, он безусловно будет для меня полезен, т.к. я только продолжаю изучать реакт. Рассматриваете ли вы студентов 122-ой как соавторов статьи? Темы могут быть разные, например, компилятор реакт, или использование socke.io для эмуляции аякс соединения. Необходимо для курсача.

Дякую за ваш коментар і круто, що у вас є така жага до знань. Ні, на жаль, поки не розглядаю співавторства.

Да, еще лучше вынести работу с апи в отдельный компонент, и при этом учитывать мутабельность, например использовать www.npmjs.com/...​ckage/immutability-helper

Так, я би використовував модульний підхід або headless React Components, що, звісно, покращить структурованість та зрозумілість коду

const [isPending, setIsPending] = useState(false);

Доречі, цього можна уникнути, використовуючи useTransition.

Олексію, дякую за те, що помітили це!

У мене таке відчуття, що з такою радістю про серверні компоненти + стрімінг + рендер на сервері пишуть лиш ті фронтендери, які не стикались з безжальною реальністю. Гадаю, хто писав сайт на новому Next, вже має відбиті пальці від SEO-шників за всі гріхи в розмітці та прокляту душу від дизайнера й відвідувачів сайту за «залипаючий» UX.

Ігоре, дякую за ваш коментар. Не знаю чи пройшов я той етап безжальної реальності про яку ви говорите, проте, можливо, новий реліз вирішить деякі з таких гріхів. Проте я не виключаю того, що все стане безпроблемно, коли ви будете використовувати нову версію React.

Можна більше деталей?

Я колись працював у здоровенному маркетплейсі, фронтенд якого був написаний на Нексті і компанія дуже покладалась на пошукову оптимізацію. Але я був на бекенді і не в курсі за складності, які там могли б бути.

Але от нещодавно вирішив переписати свій сайт на щось інтерактивне з використанням ларавеля та інерції з реактом (некст відкинув, бо сиджу на shared хостингу). Почитав трохи про всі ті проблеми з пошуковою оптимізацію, що були в SPA, і ніяк не зрозумію, чи все ще так погано. Гуглові кравлери уже уміють виконувати js на своїй стороні, в ларавелі є повноцінний роутинг, є і сервер рендеринг або плагіни для інтеграції з prerender.io. Хіба це все, що уже є, не покращує SEO?

Можна, але це треба дуже багато тексту. Якщо коротко, то є один приклад.
У документації Next стосовно Suspense та стрімінга є примітка, що це не впливає на SEO. Але по факту булшіт. Бо достатньо вимкнути JS у браузері, і побачити замість бажаного контенту div з «Loading...», який є фолбеком у верстці. Чому? Бо Suspense працює на JavaScript, такий механізм. Він справді рендерить шматок html на сервері, але вставляє його не у відповідне місце на сторінці, а в кінець у тег , який не відображається. А вже потім виконується js, який розставляє все по структурі.
Виходить верстка наступна:
header
sidebar
footer
template -> контент сторінки чи секції

Гугл хоч і обіцяє, що виконає javascript, але не гарантує, та й зазначає, що вони мають так званий budget.

хлопцы а как в одном сторе засетить данные с другого стора? Такое в реакте возможно, как он работает с ромбовидными зависимостями?

У React diamond dependencies можна вирішити за допомогою обʼєднання стейтів у один контекст або useReducer. Але у вирішенні таких комплексних задачах я би віддав перевагу Redux.

А как через подписки это делается? Применяется ли это на продакшене? Хлопцы, можно пример?

Хлопцы, а можно вместо редакса использовать use context use reduser?

Так, use context у поєднанні з useReducer можна використати для керування станами в React-застосунках.

Дуже цікава та інформативна стаття !

Привіт, Микола хороша статья!
Кому цікаво, зняв коротке відео на цю тему —
youtu.be/Bc3j0XIzDlg

хлопцы, а можете сделать по сигналам в реакте и абортконтролеру видосы?

Я поки не знімаю навчальні відео, але бачу, що Yuriy Petrichenko веде свій ютуб канал. Можете йому написати в особисті і обговорити з ним цю ідею.

Підписатись на коментарі