W dzisiejszym odcinku kursu chciałbym, abyśmy napisali pierwszy program w ReactJS, który to miałby zamieniać skalę Celsjusza na Farhenheita. Nasz, dość prosty program, powinien posiadać interfejs, przypominający ten na załączonym obrazku:
Składa się z on dwóch linii, pierwsza zawiera etykietę i formatkę tekstową do podania temperatury. Druga linia to natomiast komunikat z rezultatem przeprowadzonych obliczeń. Od czego zaczynamy?
Przede wszystkim, musimy przeanalizować nasze potrzeby i wybrać komponenty, w naszym prymitywnym przypadku możemy dostrzec dwa komponenty – górny z formatką, a także dolny z odpowiedzią. Ponadto całość (oba komponenty) można uznać jako jeden komponent realizujący funkcjonalność zamiany temperatury. Rzem daje nam to liczbę trzech komponentów.
Pierwszy etap mamy za sobą – udało nam się zidentyfikować wymagane komponenty, aby zrealizować cały działający program musimy:
- dowiedzieć się jak tworzyć komponenty (po części już to przerobiliśmy na poprzednim kursie)
- dowiedzieć się jak możemy przesyłać dane pomiędzy komponentami – musimy przecież jakoś przesłać to nasze 36.6 do linii niżej
- dowiedzieć się jak i gdzie dokonać obliczeń i jak zareagować na czynność użytkownika polegającą na wpisaniu nowej wartości stopni w skali Celsjusza
Są to trzy najpoważniejsze problemy, reszta naszych potrzeb wyjdzie w trakcie.
W kolejnym kroku, tworzymy strukturę plików i folderów naszej aplikacji:
Porównując powyższą strukturę plików z wygenerowanymi domyślnie plikami, możemy wyróżnić:
- Komponent Temperature z plikiem temperature.js czyli nasz główny komponent programu, komponent ten posiada zagnieżdżone dwa inne:
- Komponent Input – z plikiem input.js – tu będziemy obslugiwać nasz formularz
- Komponent Result z plikiem result.js – tu będziemy
Jak więc widzimy, nasz program będzie składał się z trzech komponentów, czyli trzech plików JS.
To teraz chwila teorii. React w swojej budowie pozwala na tworzenie komponentów w dwojaki sposób:
- Pierwszy z nich poznaliśmy już podczas analizy programu z pierwszego kursu, jest to stworzenie klasy, która będzie rozszerzała klasę Component, np:
123class Input extends Component {return ( <div></div> );} - Drugi sposób to utworzenie funkcji, czyli stworzenie tzw. komponentów funkcjonalnych np:
1 2 3 4 5 |
const komponent = () => { return ( <div> </div> ) } |
Warto zauważyć, iż w tym miejscu po raz kolejny wykorzystujemy dobrodziejstwo ECMA Script – czy to tworząc klasę, czy to jak w drugim wypadku używając funkcji strzałkowych (czyli rozszerzenia składni:
1 2 3 |
var funkcja = function() { return true; } |
na
1 |
const funkcja = () => { return true; } |
(zyskując przy tym pewne dodatkowe korzyści, ale to temat na osobny artykuł o ECMA Script 😉 )
Jakie więc są różnice, kiedy stosować komponenty klasowe a kiedy funkcjonalne?
- Komponenty klasowe odróżnia przede wszystkim fakt, że mają stan, oznacza to, że możemy przypisać mu pewne cechy i ów obiekt te cechy utrzyma, a służyć temu ma obiekt bezpośrednio przypisany klasie będącej komponentem, a nazywający się state. State, należący do konkretnego komponenetu może być w dowolnym momencie inicjalizowany, modyfikowany i aktualizowany. Co więcej, nie może bezpośrednio wykraczać poza komponent, aczkolwiek często zdarza się, że elementy stanu komponentu są przekazywane do komponentów potomych, co czyni ich wtedy elementami noszącymi nazwę props, o czym więcej poniżej.
- Komponenty funkcjonalne nie mają stanu, są zwykłymi funkcjami JavaScript i mogą przyjmować argumenty, które stają się elementami typu props.
Reasumując – props, słuzą do komunikacji pomiędzy komponentami, mogą być przekazywane do komponentów potomnych. Pamietamy też o tym, iż komponent potomny nie może w bezpośredni sposób modyfikować props, one służą jedynie do odczytu. Wszystkie dotychczasowe rozważania teoretyczne moża podsumować:
- state przechowuje stan komponentu, który to może być swobodnie przekazywany do potomonych komponentów stając się props.
- Props to argumenty, które są przekazywane do komponentu, które to mogą być wyświetlone czy wykorzystane do dalszych obliczeń.
Mając już wiedzę na temat rodzajów komponenetów i sposobów ich komunikacji spróbujmy napisać pierwszy element naszego systemu – komponent Result:
1 2 3 4 5 6 7 8 9 |
import React, { Component } from 'react'; const result = (props) => { return ( <div> <p>Temperatura w Fahrenheitach wynosi {props.temp}</p> </div> ) } export default result; |
Plik ten rozpoczyna jeden import Reacta, po czym uwidacznia się stworzenie stałej o nazwie result, do której przypisana zostaje funkcja z argumentem w postaci props. Funkcja ta zwraca kod JSX, w który wpleciony jest znacznik <div> a w nim to znajduje się znacznik paragrafu, na którego końcu widzimy odwołanie do naszego obiektu props, a w szczególności do jego pola temp. Widzimy, że w celu odwołania się do zmiennej użyliśmy nawiasów klamrowych.
Ostatnia linia to oczywiście eksport naszej funkcji, w celu jej użycia w innym komponencie.
Zajrzyjmy teraz do kolejnego komponentu o nazwie Input:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import React, { Component } from 'react'; class Input extends Component { render() { return ( <div> Podaj temperature w Celsjuszach: <input type="text" onChange={this.props.changeTemp}/> </div> ); } } export default Input; |
W tym wypadku postanowiłem użyć komponentu opartego na klasie. Komponent ten nie wyróźnia się niczym szczególnym – tworzy etykietę a obok niej formatkę typu input. Co warto zauważyć, formatka ta posiada atrybut onChange. Jak sama nazwa wskazuje, event zmiany wartości formatki input ma spowodować uruchomienie funkcji przekazanej w props a noszącej nazwę changeTemp. Mamy już więc opisane dwa komponenty z trzech, a jak wygląda trzeci z nich, łączący wszystko w całość?
Temperature.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import React, { Component } from 'react'; import Input from './Input/Input.js'; import Result from './Result/Result.js'; class Temperature extends Component { state = { "temperature":"" } tempUpdateHandler = (event) => { let calc = event.target.value * 9/5 + 32; this.setState({ "temperature":calc }); } render() { return (<div> <Input changeTemp={this.tempUpdateHandler} ></Input> <Result temp={this.state.temperature} ></Result> </div> ); } } export default Temperature; |
Plik ten jest minimalne bardziej rozbudowany. Kolejno widzimy w nim:
- Import dwóch komponentów, które przed chwilą stworzyliśmy
- Deklarację klasy Temperature – tworzenie komponentu
- w samym kodzie klasy widzimy deklarację zmiennej o nazwie state. I to jest właśnie nasz obiekt przechowywujący stan komponentu. W tej chwili jedyne czego potrzebujemy, to zapisanie informacji o temperaturze. tak też robimy, przy czym nie podajemy wartości domyślnej (“temperature”:””)
- Odchodząc trochę od analizy kodu linijka po linijce przejdźmy teraz do funkcji render. Zauważmy, iż zwraca ona kod JSX, który zawiera dwa znaczniki Input i Result, które tak naprawdę są odwołaniami do komponentów, które niedawno stworzyliśmy. Gdy przyjrzymy się komponentowi Input to łatwo zauważymy atrybut changeTemp. Gdzieś ta nazwa już została przez nas użyta… a mianowicie w komponencie Input! Gdy wrócimy do pliku input.js łatwo zauważyć, iż przy zmianie (onChange) wartości formatki tekstowej ma zostać wywołana funkcja changeTemp z komponentu wyżej. W Temperature.js w znaczniku input umieszczamy ów atrybut o nazwie changeTemp i deklarujemy jaką funkcję chcemy użyć do obsługi tego zdarzenia. Tą funkcją jest tempUpdateHandler.
Drugi komponent – Result – posiada atrybut o nazwie temp. Do tego atrybutu przypisana jest aktualna, najświeższa wartość zmiennej temperature pobranej z obiektu state, czyli stanu naszego komponentu Temperature. Jak ponownie zajrzymy do pliku Result.js to łatwo zauważymy, iż atrybut ten jest wykorzystywany jako element obiektu props. Odwołujemy się do niego przez props.temp (czyli tak jak nazwa atrybutu z komponentu wyżej).
Powyższy podpunkt okazuje się niezwykle ważny. Tłumaczy on, jak wykonać komunikację pomiędzy dwoma komponentami (idąc w-dół posługujemy się props, modyfikując komponent wyższy w hierarchii za pomocą komponentu zagnieżdżonego przekazujemy odwołanie do funkcji, która ma ten komponent zmodyfikować – co jest bardzo logiczne, skoro sami nie mamy prawa bezpośrednio dokonywać zmian na innym komponencie to użyjmy funkcji tego komponentu). - Pozostaje nam już tylko opisanie funkcji o nazwie tempUpdateHandler. Do czego ona służy? Jak sama nazwa wskazuje, wywoływana jest w momencie gdy nastąpi zmiana wartości temperatury początkowej (zmienią się dane początkowe). Funkcja ta za argument przyjmuje obiekt event. Z jego pomocą i instrukcji event.target możemy dotrzec do znacznika, który wywołał zmianę. W naszym wypadku będzie to oczywiście formatka input. W ostatniej fazie za pomocą pola value pobieramy jej nową wartość i na jej podstawie dokonujemy obliczeń. Na koniec za pomocą instrukcji setState ustawiamy nowe wartości. W tym miejscu warto zaznaczyć kolejną niezwykle ważną kwestię:
Aktualizacja stanu komponentu (obiektu state), powoduje aktualizację drzewa DOM, czyli w rezultacie aktualizację interfejsu użytkownika o nowe dane.
Dla nas ma to przede wszystkim takie znacznie, iż React na nowo przeanalizuje komponenty, w tym Result i naniesie nową wartość temperatury z obiektu state.
Ostatnia wymagana czynność to plik index.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import React, { Component } from 'react'; import Temperature from './Temperature/Temperature.js'; class App extends Component { render() { return (<div> <Temperature/> </div> ); } } export default App; |
czyli:
- dodanie komponentu Temperature
- .. a następnie wywołanie go w metodzie render.
Sprawdzając przeglądarkę, powinniśmy otrzymać gotowy rezultat naszej pracy.
To był duży odcinek. Z drugiej strony udało nam się przedstawić wiele zagadnień Reacta – wywoływanie komponentów, tworzenie komponentów, komunikację. Zachęcam do komentowania i już zapraszam do kolejnych wpisów!