React jest jedną z najpopularniejszych bibliotek języka Javascript, służącą do tworzenia interfejsów użytkownika, a konkretniej tych webowych, czyli stron internetowych. Został on stworzony w podziemiach tajnej fabryki Facebooka ;), inne duże korporacje, jakie z niego korzystają to PayPal czy Netflix. Na chwilę obecną można śmiało powiedzieć, ze React stał się dominującą biblioteką w ekosystemie JS. W tym React wprowadzenie utworzymy, krok po kroku, pierwszą, pełnoprawną aplikację internetową.
Najważniejsza, dla mnie, cecha Reacta
Jestem programistą wychowanym na Javie i rzeczą, która przekonała mnie do Reacta jest podział aplikacji Javascriptowej na komponenty, czyli odizolowane od siebie fragmenty kodu, gdzie każdy odpowiada za inny kawałek strony, dzięki czemu źródła programu stają się o wiele bardziej czytelne i uporządkowane.
Czyli jeden wielki plik index.html w pseudokodowej postaci dla galerii obrazków:
<body> <!-- Header starts --> <div> <div> <h2><center>...</h2></center> </div> <div> <!-- Header ends --> <!-- Images --> <div> <!-- Image --> <div> <img>...</img> <div> <!-- Image --> <div> <img>...</img> <div> .... </body>
Rozbija nam się, w przybliżeniu, na:
<body> <Header/> <Image/> <Image/> </body>
Ów Header i Image to właśnie komponenty. Prawda, że wygląda to obiecująco?
React wprowadzenie na przykładzie prostej aplikacji
Głównym czynnikiem hamulcowym na początku istnienia Reacta był poziom komplikacji jego konfiguracji. Trzeba było odpowiednio zestawić Webpack, Babel, NPM. Nawet doświadczonemu programiście Javascript potrafiło to zająć trochę czasu… a wszystkich pozostałych po prostu odrzucało.
Następnie pojawił się create-react-app, czyli starter projektów Rectowych, który zestawia to wszystko za nas, schowane, ukryte. Nawet nie musimy wiedzieć czym Babel czy Webpack jest. Szczerze? Ja nie wiedziałem, dzięki czemu mogłem szybko zabrać się za samego Reacta bez babrania się w konfiguracji środowiska, a braki uzupełniać później, w miarę potrzeb.
CRA używany był w oryginalnym poście, teraz, przy okazji aktualizacji zmienimy nieco taktykę. Postawimy projekt sami, zainstalujemy tylko niezbędne biblioteki, a tylko część odpowiedzialną za „złożenie” projektu w plik wynikowy oddelegujemy do Parcela. Parcel jest to bundler projektów JS – w dużym uproszczeniu bierze wszystkie pliki naszego projektu i klei je w jeden plik wynikowy, który umieszczamy na serwerze. Te samą funkcję pełni Webpack. Różnica jest taka, że Webpack jest świetny do niestandardowych zadań, wymaga odrobinę (albo gigantyczną ilość, w zależności od zadania) konfiguracji, natomiast Parcel doskonale sprawdza się do standardowych projektów i wskazuję się mu tylko plik wejściowy naszego projektu (np. index.html) i odpala. Cała reszta dzieje się automagicznie. Więc wciąż trochę magii jest jak w create-react-app ale jest jej zdecydowanie mniej.
Szkielet projektu
Zacznijmy od utworzenia folderu mygallery i odpaleniu komendy npm init. Acha, zakładam, że masz zainstalowanego NodeJS i jakiś edytor, na przykład Visual Studio Code. Jeśli nie – zrób to teraz, następnie otwórz ten katalog w Visual Studio Code, wybierz z górnego menu Terminal -> New i dopiero potem odpal npm init w folderze mygallery. Zapyta nas o parę rzeczy. Wybieramy wszędzie domyślne wartości używając entera.
Zostanie stworzony plik package.json, zawierający m.in. listę zewnętrznych zależności naszego projektu czy skrypty pomocnicze.
Wraz z Node instaluje nam się npm, czyli manager zewnętrznych zależności naszego projektu. Użyjemy go do zainstalowania React i wspomnianego parcel. Odpalamy w terminalu
npm install react, react-dom –save
oraz
npm install parcel-bundler –save-dev
Gdy po zakończeniu pracy tych komend otworzymy plik package.json ujrzymy tam dwa nowe wpisy – dependencies i devDependencies.
{ "name": "mygallery", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "react": "^16.12.0", "react-dom": "^16.12.0" }, "devDependencies": { "parcel-bundler": "^1.12.4" }
Pierwsze z nich są to zależności naszego programu, drugie natomiast to zależności deweloperskie – czyli takie, które są przydatne podczas tworzenia programu, natomiast klient w zupełności ich nie potrzebuje.
Następnie tworzymy katalog src, a w nim plik index.html. Jeśli używamy Visual Studio Code to możemy użyć skrótu html:5 by stworzyć szkielet html. Od razu zmieńmy title naszej strony na bardziej stosowny i dodajmy sekcję main o id „root” w body.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>Moja Galeria</title> </head> <body> <main id="root"></main> </body> </html>
Pierwszy komponent React
Stwórzmy teraz plik App.js w katalogu src, a w nim kawałek skryptu
import React from "react"; import ReactDOM from "react-dom"; export const Comp = () => React.createElement("h1", {}, "Hello!"); ReactDOM.render(React.createElement(Comp), document.getElementById("root"));
A na końcu index.html dodajemy ładowanie skryptów
<body> <main id="root"></main> <script src="./App.js"></script> </body>
Teraz komendą npx parcel .\src\index.html odpalamy nasz projekt w trybie deweloperskim.
Otwieramy adres, który podpowiedział nam Parcel w konsoli (localhost:1234, zazwyczaj) i powinniśmy zobaczyć na stronie radosne „Hello!”, nasz pierwszy Reactowy komponent.
Ale.. co tak w ogóle się podziało w tych dwóch linijkach?
W pierwszej stworzyliśmy komponent Reactowy, odbicie klasycznego taga h1 po stronie React. Drugi parametr metody, pusty obiekt, to atrybuty dla taga, i narazie tyle w temacie. Ostatni parametr to zawartość nowo utworzonego komponentu. W naszym wypadku jest to prosta wartość tekstowa „Hello!”.
Druga linijka, tam, gdzie używamy ReactDOM, służy do tego, by Reactowy komponent podpiąć (i przetłumaczyć po drodze) na klasyczny element drzewa DOM naszej strony internetowej. Powoduje to, że na naszej stronie mamy div o id root, pod który został podpięty tag h1 z naszego komponentu Reactowego.
Wprowadzenie React – JSX
W praktyce nikt nie piszę komponentów Reactowych z użyciem React.createElement. Zamiast tego stosuję się JSX (JavaScript eXtended). Jest to format zbliżony do HTML, który następnie jest tłumaczony (przez narzędzie o nazwie Babel, które jest zawarte w Parcelu) na React.createElement.
Po przetlumaczeniu na JSX nasz komponent będzie wyglądał tak:
import React from "react"; import ReactDOM from "react-dom"; export const Comp = () => <h1>Hello!</h1> ReactDOM.render(<Comp />, document.getElementById("root"));
Wygląda to teraz jakby funkcja Comp zwracała tag HTML. Jednak pamiętajmy, że JSX NIE jest to HTML pisany w pliku .js. Jest to semantyczny dodatek, który potem jest TŁUMACZONY na czysty javascript, a konkretnie na React.createElement.
Komponent dla obrazów
Zamieńmy nasz komponent na taki, który wyświetli obraz na podstawie adresu URL
import React from "react"; import ReactDOM from "react-dom"; export const Image = () => ( <img src="https://images.unsplash.com/photo-1508138221679-760a23a2285b"></img> ); ReactDOM.render(<Image />, document.getElementById("root"));
Powinniśmy otrzymać śliczny (i zarazem gigantyczny) obraz samolotu w środku lasu 😉
Szał, prawda? 🙂
Drzewo komponentów
Nasza galeria zdjęć z jednym tylko zdjęciem nie do końca zasługuje na to miano. Przydałoby się więc dodać jeszcze kilka obrazów. Możemy to zrobić powielając komponent <Image>… jednak jak ma się to przedstawiać w funkcji ReactDOM.render?
W ReactJS jeśli chcemy stworzyć kilka równorzędnych komponentów wówczas musimy je opakować w jakiś komponent nadrzędny. Możemy zrobić na przykład tak:
export const Image = () => ( <img src="https://images.unsplash.com/photo-1508138221679-760a23a2285b" height="175" width="200" ></img> ); export const App = () => { return ( <div> <Image /> <Image /> <Image /> <Image /> <Image /> </div> ); }; ReactDOM.render(<App />, document.getElementById("root"));
Stworzyliśmy dodatkowy, główny, komponent App, który ma w sobie jeden węzeł nadrzędny – ’div’ oraz pięć komponentów Image. Przy okazji ustawiliśmy na sztywno wysokość i szerokość obrazu w komponencie Image by nie był tak gigantyczny.
Natomiast dodawanie 'sztucznego’ diva o ile przy tworzeniu nowych aplikacji zdaje się relatywnie bezbolesne to w momencie, kiedy np. migrujemy istniejącą aplikację na React, a nasze testy bazują na X-Path… wtedy taki niegroźny div, jest źródłem wielu zmian w kodzie.
Dlatego jakiś czas temu do Reacta dodano React.Fragment, który zbiera w węzeł nadrzędny komponenty (jak div w przykładzie) na potrzeby Reacta. Nie ma on jednak przełożenia na ostateczny wygląd HTML, czyli po prostu go nie widać.
export const App = () => { return ( <React.Fragment> <Image /> <Image /> <Image /> <Image /> <Image /> </React.Fragment> ); };
Możemy ten kod jeszcze odrobinę skrócić i zamiast React.Fragment możemy napisać po prostu <>.
export const App = () => { return ( <> <Image /> <Image /> <Image /> <Image /> <Image /> </> ); };
React tutorial – props
Wszystko fajnie tylko po co nam 5 takich samych obrazków? Jeśli chcemy mieć inny URL dla zdjęcia musimy tworzyć nowy komponent? Na szczęście nie. Tu pojawiają się propsy, dzięki, którym komponent Image będzie mógł wyświetlać różne obrazy.
Tak jak standardowe tagi HTML mogą mieć pewne właściwości (jak height i width w tagu img), tak komponenty Reactowe również mogą mieć zdefiniowane przez nas właściwości.
Konkretnie do komponentu Image możemy dodać property „url”, który będzie wskazywać na adres obrazu.
export const App = () => { return ( <> <Image url="https://images.unsplash.com/photo-1508138221679-760a23a2285b" /> <Image url="https://images.unsplash.com/photo-1474487548417-781cb71495f3" /> <Image url="https://images.unsplash.com/photo-1580109672851-b85640868813" /> <Image url="https://images.unsplash.com/photo-1580046939256-c377c5b099f1" /> <Image url="https://images.unsplash.com/photo-1576801488695-2e4d7a14b8b5" /> </> ); };
By dobrać się do tego pola, z poziomu komponentu Image, musimy nieco go rozbudować.
export const Image = props => { console.log(props.url); return <img src={props.url} height="175" width="200"></img>; };
Po pierwsze teraz nasza funkcja tworząca komponent bierze jeden argument – props. Jest to obiekt, który ma zestaw pól odpowiadającym właściwością, jakie zostały przesłane do naszego komponentu. W naszym wypadku obiekt props ma pole url, bo zrobiliśmy tak <Image url=”https://images.unsplash.com/photo-1576801488695-2e4d7a14b8b5″ />. Nie ma natomiast propsa ala czy kot bo żaden taki klucz nie został przesłany.
Teraz by odnieść się do wartości przesłanej z poziomu kodu JavaScript używamy standardowo props.url, natomiast jeśli chcemy użyć tej wartości w elemencie zwracanym, czyli w JSX, musimy opakować ją w nawiasy klamrowe {props.url}.
Często w komponentach, zamiast ciągle pisać props. dokonuję się destrukturyzacji parametru funkcji. Wygląda to wówczas tak:
export const Image = ({ url }) => { console.log(url); return <img src={url} height="175" width="200"></img>; };
Zapis {url} oznacza „weź pole url z obiektu, który zostanie przesłany jako parametr tej funkcji i przypisz je do zmiennej url, wyrzuć wszystko inne”. Gdybyśmy mieli więcej interesujących nas pól destrukturyzacja wyglądałaby tak: {url, otherField, anotherField }.
Kod przykładu dostępny jest na Github.
Obsługa zdarzeń w React
Wykonanie kawałka kodu w React po np. kliknięciu komponentu wygląda bardzo podobnie jak w tradycyjnym JavaScript. Po pierwsze w JSX znajdujemy element, który ma nasłuchiwać na zdarzenia i przy pomocy znanego on*, czyli np. onClick, onMouseLeave etc. powiedzieć, na jakie zdarzenie chcemy czekać.
export const Image = ({ url }) => { return ( <img src={url} height="175" width="200" onClick=... ></img> ); };
W powyższym przykladzie dodaliśmy w tagach img właściwość onClick, bo chcemy wykonać kawałek logiki, gdy użytkownik kliknie obrazek.
Ten kawałek logiki umieszczamy w nawiasach klamrowych po onClick=. Musi być to funkcja, która może przyjmować jeden argument – będzie to obiekt event z danymi zdarzenia jakie zostało wywołane.
<img src={url} height="175" width="200" onClick={event => console.log(url)} ></img>
Czyli po kliknięciu elementu img w komponencie Image na konsoli zostanie wypisany adres url danego obrazu.
Wszystko pięknie, tylko nie zawsze logika, którą chcemy wykonać zmieści się w kilku tylko znakach. Wówczas warto przenieść funkcję obsługującą dane zdarzenie do osobnej zmiennej.
export const Image = ({ url }) => { const handleOnClick = event => { console.log(url); }; return ( <img src={url} height="175" width="200" onClick={handleOnClick}> </img> ); };
Czyli całość obsługi zdarzenia przypisujemy do zmiennej, a potem podajemy ją jako wartość dla onClick.
Tyle, jeśli chodzi o podstawy, są natomiast dwie sprawy, na które należy zwrócić uwagę pracując z event handlerami.
Gubienie kontekstu, czyli this to nie ten this
Problem ten dotyczy w głównej mierze komponentów klasowych, od których obecnie można w dużej mierze odejść, bo od czasu hooks komponenty funkcyjne mogą mieć własny stan i obsługę zdarzeń cyklu życia, ale to tym w następnym w wpsie.
Nie zmienia to jednak faktu, że warto wiedzieć o problemie z 'this’ przy event handlerach.
Deklarując funkcję obsługującą zdarzenie użyłem arrow function, zamiast klasycznego function(event). Poniżej wersja function vs arrow function.
const hoc = function(event) { console.log(url); } const handleOnClick = event => { console.log(url); };
W naszym przykładzie nie ma to znaczenia, ma natomiast gdybyśmy pracowali z klasami, to przy użyciu function do zdefiniowania funkcji obsługującej event handling zgubilibyśmy kontekst (this).
Arrow function automatycznie łączy daną funkcję z kontekstem, w którym została zdefiniowana.
Dlatego, przeskakując do konkluzji, deklarując funkcję obsługujące zdarzenia warto zawsze używać arrow function zamiast klasycznego podejścia.
Blokowanie domyślnego zachowania
Ważne jest również, by pamiętać o dwóch funkcjach, które powinny być wołane na początku obsługi wydarzenia, mianowicie event.preventDefault() oraz event.stopPropagation().
Do czego one służą?
Część elementów w HTML ma domyślne zachowanie przy pewnych wydarzeniach. Podstawowym przykładem jest kliknięcie elementu button w formularzu. Spowoduje ono przesłanie formularza, a w praktyce przeładowanie strony. Co przy używaniu React jest bardzo niepożądanym zachowaniem. Dlatego na początku funkcji obsługującej zdarzenie warto dodać event.preventDefault() by wyłączyć domyślne zachowanie.
Gdy zmienimy nieco definicję naszego głównego komponentu poprzez dodanie <div> z nasłuchiwaniem na kliknięcie.
export const App = () => { return ( <div onClick={event => console.log("Event from div")}> <Image url="https://images.unsplash.com/photo-1508138221679-760a23a2285b" /> <Image url="https://images.unsplash.com/photo-1474487548417-781cb71495f3" /> <Image url="https://images.unsplash.com/photo-1580109672851-b85640868813" /> <Image url="https://images.unsplash.com/photo-1580046939256-c377c5b099f1" /> <Image url="https://images.unsplash.com/photo-1576801488695-2e4d7a14b8b5" /> </div> ); };
To po kliknięciu obrazu zostanie wywołany event listener zarówno z danego kompnentu Image (czyli wypisze URL na konsolę) ORAZ zostanie wywołany event z div’a, czyli na konsoleęzostanie wypisany też tekst „Event from div”. To zachowanie jest spowodowane tak zwanym 'event propagation’ – zdarzenie wędruję w górę drzewa DOM uruchamiając każdy nasłuchujący na nie event listener.
event.stopPropagation() przerywa propagację. Tak zdefiniowany event listener w Image spowoduję, że pojawi się tylko URL obrazu, a onClick z div’a nie zostanie już uruchomiony.
export const Image = ({ url }) => { const handleOnClick = event => { event.preventDefault(); event.stopPropagation(); console.log(url); }; return <img src={url} height="175" width="200" onClick={handleOnClick}></img>; };
Tyle w temacie obsługi zdarzeń. Nie zrobiliśmy nic w temacie wyglądu czy funkcjonalności strony, ale to się zmieni w przyszłości.
Nowy komponent – Showcase
Stwórzmy nowy komponent o nazwie Showcase, w którym będziemy wyświetlać w większym rozmiarze obraz, na który kliknie użytkownik.
Zacznijmy od wersji ze statycznym adresem grafiki.
export const Showcase = () => { return ( <img src="https://images.unsplash.com/photo-1580109672851-b85640868813" height="525" width="600" ></img> ); }; export const App = () => { return ( <div onClick={event => console.log("Event from div")}> <Image url="https://images.unsplash.com/photo-1508138221679-760a23a2285b" /> <Image url="https://images.unsplash.com/photo-1474487548417-781cb71495f3" /> <Image url="https://images.unsplash.com/photo-1580109672851-b85640868813" /> <Image url="https://images.unsplash.com/photo-1580046939256-c377c5b099f1" /> <Image url="https://images.unsplash.com/photo-1576801488695-2e4d7a14b8b5" /> <Showcase /> </div> ); };
Wszystko fajnie, jednak chcemy, by adres URL się zmieniał w zależności od klikniętego obrazu, więc zmieniał się.
Tak przy okazji… obrazki będą ładować się wolno, bo są w gigantycznej rozdzielczości 🙂
Stan komponentu
Stan jest spokrewniony z propsami. Jest to czysto Reactowa rzecz. O ile propsy przychodzą z góry jako właściwości komponentu przy jego tworzeniu, to state może się zmieniać w zależności od tego, jakie akcje wykona użytkownik. Dodatkowo każda zmiana stanu powoduję ponowne wyrysowanie komponentu w oknie przeglądarki, czyli pozwala na zmianę tego, co widzi użytkownik bez konieczności przeładowania strony.
Stan w komponentach funkcyjnych (a na nich się w tym tutorialu koncentrujemy) tworzymy za pomocą metody useState.
Jest to jeden z podstawowych hooków. Tak zwane hooki w React to specjalne funkcje, które zostały dodane w wersji 16 tej biblioteki. Dzięki nim komponenty funkcyjne zyskały o wiele więcej możliwości, bo wcześniej stan i ogrom innych możliwości był zarezerwowany tylko dla komponentów klasowych, które wymagały więcej kodu do utworzenia.
useState jako argument przyjmuje domyślną (pierwszą) wartość, jaką dany stan ma przyjąć, a zwraca dwie rzeczy: zmienną, pod którą dany stan będzie dostępny, oraz funkcję, za pomocą której będziemy mogli wartość stanu zmienić.
import React, { useState } from "react"; import ReactDOM from "react-dom"; //... export const Showcase = () => { const [url, setUrl] = useState( "https://images.unsplash.com/photo-1580109672851-b85640868813" ); return <img src={url} height="525" width="600"></img>; };
Dodaliśmy import, by móc korzystać z funkcji useState, a następnie użyliśmy jej, by utworzyć nowy stan – url. Dodatkowo dostajemy odnośnik do kolejnej funkcji – setUrl, która będzie wykorzystywana do ustawienia stanu.
Rozbudujmy jeszcze trochę nasz komponent Showcase, dodajmy użycie metody setUrl
export const Showcase = () => { const [url, setUrl] = useState( "https://images.unsplash.com/photo-1580109672851-b85640868813" ); return ( <> <img src={url} height="525" width="600"></img> <button onClick={e => setUrl("https://images.unsplash.com/photo-1580046939256-c377c5b099f1") } > First </button> <button onClick={e => setUrl("https://images.unsplash.com/photo-1508138221679-760a23a2285b") } > Second </button> </> ); };
Dodaliśmy dwa guziki. Po kliknięciu zmienia się obraz wyświetlany przez komponent Showcase. Bez przeładowania strony. Wszystko dzięki użyciu metody setUrl, która zmienia stan, co powoduje przerysowanie strony, a w efekcie wyświetlenie innego obrazu.
Wszystko fajnie, ale nas interesuje, by użytkownik kliknął komponent Image, a w efekcie komponent Showcase wyświetlił odpowiedni obraz. Nie interesują nas dodatkowe guziki.
Przepływ danych w React
Co możemy zrobić to wychwycić zdarzenie kliknięcia komponent Image, dobrać się do wartości właściwości url i przesłać ją do komponentu Showcase. O ile pierwszą część jesteśmy w stanie już zrobić, to przesłanie danych pomiędzy komponentami nie jest na pierwszy rzut oka takie oczywiste (choć na drugi jest już bardzo fajnym i „czystym” rozwiązaniem).
Obecnie drzewo komponentów aplikacji wygląda tak:
Mamy komponent App, który zawiera pewną ilość komponentów Image oraz jeden komponent Showcase.
Chcemy przesłać dane z komponentu Image do komponentu Showcase. W obecnej sytuacji jest to jednak w React niemożliwe. Dlaczego?
W React dane możemy przesyłać tylko w dół drzewa DOM, czyli do komponentów, które nasz komponent zawiera, są jego bezpośrednimi potomkami. Robimy to za pomocą props. Nie ma możliwości przesyłania ani w górę drzewa, do rodzica, ani do swoich braci i sióstr.
W React dane przesyłane są tylko w dół drzewa komponentów.
Podejście do rozwiązania naszego problemu jest następujące: dane, które chcemy współdzielić pomiędzy rodzeństwem ustawiamy jako stan ich rodzica, a następnie przesyłamy do potomstwa ów stan i, jeśli jest taka potrzeba, funkcję do jego zmiany.
Wracając na grunt naszej galerii. Chcemy współdzielić (ustawiać w jednym komponencie, a czytać w drugim) adres URL obrazu, który ma być wyświetlany w Showcase. Ustawmy więc ten stan na poziomie komponentu-rodzica, czyli komponentu App.
export const App = () => { const [showcaseURL, setShowcaseURL] = useState(""); return ( <div> <Image url="https://images.unsplash.com/photo-1508138221679-760a23a2285b" /> <Image url="https://images.unsplash.com/photo-1474487548417-781cb71495f3" /> <Image url="https://images.unsplash.com/photo-1580109672851-b85640868813" /> <Image url="https://images.unsplash.com/photo-1580046939256-c377c5b099f1" /> <Image url="https://images.unsplash.com/photo-1576801488695-2e4d7a14b8b5" /> <Showcase /> </div> ); };
Następnie chcemy by stan ten ustawiany był po kliknięciu komponent Image. Propsy mają tę zaletę, że możemy tam wysyłać nie tylko wartości prymitywne czy obiekty, ale też i funkcje. Możemy więc do komponentu Image wysłać funkcję służącą do ustawiania stanu showcaseURL i wywołać ją po kliknięciu w tag img.
export const Image = ({ url, setShowcaseURL }) => { const handleOnClick = event => { event.preventDefault(); event.stopPropagation(); setShowcaseURL(url); }; return <img src={url} height="175" width="200" onClick={handleOnClick}></img>; }; export const App = () => { const [showcaseURL, setShowcaseURL] = useState(""); return ( <div> <Image url="https://images.unsplash.com/photo-1508138221679-760a23a2285b" setShowcaseURL={setShowcaseURL} /> <Image url="https://images.unsplash.com/photo-1474487548417-781cb71495f3" setShowcaseURL={setShowcaseURL} /> <Image url="https://images.unsplash.com/photo-1580109672851-b85640868813" setShowcaseURL={setShowcaseURL} /> <Image url="https://images.unsplash.com/photo-1580046939256-c377c5b099f1" setShowcaseURL={setShowcaseURL} /> <Image url="https://images.unsplash.com/photo-1576801488695-2e4d7a14b8b5" setShowcaseURL={setShowcaseURL} /> <Showcase /> </div> ); };
Dodatkowo w komponencie App, przy tworzeniu komponentów Image dodajemy propsa setShowcaseURL i jako jego wartość podajemy funkcję do zmiany stanu, którą zwrócił nam useState.
Pomimo, że funkcja zostanie wywołana w innym komponencie, niż została zdefiniowana, to wciąż samo wywołanie będzie miało miejsce w kontekście komponentu App i zmieni nam jego stan.
Ostatnią rzeczą do zrobienia jest przesłanie url klikniętego obrazu do komponentu Showcase jako props. Przy okazji wywalimy zbędne buttony.
export const Showcase = ({ url }) => { return ( <> <img src={url} height="525" width="600"></img> </> ); };
W komponencie App przesyłamy wartość stanu showcaseURL jako props url.
export const App = () => { const [showcaseURL, setShowcaseURL] = useState(""); return ( <div> <Image url="https://images.unsplash.com/photo-1508138221679-760a23a2285b" setShowcaseURL={setShowcaseURL} /> //... <Showcase url={showcaseURL} /> </div> ); };
Gdy teraz odpalimy naszą aplikację za pomocą npx parcel ./src/index.html w końcu będzie ona działać jak należy
Podsumowywując pracę ze stanem:
- Stan komponentu to wartość wewnętrzna danego komponentu, której to zmiana powoduje przerysowanie komponentu.
- Do utworzenia stanu i funkcji go zmieniającej używamy metody useState z biblioteki react.
- W React dane mogą być przesyłane do innych komponentów tylko w dół drzewa DOM, za pomocą props.
- Jeśli jakieś komponenty muszą współdzielić stan, wówczas przenosi sie go do wspólnego komponentu nadrzędnego.
- Jako props możemy przesyłać funkcję, a szczególne funkcje do ustawiania stanu
Dynamiczne tworzenie komponentów
Na chwilę obecną komponenty ilość komponentów Image jest znana z góry, każdy komponent tworzymy ręcznie. Zmienimy tę sytuację.
Jedyne czym różnią się owe komponenty to adres URL wyświetlanego obrazu. Wyciągnijmy je z propsów do tradycyjnej tablicy.
const images = [ "https://images.unsplash.com/photo-1508138221679-760a23a2285b", "https://images.unsplash.com/photo-1474487548417-781cb71495f3", "https://images.unsplash.com/photo-1580109672851-b85640868813", "https://images.unsplash.com/photo-1580046939256-c377c5b099f1", "https://images.unsplash.com/photo-1576801488695-2e4d7a14b8b5" ];
Następnie możemy skorzystać z funkcji map, by zamienić każdy element tablicy na coś innego i przypisać tablicę tak stworzonych komponentów do zmiennej. Na przykład na komponent React.
const imgComponents = images.map(url => { return <Image url={url} setShowcaseURL={setShowcaseURL} />; });
JSX to tylko sposób na wygodniejsze zapisanie funkcji React.createElement, więc możemy go wykorzystywać również poza 'return’. Zamiast kilklu komponentów Image możemy zwrócić zmienną imgComponents, pod którą to kryję się cały zestaw owych komponentów.
Finalnie nasz komponent App wygląda następująco
export const App = () => { const [showcaseURL, setShowcaseURL] = useState(""); const images = [ "https://images.unsplash.com/photo-1508138221679-760a23a2285b", "https://images.unsplash.com/photo-1474487548417-781cb71495f3", "https://images.unsplash.com/photo-1580109672851-b85640868813", "https://images.unsplash.com/photo-1580046939256-c377c5b099f1", "https://images.unsplash.com/photo-1576801488695-2e4d7a14b8b5" ]; const imgComponents = images.map(url => { return <Image url={url} setShowcaseURL={setShowcaseURL} />; }); return ( <div> {imgComponents} <Showcase url={showcaseURL} /> </div> ); };
Zdecydowanie bardziej kompaktowy i jasno przekazuję co się dzieje – dla każdego adresu URL tworzymy stosowny komponent Image.
Jednak jeśli otworzymy konsolę przeglądarki zobaczymy błąd
Each child in a list should have a unique „key” prop.
React key. Prop „key”
W przypadku, gdy dynamicznie tworzymy komponenty React potrzebuje, by każdy miał dodatkowego propsa key. Jest to unikalny identyfikator komponentu. W dużym uproszczeniu – jeśli nie podamy tego komponentu i stan jednego z komponentów z listy się zmieni (w naszym wypadku imgComponents) to React wyrysuje ponownie wszystkie komponenty, bo nie będzie wiedział, w którym konkretnie coś się zmieniło. Natomiast, gdy ten props będzie obecny przerysowany zostanie jeden, konkretny komponent z listy.
Dodajmy go. Musi on być unikalny, więc najlepszym kandydatem w naszym przypadku będzie po prostu adres URL obrazu. Oczywiście nie jest to idealny kandydat, bo jeśli podamy dwa identyczne URL to i klucz będzie się powtarzał, ale zostawimy sobie ten przypadek bez rozwiązania.
const imgComponents = images.map(url => { return <Image url={url} key={url} setShowcaseURL={setShowcaseURL} />; });
Warning nie powinien się już pokazywać.
Obsługa prostych formularzy
Wszystko fajnie, komponenty tworzą się dynamiczne, ale ich zawartość zbyt dynamiczna nie jest.
Dodajmy więc opcję dodawania adresów wyświetlanych obrazów przez użytkownika strony, czyli stary, dobry input. Stworzymy go w komponencie App wraz z guzikiem do dodawania.
<div> <form> <input type="text" placeholder="URL obrazu..." value=""></input> <button onClick="">Dodaj</button> </form> {imgComponents} <Showcase url={showcaseURL} /> </div>
By React mógł na żywo renderować dodawane obrazy, ich lista musi być przechowywana jako stan. Tylko w ten sposób może on wykryć jakąkolwiek zmianę i stosownie zareagować.
Dodajemy więc kolejny hook setState tym razem z tablicą jako wartością. Przy okazji zmieniliśmy nazwę zmiennej z początkowym zestawem obrazów.
export const App = () => { const [showcaseURL, setShowcaseURL] = useState(""); const defaultImages = [ "https://images.unsplash.com/photo-1508138221679-760a23a2285b", "https://images.unsplash.com/photo-1474487548417-781cb71495f3", "https://images.unsplash.com/photo-1580109672851-b85640868813", "https://images.unsplash.com/photo-1580046939256-c377c5b099f1", "https://images.unsplash.com/photo-1576801488695-2e4d7a14b8b5" ]; const [images, setImages] = useState(defaultImages); const imgComponents = images.map(url => { return <Image url={url} key={url} setShowcaseURL={setShowcaseURL} />; }); return ( <div> <form> <input type="text" placeholder="URL obrazu..." value=""></input> <button onClick="">Dodaj</button> </form> {imgComponents} <Showcase url={showcaseURL} /> </div> ); };
W następnym kroku musimy wartość z input dodawać do tablicy.
Ponownie jednak musimy aktualną wartość input boxa trzymać jako osobny stan (const [newImageUrl, setNewImageUrl] = useState(„”)) i przypisać go jako value (<input … value={newImageUrl}/>). Natomiast do zdarzenia onChange na elemencie input musimy przypisać metodę służącą do zmiany danego stanu (<input … onChange={e => setNewImageUrl(e.target.value)}).
export const App = () => { const [showcaseURL, setShowcaseURL] = useState(""); const defaultImages = [ "https://images.unsplash.com/photo-1508138221679-760a23a2285b", "https://images.unsplash.com/photo-1474487548417-781cb71495f3", "https://images.unsplash.com/photo-1580109672851-b85640868813", "https://images.unsplash.com/photo-1580046939256-c377c5b099f1", "https://images.unsplash.com/photo-1576801488695-2e4d7a14b8b5" ]; const [images, setImages] = useState(defaultImages); const [newImageUrl, setNewImageUrl] = useState(""); const imgComponents = images.map(url => { return <Image url={url} key={url} setShowcaseURL={setShowcaseURL} />; }); return ( <div> <form> <input type="text" placeholder="URL obrazu..." value={newImageUrl} onChange={e => setNewImageUrl(e.target.value)} ></input> <button onClick="">Dodaj</button> </form> {imgComponents} <Showcase url={showcaseURL} /> </div> ); };
Kolejnym krokiem jest dodanie wprowadzonego URL do tablicy na podstawie której tworzone są komponenty Image. Powinniśmy jeszcze weryfikować czy to co wprowadził użytkownik w polu tekstowym to faktycznie URL, a nie jakieś głupoty, ale pominiemy ten krok i skupimy się na dodaniu do tablicy.
Dodawanie nowego elementu tablicy zrobimy przy wywołaniu wydarzenia onClick na guziku dodaj (teraz mamy tam pustki „”).
Po pierwsze trzeba będzie zatrzymać domyślną obługę zdarzenia i jego propagowanie.
Następnie zostanie ustawiony nowy stan za pomocą metody setImages. Zastosujemy tam destrukturyzację, czyli rozbicie tablicy na pojedyczne elementy, które to użyjemy do utworzenia nowej tablicy rozszerzonej o element newImageUrl.
export const App = () => { const [showcaseURL, setShowcaseURL] = useState(""); const defaultImages = [ "https://images.unsplash.com/photo-1508138221679-760a23a2285b", "https://images.unsplash.com/photo-1474487548417-781cb71495f3", "https://images.unsplash.com/photo-1580109672851-b85640868813", "https://images.unsplash.com/photo-1580046939256-c377c5b099f1", "https://images.unsplash.com/photo-1576801488695-2e4d7a14b8b5" ]; const [images, setImages] = useState(defaultImages); const [newImageUrl, setNewImageUrl] = useState(""); const imgComponents = images.map(url => { return <Image url={url} key={url} setShowcaseURL={setShowcaseURL} />; }); const addImage = event => { event.preventDefault(); event.stopPropagation(); setImages([...images, newImageUrl]); }; return ( <div> <form> <input type="text" placeholder="URL obrazu..." value={newImageUrl} onChange={e => setNewImageUrl(e.target.value)} ></input> <button onClick={addImage}>Dodaj</button> </form> {imgComponents} <Showcase url={showcaseURL} /> </div> ); };