Wyślij zapytanie Dołącz do Sii

W październiku 2020 roku ukazała się wersja Reacta 17, która nie wprowadziła zbyt wielu nowości. Po około 1,5 roku od tej daty pojawiło się wydanie Reacta 18. Znalazło się w nim wiele zmian, które powinny przykuć uwagę niejednego front-end developera. W tym artykule przedstawię czego możemy się spodziewać w najnowszej wersji tej biblioteki.

Instalacja i aktualizacja Reacta do wersji 18

Aby zainstalować Reacta 18, należy w konsoli użyć następującej komendy: „npm install react@18 react-dom@18” lub „npm install react react-dom”. Możemy również skorzystać z polecenia „npx create-react-app nazwa_aplikacji”, jeśli chcemy, żeby została także stworzona od razu podstawowa struktura projektu.

Należy pamiętać, że jeśli chcemy zaktualizować do wersji React 18 już istniejący projekt, to starszą funkcję ReactDom.render należy zastąpić nowszą funkcją ReactDom.createRoot. Następujący fragment kodu:

import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

trzeba zamienić na poniższy:

import ReactDOM from "react-dom/client";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root. Render(<App />);

Jeśli tego nie zrobimy, aplikacja nadal będzie działać, ale nie będziemy mieli możliwości skorzystania z nowych funkcjonalności, które pojawiły się w aktualnej wersji i dzięki którym program jest w stanie osiągnąć lepszą wydajność.

Concurrent rendering mode

Jedną z najistotniejszych zmian, jakie zostały wprowadzone, jest concurrent mode, czyli możliwość renderowania współbieżnego. Nie jest to funkcjonalność, z której można bezpośrednio skorzystać. Jest ona zaimplementowana pod maską Reacta i wykorzystuje takie techniki jak kolejki priorytetów czy wielokrotne buforowanie. Jednak, jak sama dokumentacja wskazuje, znajomość implementacji tej funkcjonalności nie jest potrzebna dla dewelopera.

Tryb renderowania współbieżnego, który został wprowadzony, znacząco poprawia wydajność renderowania aplikacji. Daje on możliwość ustalania priorytetów, kolejkowania oraz przerywania renderów w programie. W dalszej części artykułu znajdziecie informacje, jak wykorzystać ten tryb, używając np. hooka useTransition przy pomocy, którego można ustalić, które renderingi są bardziej priorytetowe od innych.

Transitions

Nowym elementem, jaki pojawił się w React 18 są „transitions” czyli przejścia. Dają one możliwość ustalenia, które aktualizacje interfejsu użytkownika są pilne, a które mniej. Jako przykłady pilnych aktualizacji można wskazać takie interakcje jak wpisywanie tekstu w pole tekstowe czy kliknięcie przycisku. „Transitions” dostarczają hook o nazwie useTransition. Jeśli korzystamy z komponentów opartych o klasy, możemy skorzystać z funkcji startTransition.

Przykładem, kiedy przejścia okazują się przydatne, może być input do filtrowania danych po nazwie wykorzystujący event onChange. Załóżmy, że w evencie jest wykonywane filtrowanie, które zajmuje nieco czasu. W takim przypadku, gdy użytkownik wpisuje tekst do inputa, to z opóźnieniem będzie widział to, co do niego wpisał. Przy pomocy przejść możemy sprawić, aby aktualizacja stanu w polu tekstowym była bardziej priorytetowa niż wyświetlenie przefiltrowanych danych.

Dla zobrazowania posłużę się kodem aplikacji, który wykonując 10 tysięcy iteracji w pętli, ma za zadanie wylosować unikalne liczby z zakresu od 1 do x + 100 000, gdzie x to wartość wpisana do inputa. Następnie wszystkie wylosowane liczby mają zostać wyświetlone. Po uruchomieniu poniższego kodu można zauważyć, że wciśnięcie jakiejkolwiek liczby na klawiaturze (będąc w polu numerycznym) doprowadzi do jej pojawienia się z pewnym opóźnieniem w tym inpucie.

Aby bardziej wyraźnie zaobserwować to niepożądane zjawisko, można zmniejszyć wydajność procesora w przeglądarce przy użyciu narzędzi deweloperskich.

import React, { useState } from "react";

const App = () => {
  const [inputValue, setInputValue] = useState("");
  const [numbers, setNumbers] = useState([]);

  const drawNumbers = (event) => {
    setInputValue(event.target.value);
    if (event.target.value) {
      const drawnNumbers = new Set();
      const multipler = 100000 + parseInt(event.target.value);
      for (let i = 0; i < 10000; i++) {
        const randomNumber = Math.floor(Math.random() * multipler + 1);
        drawnNumbers.add(randomNumber);
      }
      setNumbers([...drawnNumbers]);
    } else {
      setNumbers([]);
    }
  };

  return (
    <>
      <input type="number" onChange={drawNumbers} value={inputValue} />
      {numbers.map((number) => (
        <p key={number}>{number}</p>
      ))}
    </>
  );
};

export default App;

Rozwiązaniem tego problemu jest wykorzystanie tzw. przejść, co zostało zaprezentowane w następnym fragmencie kodu. Został w nim użyty hook useTransition, który dostarcza tablicę dwuelementową. W jej pierwszym elemencie znajduje się flaga isPending, która informuje, czy komponent jest w trakcie wykonywania przejścia. Drugi element to funkcja startTransition, w którą powinny zostać opakowane niekrytyczne aktualizacje interfejsu. W przypadku poniższego kodu jest to aktualizacja stanu wylosowanych liczb.

Dzięki temu zmiana stanu w inpucie ma wyższy priorytet przy renderowaniu i tym samym opóźnienie po wciśnięciu dowolnej liczby na klawiaturze zostało znacząco zniwelowane. Aktualizacje, które znajdują się w startTransition, są traktowane jako mniej priorytetowe i zostają przerwane, gdy w programie pojawią się bardziej pilne zmiany takie jak np. kliknięcie przycisku lub wpisywanie tekstu do pola tekstowego.

import React, { useState, useTransition } from "react";

const App = () => {
  const [inputValue, setInputValue] = useState("");
  const [numbers, setNumbers] = useState([]);
  const [isPending, startTransition] = useTransition();

  const drawNumbers = (event) => {
    setInputValue(event.target.value);
    startTransition(() => {
      if (event.target.value) {
        const drawnNumbers = new Set();
        const multipler = 100000 + parseInt(event.target.value);
        for (let i = 0; i < 10000; i++) {
          const randomNumber = Math.floor(Math.random() * multipler + 1);
          drawnNumbers.add(randomNumber);
        }
        setNumbers([...drawnNumbers]);
      } else {
        setNumbers([]);
      }
    });
  };

  return (
    <>
      <input type="number" onChange={drawNumbers} value={inputValue} />
      {isPending ? (
        <p>Loading</p>
      ) : (
        numbers.map((number) => <p key={number}>{number}</p>)
      )}
    </>
  );
};

export default App;

useDeferredValue

useDeferredValue to hook, który daje możliwość przesunięcia w czasie mało istotnych aktualizacji interfejsu użytkownika. Innymi słowy ma za zadanie odroczyć wyrenderowanie stanu o niższym priorytecie. Jako parametr przyjmuje wartość, która ma zostać odroczona oraz zwraca tę odroczoną wartość. W porównaniu do hooka useTransition wykorzystuje się go, gdy nie mamy pełnej kontroli nad aktualizacją stanu np. gdy przekazujemy wartość stanową przy użyciu propsów.

Aby rozwiązać problem, który został przedstawiony w poprzednim przykładzie polegający na opóźnionym wyświetleniu w polu numerycznym liczby, która została wciśnięta na klawiaturze, można również skorzystać z hooka useDeferredValue, co zaprezentowałem poniżej. Poprzedni kod został nieco zmieniony przez dodanie komponentu NumbersList, w którym wyświetlone zostają wylosowane liczby oraz w którym został wykorzystany omawiany hook.

import React, { useState } from "react";
import NumbersList from "./NumbersList";

const App = () => {
  const [inputValue, setInputValue] = useState("");
  const [numbers, setNumbers] = useState([]);

  const drawNumbers = (event) => {
    setInputValue(event.target.value);
    if (event.target.value) {
      const drawnNumbers = new Set();
      const multipler = 100000 + parseInt(event.target.value);
      for (let i = 0; i < 10000; i++) {
        const randomNumber = Math.floor(Math.random() * multipler + 1);
        drawnNumbers.add(randomNumber);
      }
      setNumbers([...drawnNumbers]);
    } else {
      setNumbers([]);
    }
  };

  return (
    <>
      <input type="number" onChange={drawNumbers} value={inputValue} />
      <NumbersList numbers={numbers}></NumbersList>
    </>
  );
};

export default App;


import React, { useDeferredValue } from "react";

const NumbersList = ({ numbers }) => {
  const deferredNumbers = useDeferredValue(numbers);
  return (
    <>
      {deferredNumbers.map((number) => (
        <p key={number}>{number}</p>
      ))}
    </>
  );
};

export default NumbersList;

useId

Nowy hook useId został stworzony w celu generowania unikatowych id zarówno po stronie klienta jak i po stronie serwera. Tego hooka nie powinno się wykorzystywać do generowania kluczy w liście. Można go użyć w celu połączenia elementów html takich jak input oraz label co zostało zilustrowane w przykładzie poniżej.

import React, { useId } from 'react';

const App = () => {
  const id = useId();

  return (
    <>
      <label htmlFor={"fullName-" + id}>Full Name: </label>
      <div>
        <input id={"fullName-" + id} type="test" />
      </div>
      <label htmlFor={"emailAddress-" + id}>Email: </label>
      <div>
        <input id={"emailAddress-" + id} type="email" />
      </div>
    </>
  );
}

export default App;

useSyncExternalStore

Jest to hook przeznaczony dla autorów bibliotek związanych z zarządzaniem stanem w React. Został wprowadzony po to, aby zapewnić pomoc przy implementowaniu zmian stanu po tym jak wprowadzono tryb współbieżny.

useInsertionEffect

Ten hook wykorzystywany jest przez twórców bibliotek CSS-in-JS. Jest on pewną wersją hooka useEffect, ale jest on wywoływany synchronicznie przed wszystkimi mutacjami DOM.

Strict Mode

W bibliotece React, Strict Mode to narzędzie, które informuje o potencjalnych problemach w aplikacji. Możemy je dodać dla całej lub części aplikacji. Aby to zrobić w wybranych komponentach, należy je owinąć w komponent <React.StrictMode>. W najnowszej wersji Reacta zostało dodane nowe zachowanie trybu Strict Mode, a mianowicie – tryb ścisły może teraz obsługiwać montowanie i demontaż komponentów, a w przyszłości ma zachowywać i przywracać ich stan przy kolejnych montowaniach.

Automatic batching

Do tej pory w React występował tzw. state batching, ale był bardziej ograniczony niż automatic batching. Jeśli wystąpiły dwie aktualizacje stanu lub więcej, to miał on za zadanie grupowanie aktualizacji stanów tak, aby nastąpił tylko jeden ponowny rendering komponentu. Jednak miało to tylko miejsce w środku tzw. synthetic events (onClick, onChange itd.), tak jak pokazałem niżej.

import React, { useState } from "react";

const App = () => {
  const [counter, setCounter] = useState(0);
  const [toogle, setToogle] = useState(false);

  const incrementCounter = () => {
    setCounter(counter + 1);
    setToogle(!toogle);
  };

  return (
    <>
      <button
        onClick={incrementCounter}
        style={{ color: toogle ? "blue" : "green" }}
      >
        Increment Counter
      </button>
      {<p>counter : {counter}</p>}
    </>
  );
};

export default App;

Po uruchomieniu powyższego kodu, zarówno w starszej jak i najnowszej wersji Reacta, komponent zostanie tylko raz przerenderowany po kliknięciu przycisku.

Natomiast jeśli będą aktualizowane dwa stany lub więcej w promise, funkcji setTimeout lub natywnym event handlerze, to w przypadku, gdy aplikacja korzysta ze starszej wersji Reacta, komponent zostanie kilka razy przerysowany. Dla odmiany automatic batching grupuje aktualizacje stanów nawet wtedy, gdy znajdują się one we wcześniej wymienionych miejscach. W kodzie, który dodałem pod spodem, znajduje się promise w funkcji getData, w którym uaktualniane są dwa stany.

Mimo to w najnowszym wydaniu Reacta, wykonując funkcje getData, komponent wyrenderuje się ponownie tylko raz.  

import React, { useState } from "react";

const App = () => {
  const [products, setProducts] = useState([]);
  const [, setIsDownloaded] = useState(false);

  const getData = async () => {
    fetch("https://dummyjson.com/products", {
      method: "GET"
    }).then((response) =>
      response.json().then((response) => {
        setProducts(response.products);
        setIsDownloaded(true);
      })
    );
  };

  return (
    <>
      <button onClick={getData}>Get Data</button>
      {products.map((product) => (
        <p key={product.id}>{product.title}</p>
      ))}
    </>
  );
};

export default App;

Suspense

W najnowszym wydaniu Reacta dodano możliwość wykorzystania komponentu Suspense po stronie serwera m.in. z takimi frameworkami jak Next.js czy Remix.

Wcześniej komponent ten było można użyć tylko po stronie klienta w połączeniu z komponentami leniwymi. Wyświetlał wówczas komponent zastępczy, który przekazuje się we właściwości fallback i jest prezentowany, dopóki komponent lub komponenty znajdujące się w środku komponentu Suspense nie załadowały się w pełni. Zaprezentowałem to poniżej.

import React, { lazy, Suspense } from "react";

const loadingTime = 2000;
const delayRendering = (component) =>
  new Promise((resolve) => setTimeout(() => resolve(component), loadingTime));

const HomePage = lazy(() => delayRendering(import("./HomePage")));
const Loading = () => <div>Loading...</div>;

const App = () => {
  return (
    <Suspense fallback={<Loading />}>
      <HomePage></HomePage>
    </Suspense>
  );
};

export default App;



import React from "react";

const HomePage = () => {
  return <p>Home Page</p>;
};

export default HomePage;

Podsumowanie

W wersji React 18 wprowadzone duże zmiany, których głównym zadaniem jest poprawa wydajności renderowanych aplikacji. Większość wprowadzonych nowości bazuje na trybie renderowania współbieżnego, na co czekaliśmy od dawna. Dzięki pojawieniu się tych możliwości, React ma szansę zyskać jeszcze większą popularność niż ma obecnie. 

Źródła

  1. React Dev Blog
  2. Legacy ReactJS
  3. Vived Blog

***

Jeśli interesuje Cię tematyka Reacta, zajrzyj również do innych artykułów naszych ekspertów.

4.9/5 ( głosy: 12)
Ocena:
4.9/5 ( głosy: 12)
Autor
Avatar
Marcin Tkaczuk

Absolwent Informatyki na Politechnice Białostockiej. Pracuje w Sii jako Frontend Developer. Ma 5-letnie doświadczenie przy tworzeniu aplikacji webowych. Jego ulubionym językiem programowania jest JavaScript ze względu na dużą nieprzewidywalność tego języka. Od 3 lat związany głównie z technologią React. Prywatnie lubi spędzać czas, na świeżym powietrzu i jeździć na rowerze.

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Może Cię również zainteresować

Pokaż więcej artykułów

Bądź na bieżąco

Zasubskrybuj naszego bloga i otrzymuj informacje o najnowszych wpisach.

Otrzymaj ofertę

Jeśli chcesz dowiedzieć się więcej na temat oferty Sii, skontaktuj się z nami.

Wyślij zapytanie Wyślij zapytanie

Natalia Competency Center Director

Get an offer

Dołącz do Sii

Znajdź idealną pracę – zapoznaj się z naszą ofertą rekrutacyjną i aplikuj.

Aplikuj Aplikuj

Paweł Process Owner

Join Sii

ZATWIERDŹ

This content is available only in one language version.
You will be redirected to home page.

Are you sure you want to leave this page?