React 19: najważniejsze nowości i korzyści dla programistów

React doczeka się dużej aktualizacji w wersji 19, ale zanim zaczniesz się martwić, ile nowych rzeczy trzeba będzie opanować, mam dobrą wiadomość. React 19 skupia się nie tyle na kodzie, który trzeba napisać, ile na zmniejszeniu jego ilości. W tym artykule omawiam najważniejsze funkcje i usprawnienia wprowadzone w React 19.

Nowości w React 19 - krótki przegląd

react mind
react mind

🤖 React Compiler

Większość nowości w React 19 zawdzięczamy kompilatorowi, więc co on właściwie robi?

React Compiler przekształca Twój kod Reacta w standardowy JavaScript. To on pilnuje teraz ponownych renderowań, co wyraźnie poprawia wydajność aplikacji. W praktyce oznacza to mniej zmartwień związanych z optymalizacją: React sam ustala, które komponenty i kiedy trzeba zoptymalizować oraz co powinno zostać ponownie wyrenderowane, co upraszcza pracę.

react compiler
react compiler

Idempotentność w React

Jednym z fundamentów, na których opiera się React Compiler, jest idempotentność komponentów. W tym kontekście oznacza ona, że komponent Reacta dla tych samych danych wejściowych powinien zwracać ten sam wynik, bez efektów ubocznych takich jak mutowanie propsów czy stanu. Ta zasada jest kluczowa dla optymalizacji wykonywanych przez kompilator, bo pozwala Reactowi automatycznie renderować ponownie dokładnie te fragmenty UI, których dotyczy zmiana stanu - dzięki temu aktualizacje są oszczędne, a wydajność rośnie.

Wpływ nowej wersji Reacta na idempotentność

Pojawienie się React Compilera w nadchodzącej wersji mocno zmienia znaczenie idempotentności w aplikacjach reactowych i to, jak jest egzekwowana. Trzymając się zasad JavaScriptu i Reacta, w tym idempotentności komponentów, programiści mogą zostawić optymalizację kompilatorowi. W efekcie ręczna memoizacja stosowana dziś, by zapobiegać nadmiarowym renderowaniom (useMemo, useCallback i memo), może stać się mniej potrzebna, a nawet zbędna, bo proces optymalizacji przejmuje kompilator.

memoizacja
memoizacja

Przykład użycia hooków wydajnościowych przed React 19:

tsx
import { useCallback, useMemo, useState } from "react";

function NameFilterApp() {
  const [names, setNames] = useState([
    "Tommy",
    "Arthur",
    "Polly",
    "John",
    "Grace",
  ]);
  const [filter, setFilter] = useState("");

  const handleFilterChange = useCallback((event) => {
    setFilter(event.target.value);
  }, []);

  const filteredNames = useMemo(
    () =>
      names.filter((name) => name.toLowerCase().includes(filter.toLowerCase())),
    [names, filter],
  );

  const totalLettersCount = useMemo(
    () => filteredNames.reduce((acc, name) => acc + name.length, 0),
    [filteredNames],
  );

  return (
    <>
      <input
        type="text"
        value={filter}
        onChange={handleFilterChange}
        placeholder="Filter names..."
      />
      <div>
        <strong>Filtered Names:</strong>
        {filteredNames.length > 0 ? filteredNames.join(", ") : "No results"}
      </div>
      <div>
        <strong>Total Number of Letters:</strong> {totalLettersCount}
      </div>
    </>
  );
}

export default NameFilterApp;

W tym przykładzie:

  1. useState służy do śledzenia bieżącego filtra (filter) i listy imion (names).
  2. useCallback optymalizuje funkcję obsługującą zmiany w polu tekstowym filtra, żeby uniknąć niepotrzebnego ponownego renderowania.
  3. useMemo optymalizuje obliczenia związane z filtrowaniem listy imion i liczeniem łącznej liczby liter w przefiltrowanych imionach. Dzięki temu obliczenia wykonują się tylko wtedy, gdy zmieni się names lub filter, a nie przy każdym renderowaniu.

Nowy kompilator zoptymalizuje kod aplikacji automatycznie, więc możesz całkowicie usunąć z kodu wszystkie używane dotąd hooki wydajnościowe:

Without memoizationtsx
import { useState } from "react";

function NameFilterApp() {
  const [names, setNames] = useState([
    "Tommy",
    "Arthur",
    "Polly",
    "John",
    "Grace",
  ]);
  const [filter, setFilter] = useState("");

  const handleFilterChange = (event) => {
    setFilter(event.target.value);
  };

  const filteredNames = names.filter((name) =>
    name.toLowerCase().includes(filter.toLowerCase()),
  );

  const totalLettersCount = filteredNames.reduce(
    (acc, name) => acc + name.length,
    0,
  );

  return (
    <>
      <input
        type="text"
        value={filter}
        onChange={handleFilterChange}
        placeholder="Filter names..."
      />
      <div>
        <strong>Filtered Names:</strong>
        {filteredNames.length > 0 ? filteredNames.join(", ") : "No results"}
      </div>
      <div>
        <strong>Total Number of Letters:</strong> {totalLettersCount}
      </div>
    </>
  );
}

export default NameFilterApp;

W React 19 nie musisz już też używać forwardRef.

komponent z forwardRef
komponent z forwardRef

Teraz możemy przekazać ref jako props i korzystać z niego tak jak z każdego innego propsa, co jest sporym ułatwieniem. Zobacz, co się zmieniło:

komponent bez forwardRef
komponent bez forwardRef

💪 Dyrektywy i akcje

dyrektywy
dyrektywy

'use client'

Dyrektywa 'use client' oznacza w React kod, który ma działać po stronie klienta. Umieszczając 'use client' na początku pliku, sygnalizujesz Reactowi i zgodnym bundlerom, że moduł wraz z zależnościami jest przeznaczony do wykonania po stronie klienta. Przydaje się to szczególnie w aplikacjach korzystających z komponentów serwerowych, bo pozwala wyraźnie oddzielić kod działający na serwerze od kodu działającego na kliencie.

przypadek użycia

Typowe zastosowanie 'use client' to interaktywne komponenty, które potrzebują dostępu do API przeglądarki albo muszą zachowywać stan między renderowaniami. Komponent z interaktywnymi formularzami, animacjami czy dostępem do local storage musi działać po stronie klienta.

InteractiveComponenttsx
'use client';

import { useState } from 'react';

function InteractiveComponent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

Komponenty, które bezpośrednio manipulują DOM-em albo korzystają z API specyficznych dla przeglądarki, na przykład pobierają geolokalizację, odtwarzają dźwięk lub animują elementy przez Web Animations API, powinny być oznaczone do działania po stronie klienta.

GeolocationComponenttsx
'use client';

import { useEffect } from 'react';

function GeolocationComponent() {
  useEffect(() => {
    navigator.geolocation.getCurrentPosition(position => {
      console.log(position);
    });
  }, []);

  return <p>Check the console for the current position.</p>;
}

Ograniczenia i wady

  • Podział kodu: Dyrektywa 'use client' pozwala wyraźnie odróżnić kod kliencki od serwerowego, ale programista musi tym podziałem zarządzać, co w większych aplikacjach zwiększa złożoność.
  • Większy bundle: Kod oznaczony do działania po stronie klienta trafia do przeglądarki, co może zwiększyć rozmiar bundle'a, zwłaszcza gdy po stronie klienta ma działać dużo kodu.
  • Zarządzanie stanem: Komponenty klienckie same zarządzają swoim stanem, co może prowadzić do powielania logiki stanu między klientem a serwerem.

Mimo tych wyzwań dyrektywa 'use client' daje duże możliwości optymalizacji wydajności i poprawy wrażeń użytkownika, bo pozwala uruchamiać interaktywne komponenty po stronie klienta. Świadomie decydując, które części aplikacji powinny działać na kliencie, można znaleźć dobrą równowagę między wydajnością a bogactwem funkcji.

dokumentacja dyrektywy use client w React

'use server'

Dyrektywa 'use server' pozwala wywoływać funkcje serwerowe z poziomu klienta, oznaczając je jako akcje serwerowe. Umieszczenie 'use server' na początku ciała funkcji asynchronicznej sygnalizuje, że może ją wywołać kod kliencki - wtedy do serwera idzie żądanie sieciowe z serializowanymi argumentami.

Przykład akcji formularza

Typowe zastosowanie 'use server' to obsługa wysyłania formularzy. Formularz zapisu do newslettera może na przykład wywołać akcję serwerową, która doda adres e-mail do bazy danych.

SignUpFormtsx
// SignUpForm.js (Client Component)
import { useState } from 'react';

function SignUpForm({ submitEmail }) {
  const [email, setEmail] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    await submitEmail({ email });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <button type="submit">Sign Up</button>
    </form>
  );
}

// submitEmail.js (Server Action)
'use server';

export async function submitEmail({ email }) {
  // Logic to add email to database
}

Mutacje danych

Akcje serwerowe można też wykorzystać do mutacji danych, takich jak aktualizacja profilu użytkownika czy dodanie komentarza, gdzie logika mutacji jest zamknięta w funkcji serwerowej.

Ograniczenia i wady

  • Asynchroniczne wykonanie: Akcje serwerowe wymagają wywołania sieciowego, więc z natury są asynchroniczne, co może komplikować zarządzanie stanami UI i informacją zwrotną przy długotrwałych operacjach.
  • Ograniczenia serializacji: Argumenty przekazywane do akcji serwerowych i zwracane przez nie wartości muszą być serializowalne. Ogranicza to typy danych, które można przesyłać między klientem a serwerem, i wyklucza funkcje, elementy JSX oraz inne typy nieserializowalne.
  • Bezpieczeństwo: Akcje serwerowe można wywoływać z danymi kontrolowanymi przez klienta, dlatego trzeba koniecznie walidować i sanityzować dane wejściowe, by zapobiegać podatnościom takim jak SQL injection czy cross-site scripting (XSS).
  • Zależność od opóźnień sieci: Szybkość reakcji akcji serwerowych zależy od warunków sieciowych. Może to wpływać na odczuwaną szybkość operacji uruchamianych przez użytkownika, szczególnie przy wolnym lub niestabilnym połączeniu.

Zalety

  • Mniejsza złożoność po stronie klienta: Przenosząc operacje na serwer, upraszczasz logikę kliencką, zwłaszcza przy przetwarzaniu wrażliwych danych lub złożonych transakcjach.
  • Dostęp do zasobów serwerowych: Akcje serwerowe mogą korzystać z zasobów po stronie serwera, takich jak bazy danych czy zewnętrzne API, do których klient nie ma bezpośredniego dostępu.
  • Większe bezpieczeństwo: Wykonywanie akcji na serwerze daje lepszą kontrolę nad bezpieczeństwem, bo wrażliwe operacje i dostęp do danych można ściślej kontrolować i monitorować.

Dyrektywa 'use server' daje wygodny model włączania logiki serwerowej do aplikacji reactowych i pozwala budować wydajniejsze oraz bezpieczniejsze aplikacje webowe. Rozsądnie stosując akcje serwerowe, można poprawić wrażenia użytkownika, zachowując niezawodność i skalowalność aplikacji.

dokumentacja dyrektywy use server w React

Obie dyrektywy ułatwiają tworzenie aplikacji z wyraźnym rozdzieleniem odpowiedzialności między logiką kliencką i serwerową, co przekłada się na efektywność i wydajność aplikacji w React.

🪝 Nowe hooki Reacta

Hook use()

hook use()
hook use()

Hook use() w React 19 wprowadza elastyczny sposób dostępu do zasobów, takich jak promise'y czy konteksty, wewnątrz komponentów. W przeciwieństwie do innych hooków można go używać w pętlach i instrukcjach warunkowych. Hook został zaprojektowany z myślą o współpracy z Suspense i error boundaries, co daje wygodniejszy sposób asynchronicznego pobierania danych i zarządzania kontekstem

Pobieranie danych przed React 19

fetch data before react 19tsx
import { useEffect, useState } from "react";

const FetchDataComponent = () => {
  const [posts, setPosts] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(
          "https://jsonplaceholder.typicode.com/posts",
        );
        if (!response.ok) {
          throw new Error("Data could not be fetched!");
        }
        const data = await response.json();
        setPosts(data);
      } catch (error) {
        setError(error.message);
      } finally {
        setIsLoading(false);
      }
    };

    fetchData();
  }, []);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error: {error}</div>;
  }

  return (
    <div>
      <h1>Test Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
};

export default FetchDataComponent;


W tym komponencie wykonujemy następujące kroki:

  1. Definicja stanu: Hookiem useState tworzymy stany przechowujące wpisy, stan ładowania (isLoading) i ewentualne błędy (error).
  2. Pobieranie danych: Hookiem useEffect po załadowaniu komponentu wykonujemy asynchroniczne żądanie fetch do API. Funkcja fetchData jest asynchroniczna, więc możemy użyć await, żeby poczekać na odpowiedź sieciową.
  3. Zarządzanie stanem: W zależności od odpowiedzi aktualizujemy stan wpisów, ładowania i błędu. Do obsługi błędów podczas żądania używamy try...catch.
  4. Renderowanie: Na podstawie stanu komponent renderuje informację o ładowaniu, błąd albo listę wpisów.

Pobieranie danych w React 19 z hookiem use()

fetch data react using the use() hook in react 19tsx
// 1. Import React Hooks and Components
import { use, Suspense } from "react";

// 2. Define the Data Fetching Function
// This function fetches posts from a remote API and returns a promise.
const fetchPosts = () => {
  return fetch("https://jsonplaceholder.typicode.com/posts")
    .then((response) => {
      if (!response.ok) {
        throw new Error("Data could not be fetched!");
      }
      return response.json();
    });
};

// Component that uses the `use` hook to fetch and display posts
const FetchDataComponent = () => {
  // 3. Use the `use` Hook to Fetch Data
  // The `use` hook is called with the promise returned by `fetchPosts`.
  const postsPromise = fetchPosts();
  const posts = use(postsPromise);

  // 5. Map Over the Fetched Data
  // This renders a list of posts by mapping over the array of fetched posts.
  return (
    <div>
      <h1>Test Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
};

// 6. Render the Component Tree
// The `App` component that renders the `FetchDataComponent` within a `Suspense` component.
export default function App() {
  return (
    // 4. Handle Loading State with `Suspense`
    // `Suspense` shows a fallback UI while the data is being fetched.
    <Suspense fallback={<div>Loading...</div>}>
      <FetchDataComponent />
    </Suspense>
  );
}

W tym podejściu, z użyciem nowego hooka use(), wykonujemy następujące kroki:

  1. Import hooków i komponentów Reacta: Kod zaczyna się od zaimportowania potrzebnych hooków i komponentów z Reacta, w tym hooka use i komponentu Suspense.
  2. Definicja funkcji pobierającej dane: Poza komponentem Reacta definiujemy funkcję fetchPosts. Odpowiada ona za pobranie danych ze zdalnego API (https://jsonplaceholder.typicode.com/posts). Korzysta z API fetch, wysyła żądanie GET i zwraca promise, który rozwiązuje się danymi albo odrzuca błędem.
  3. Użycie hooka use do pobrania danych: W komponencie FetchDataComponent wywołujemy hook use, przekazując mu promise zwrócony przez fetchPosts. Hook use odczytuje wartość tego promise'a. Warto pamiętać, że sposób użycia hooka use z promise'em może się różnić w zależności od konfiguracji projektu.
  4. Obsługa stanu ładowania przez Suspense: FetchDataComponent jest owinięty komponentem Suspense w nadrzędnym komponencie App. Komponent Suspense służy do wyświetlania fallback UI (w tym przypadku <div>Loading...</div>), dopóki promise oczekuje na rozwiązanie. Gdy to nastąpi, fallback UI zostaje zastąpiony interfejsem FetchDataComponent z pobranymi danymi.
  5. Mapowanie pobranych danych: W FetchDataComponent mapujemy pobrane dane (zakładamy, że to tablica wpisów), żeby wyrenderować ich listę. Każdy wpis jest wyświetlany jako element listy (<li>) ze swoim tytułem.
  6. Renderowanie drzewa komponentów: Na koniec renderowany jest komponent App (zawierający Suspense i FetchDataComponent). Po wyrenderowaniu App najpierw pokazuje stan ładowania, a gdy dane zostaną pobrane i promise się rozwiąże, wyświetla listę wpisów.

Odczyt kontekstu przed React 19

W tym przykładzie ThemeContext.Provider pozwala przekazać wartość "dark" wszystkim komponentom w drzewie, które korzystają z tego kontekstu. ThemeComponent odczytuje wartość kontekstu za pomocą useContext.

app.tstsx
import React from 'react';
import { ThemeContext, ThemeComponent } from './Context';

// The main application component with a context provider
const App: React.FC = () => {
  return (
    <ThemeContext.Provider value="dark">
      <ThemeComponent />
    </ThemeContext.Provider>
  );
}

export default App;
context.tsxtsx
import React, { createContext, useContext, ReactNode } from 'react';

// Creating a Context with a default value of 'light'
export const ThemeContext = createContext<string>('light');

interface ThemeComponentProps {
  children?: ReactNode;
}

// Component using useContext
export const ThemeComponent: React.FC<ThemeComponentProps> = () => {
  const theme = useContext(ThemeContext);
  return <div>Current theme: {theme}</div>;
}

W tym przykładzie ThemeContext.Provider pozwala przekazać wartość "dark" wszystkim komponentom w drzewie, które korzystają z tego kontekstu. ThemeComponent odczytuje wartość kontekstu za pomocą useContext.

Odczyt kontekstu w React 19

Teraz zamiast useContext() będziemy mieć use(context).

context.tsxtsx
import React, { createContext, ReactNode, use } from 'react';

// Creating a Context with a default value of 'light'
export const ThemeContext = createContext<string>('light');

interface ThemeComponentProps {
  children?: ReactNode;
}

// Component using the new use() hook
export const ThemeComponent: React.FC<ThemeComponentProps> = () => {
  const theme = use(ThemeContext);
  return <div>Current theme: {theme}</div>;
}

Zalety hooka use

  • Asynchroniczne ładowanie zasobów: Różne zasoby można bez trudu ładować asynchronicznie, co poprawia wydajność i wrażenia użytkownika.
  • Prostsze pobieranie danych: Zastępuje hook useEffect przy pobieraniu danych, dając prostszy sposób wysyłania żądań do API i obsługi odpowiedzi.
  • Dostęp do danych z kontekstu: Może zastąpić useContext przy odczycie kontekstu, co ogranicza boilerplate i upraszcza zarządzanie kontekstem.

Praktyczne zastosowania

  • Pobieranie danych: Hook use sprawia, że pobieranie danych z API jest bardziej intuicyjne. W połączeniu z React Suspense pozwala wyświetlić fallback UI podczas ładowania, a po zakończeniu pobierania płynnie zaktualizować interfejs.
  • Zarządzanie kontekstem: Upraszcza odczyt kontekstu, zastępując useContext hookiem use. Łatwiej jest korzystać z kontekstu w komponentach, co dodatkowo zmniejsza złożoność aplikacji w React.

oficjalna dokumentacja hooka use()

Hook useFormStatus()

useFormStatus()
useFormStatus()

Hook useFormStatus w React dostarcza przydatnych informacji o statusie wysyłania formularza, co jest kluczowe przy obsłudze stanów formularza i interakcji użytkownika. Został zaprojektowany z myślą o komponentach renderowanych wewnątrz formularza i zwraca obiekt statusu ze szczegółami dotyczącymi wysyłania.

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

Jak to działa

Aby skutecznie korzystać z hooka useFormStatus, Twój komponent, na przykład przycisk Submit, musi znajdować się wewnątrz elementu <form>. Hook podłącza się pod proces wysyłania formularza i udostępnia właściwości takie jak pending, informującą, czy formularz jest właśnie wysyłany.

Użycie

  • Parametry: useFormStatus nie przyjmuje żadnych parametrów.
  • Zwraca: Hook zwraca obiekt statusu z kilkoma właściwościami:
    • pending: Wartość logiczna informująca, czy formularz jest w trakcie wysyłania. Wartość true oznacza, że wysyłanie trwa.
    • data: Obiekt zgodny z interfejsem FormData, zawierający dane wysyłane przez formularz. Będzie mieć wartość null, jeśli żadne wysyłanie nie trwa albo komponent nie znajduje się w formularzu.
    • method: Ciąg znaków 'get' lub 'post' określający metodę HTTP używaną przez formularz. Metoda wynika z atrybutu method formularza, a jeśli go nie podano, domyślnie jest to 'get'.
    • action: Funkcja przekazana do propsa action formularza. Będzie mieć wartość null, jeśli komponent nie jest objęty nadrzędnym <form>, jeśli props action jest adresem URI albo jeśli nie podano propsa action.

Ważne uwagi

  • Zależność od formularza: useFormStatus trzeba wywołać w komponencie znajdującym się wewnątrz <form>. Hook dostarcza informacje wyłącznie o nadrzędnym <form> i poza tym kontekstem nie zadziała.
  • Zakres informacji o statusie: Hook śledzi wyłącznie status wysyłania nadrzędnego <form>. Nie obejmuje formularzy zagnieżdżonych w tym samym komponencie ani w komponentach potomnych.

Przykład:

useFormStatustsx
import { useFormStatus } from "react-dom";
import { submitForm } from "./actions.js";

function Submit() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? "Submitting..." : "Submit"}
    </button>
  );
}

function Form({ action }) {
  return (
    <form action={action}>
      <Submit />
    </form>
  );
}

export default function App() {
  return <Form action={submitForm} />;
}

Korzystając z useFormStatus w formularzach Reacta, możesz dać użytkownikowi lepszą informację zwrotną podczas wysyłania i uzyskać większą kontrolę nad tym procesem, co czyni interfejs bardziej responsywnym i czytelnym.

Hook useFormState()

Hook useFormState oferuje nowatorskie podejście do zarządzania stanem formularza na podstawie wyników akcji formularza. Jest szczególnie przydatny w połączeniu z komponentami serwerowymi, bo poprawia interaktywność formularzy, jeszcze zanim JavaScript po stronie klienta w pełni się załaduje.

Co daje useFormState

useFormState pozwala tworzyć stan komponentu aktualizowany w odpowiedzi na akcje formularza. Przekazując do useFormState funkcję akcji i stan początkowy, dostajesz nową akcję formularza oraz najnowszy stan, z których możesz korzystać w komponentach formularza.

Jak używać useFormState

Zdefiniujmy prostą funkcję akcji symulującą wysłanie formularza, które może zakończyć się sukcesem albo błędem w zależności od danych wejściowych. Następnie zaktualizujemy komponent StatefulForm, żeby wyświetlał komunikaty o sukcesie i błędzie.

Akcja formularza symulująca sukces i błąd

submitFormtypescript
// actions.js
// Simulate a form submission that can either succeed or fail.
export async function submitForm(previousState, formData) {
  try {
    // Simulate checking formData for success or failure criteria.
    if (formData.get('exampleField') === 'success') {
      return { status: 'success', message: 'Form submitted successfully.' };
    } else {
      throw new Error('Form submission failed.');
    }
  } catch (error) {
    return { status: 'error', message: error.message };
  }
}

Komponent StatefulForm z komunikatami o sukcesie i błędzie

StatefulFormtsx
import React from 'react';
import { useFormState } from 'react-dom';
import { submitForm } from './actions';

function StatefulForm() {
  const [state, formAction] = useFormState(submitForm, { status: 'idle', message: '' });

  const handleSubmit = (event) => {
    event.preventDefault();
    const formData = new FormData(event.target);
    formAction(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="exampleField" placeholder="Type 'success' for success" />
      <button type="submit">Submit</button>

      {/* Feedback based on the form's submission state */}
      {state.status === 'success' && <p style={{ color: 'green' }}>{state.message}</p>}
      {state.status === 'error' && <p style={{ color: 'red' }}>{state.message}</p>}
    </form>
  );
}

export default StatefulForm;

Wyjaśnienie kodu

  • Funkcja akcji (submitForm): Ta funkcja symuluje proces wysyłania formularza. Zwraca obiekt stanu zawierający status i message. status może przyjąć wartość success lub error w zależności od wysłanych danych.
  • Komponent formularza (StatefulForm):
    • Inicjalizuje stan formularza za pomocą useFormState, ze stanem początkowym oznaczającym bezczynność.
    • Renderuje prosty formularz z polem tekstowym i przyciskiem wysyłania. Wysłanie obsługuje handleSubmit, który tworzy obiekt FormData i przekazuje go do formAction.
    • Wyświetla komunikaty zależne od stanu wysyłania. Komunikaty o sukcesie są zielone, a o błędzie czerwone.

Ten przykład pokazuje, jak za pomocą useFormState dynamicznie zarządzać informacją zwrotną po wysłaniu formularza, dając użytkownikowi natychmiastową i konkretną reakcję na jego działania.

Hook useOptimistic

hook useOptimistic()
hook useOptimistic()

Hook useOptimistic w React pozwala sprawić, że interfejs sprawia wrażenie szybszego, bo optymistycznie aktualizuje go przed zakończeniem długotrwałych operacji, takich jak żądania sieciowe. Takie podejście szczególnie przydaje się tam, gdzie zależy nam na natychmiastowej reakcji na działania użytkownika, na przykład przy wysyłaniu formularzy czy aktualizacji danych.

Jak działa useOptimistic

useOptimistic pozwala tymczasowo zaktualizować UI na podstawie oczekiwanego wyniku, przy założeniu, że operacja się powiedzie. Przyjmuje fragment stanu i funkcję aktualizującą (updateFn), która określa, jak połączyć bieżący stan z optymistyczną aktualizacją. Hook zwraca dwa elementy:

  • optimisticState: Stan uwzględniający optymistyczną aktualizację.
  • addOptimistic: Funkcja, którą wywołujesz z wartością optymistycznej aktualizacji.

Przykład z useOptimistic

Stwórzmy prosty przykład optymistycznej aktualizacji UI przy wysyłaniu wiadomości. Interfejs od razu pokaże wiadomość ze statusem "Sending...", dopóki operacja się nie zakończy.

MessageSendertsx
import React, { useState } from 'react';
import { useOptimistic } from 'react';

function MessageSender() {
  const [messages, setMessages] = useState([]);
  const [messageInput, setMessageInput] = useState('');

  const [optimisticMessages, addOptimistic] = useOptimistic(messages, (currentMessages, optimisticMessage) => {
    // Merge the current messages with the optimistic update
    return [...currentMessages, optimisticMessage];
  });

  const sendMessage = async (message) => {
    addOptimistic({ id: Date.now(), text: message, status: 'Sending...' });
    try {
      // Simulate a network request to send the message
      await new Promise(resolve => setTimeout(resolve, 1000));
      // After the request completes, update the actual state
      setMessages(prev => [...prev, { id: Date.now(), text: message, status: 'Sent' }]);
    } catch (error) {
      console.error('Failed to send message:', error);
    }
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    sendMessage(messageInput);
    setMessageInput('');
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input 
          value={messageInput} 
          onChange={(e) => setMessageInput(e.target.value)} 
          placeholder="Type a message" 
        />
        <button type="submit">Send</button>
      </form>
      <ul>
        {optimisticMessages.map(msg => (
          <li key={msg.id}>{msg.text} - {msg.status}</li>
        ))}
      </ul>
    </div>
  );
}

Wyjaśnienie kodu

  1. Inicjalizacja stanu: Komponent przechowuje dwie zmienne stanu: messages z faktycznymi wiadomościami i messageInput z bieżącą wartością pola.
  2. Stan optymistyczny: useOptimistic tworzy stan optimisticMessages, który łączy faktyczne wiadomości z optymistycznymi aktualizacjami.
  3. Wysyłanie wiadomości: Funkcja sendMessage optymistycznie aktualizuje UI za pomocą addOptimistic, od razu pokazując wiadomość ze statusem "Sending...". Następnie symuluje żądanie sieciowe, a po jego zakończeniu aktualizuje stan messages, odnotowując, że wiadomość została wysłana.
  4. Renderowanie UI: Komponent renderuje formularz do wpisywania wiadomości i ich listę. Lista odzwierciedla stan optimisticMessages, pokazując optymistycznie status "Sending..." do czasu zakończenia symulowanego żądania.

Ten przykład pokazuje, jak useOptimistic poprawia wrażenia użytkownika, dając natychmiastową reakcję na działania, których wynik jest spodziewany, ale jeszcze niepotwierdzony - dzięki temu aplikacja sprawia wrażenie szybszej i bardziej responsywnej.

Podsumowanie trzech poznanych hooków do formularzy

nowe hooki do formularzy
nowe hooki do formularzy

Podsumowanie

React 19 wprowadza zestaw funkcji i usprawnień, które mają poprawić wygodę pracy programistów i wydajność aplikacji. W centrum tych zmian jest React Compiler, który optymalizuje ponowne renderowania i ogólną wydajność aplikacji, znacznie ograniczając potrzebę ręcznych optymalizacji. Nowa wersja ma usprawnić proces tworzenia aplikacji i pozwolić pisać bardziej czytelny i zwięzły kod.

Najważniejsze cechy React 19:

  • React Compiler: Główny element React 19, który przekształca kod Reacta w zoptymalizowany JavaScript, automatyzując optymalizacje wydajności i ograniczając niepotrzebne renderowania. Dzięki temu mniej polegamy na ręcznych technikach memoizacji, takich jak useCallback(), useMemo() i memo().
  • Prostsze tworzenie aplikacji: Skoro optymalizacją zajmuje się kompilator, kod komponentów może być prostszy. Pokazany przykład przechodzi od wersji wymagającej hooków wydajnościowych do czystszej wersji bez nich, właśnie dzięki optymalizacjom kompilatora.
  • Dyrektywy i akcje: React 19 wprowadza dyrektywy 'use client' i 'use server', które wyraźniej rozdzielają logikę kliencką i serwerową, dzięki czemu kod jest wydajniejszy i lepiej zorganizowany. Dyrektywa 'use server' ułatwia wdrażanie akcji serwerowych, upraszczając mutacje danych i wysyłanie formularzy.
  • Nowe hooki Reacta: Zestaw nowych hooków, w tym use() do dostępu do zasobów, useFormStatus() do statusu wysyłania formularza i useOptimistic do optymistycznych aktualizacji UI, dodatkowo ułatwia pracę, upraszczając asynchroniczne pobieranie danych, zarządzanie stanem formularzy i budowanie responsywnych interfejsów.

React 19 nie skupia się na nowej składni czy koncepcjach, które utrudniałyby naukę, lecz na dopracowaniu frameworka: mniej boilerplate'u, lepsza wydajność i bardziej intuicyjna praca z Reactem. Ta wersja to duży krok w stronę wydajniejszych i przyjaźniejszych programistom aplikacji w React.

Dziękuję za poświęcony czas i przeczytanie tego wpisu. Mam nadzieję, że te przemyślenia okazały się dla Ciebie wartościowe i dały Ci do myślenia. Nie ma tu na razie sekcji komentarzy, ale bardzo doceniam Twoje zaangażowanie i zainteresowanie.

Rozwijaj się, ucz i inspiruj innych.

Na razie!