Viac komponentové aplikácie
tvorba vlastných komponentov, organizácia komponentov v projekte, kompozícia komponentov, odovzdávanie údajov medzi komponentami, properties
Záznam z prednášky
Introduction
(slide) je to neuveriteľné, ale stále pracujeme na baterke.
Naša baterka svieti pomocou blesku kamery na telefóne. A pri jej zapínaní a vypínaní prehráme aj zvuk.
Dokonca vieme aj v prípade, že zariadenie baterkou nedisponuje, zobraziť správu a aplikáciu vypnúť. Dokonca to robíme už tesne po spustení aplikácie.
Pred prístupom k baterke vieme dokonca overiť aj to, či máme povolený prístup ku kamere na OS Android.
Máme ju dokonca pripravenú pre globálny trh, pretože sme vyriešili aj problematiku lokalizácie aplikácie do cudzieho jazyka.
Dnes sa pozrieme na to, ako z baterky urobíme viackomponentovú aplikáciu.
Multi Component Application
(slide) Vráťme sa teraz naspäť k definícii komponentu. V predchádzajúcich prednáškach sme komponenty v rámci React-u definovali ako
malé izolované časti na tvorbu používateľských rozhraní
Používateľské rozhranie našej aplikácie sa aktuálne skladá z dvoch takýchto častí
- z obrázka
- z tlačidla
Miesto toho, aby sme sa na ne pozerali ako na dva samostatné komponenty, sú súčasťou jedného komponentu, v ktorom sa tento obrázok spolu s tlačidlom nachádzajú. Upravme teda projekt tak, že ho rozbijeme na tri komponenty:
- obrázok ako komponent
ImageSwitch
, - tlačidlo ako komponent
ButtonSwitch
, a - samotnú aplikáciu ako komponent
App
.
- obrázok ako komponent
V štruktúre projektu v priečinku
src/
preto vytvoríme ďalší priečinok s názvomcomponents/
, do ktorého tieto komponenty umiestnime - každý do samostatného súboru.Upozornenie
Z tohto priečinku nie je dobré vytvárať modul, v ktorom by boli všetky komponenty, a teda nie je dobré v ňom vytvárať súbor
index.js
, v ktorom budeme exportovať všetky komponenty. Veľmi ľahko totiž môže dôjsť k problému Require Cycle Warnings. Vtedy napr. modulA
importuje moduleB
a ten zasa (napr. prostredníctvom ďalších modulov) zasa vyžaduje modulA
. Komponenty sú samozrejme súčasťou iných komponentov, takže porušiť tento princíp je veľmi jednoduché. Riziko rastie tým, ak sa na importovanie v každom module/komponent používa práveindex.js
súbor.Zatiaľ vytvoríme len kostru týchto komponentov extrahovaním len potrebného kódu pre každého jedného z nich. Následne z nich postupne opätovne vytvoríme celú aplikáciu.
Komponent
ImageSwitch
bude uložený v súboreImageSwitch.js
a bude vyzerať nasledovne:import React from "react"; import { Image, Pressable } from "react-native"; export function ImageSwitch() { const image = require("../assets/bulb_off.png"); return ( <Pressable onPress={null}> <Image source={image} /> </Pressable> ; ) }
Komponent
ButtonSwitch
bude uložený v súboreButtonSwitch.js
a bude vyzerať nasledovne:import { useTranslation } from "react-i18next"; import React from "react"; import { Button } from "react-native"; export function ButtonSwitch() { const { t } = useTranslation(); return ( <Button ={null} onPress={t("Turn On")} title/> ; ) }
Komponent
App
bude uložený v súboreApp.js
a bude vyzerať nasledovne:import React, { useState } from "react"; import { StyleSheet, View } from "react-native"; import { ButtonSwitch } from "./ButtonSwitch"; import { ImageSwitch } from "./ImageSwitch"; import "../i18n"; export function App() { const [isOn, setIsOn] = useState(false); return ( <View style={styles.container}> <ImageSwitch></ImageSwitch> <ButtonSwitch></ButtonSwitch> </View> ; ) }
Po spustení sa síce aplikácia spustí a bude aj vyzerať ako predtým, ale tentokrát nebude fungovať. Ak totiž ťapneme na jeden alebo druhý komponent, nedôjde k žiadnej zmene v aplikácii - ani obrázok a ani tlačidlo sa nijak nezmenia. Rovnako tak sa nerozsvieti ani blesk.
Vyzerá to teda tak, že aplikácia prestala pracovať. Jej znefunkčnenie je spôsobené tým, že stav aplikácie, ktorý sa menil po ťapnutí či už na obrázok alebo tlačidlo, zostal izolovaný len v komponente
App
.Ako teda zabezpečiť, aby bol stav zdieľaný naprieč všetkými komponentmi, z ktorých sa aplikácia skladá?
Share Data Between the Components
(slide) Problém, s ktorým sa stretávame, je bežný problém, s ktorým sa dá stretnúť v prípade viackomponentových aplikácií. Týmto problémom je zdieľanie údajov medzi komponentmi.
A nevyhli sme sa mu ani v našom prípade. A to aj napriek tomu, že aplikácia, ktorú vytvárame, má len tri komponenty. Stav v našej aplikácie hovorí o tom, či je baterka zasvietená alebo zhasnutá. A tento stav môžeme zmeniť kliknutím buď na obrázok so žiarovkou alebo na tlačidlo pod ním. A so zmenou stavu dôjde aj k zmene vzhľadu či už tlačidla a textu na ňom alebo obrázku reprezentujúcom stav baterky.
Komponent je malá izolovaná časť na tvorbu používateľských rozhraní. My sme vytvorili tri takéto izolované časti. Ako teda zabezpečíme to, že po kliknutí na tlačidlo sa o tom dozvie aj obrázok, keď aj jeden aj druhý komponent sú navzájom izolované? Čo znamená, že aj jeden aj druhý disponuje vlastným stavom komponentu?
(slides) Existuje viacero mechanizmov, ktoré je možné použiť na vyriešenie tohto problému. Každý je však špecifický a záleží od toho, medzi ktorými komponentmi v hierarchickej štruktúre komponentov chceme údaje zdieľať. Preto je potrebné rozlišovať aj to, ktorým smerom budú údaje medzi komponentmi zdieľané:
- od komponentu rodiča ku potomkovi (z angl. parent to child component),
- od komponentu potomka ku rodičovi (z angl. child to parent component),
- medzi súrodencami (z angl. between siblings), alebo
- medzi ľubovoľnými komponentmi (z angl. sharing data between not related components)
Na jednotlivé mechanizmy pre jednotlivé typy zdieľania údajov sa pozrieme bližšie.
Poznámka
Jednotlivé mechanizmy sa môžu líšiť aj v závislosti od toho, či sú komponenty reprezentované triedami alebo funkciami. V našom prípade sa pozrieme na možnosti zdieľania údajov medzi komponentmi, ktoré sú reprezentované funkciami. V prípade záujmu o existujúce mechanizmy, ktoré je možné použiť pre komponenty reprezentované ako triedy, sa pozrite napr. na článok 8 no-Flux strategies for React component communication.
Parent to Child Component
(slide) Toto je veľmi častý prípad zdieľania údajov medzi komponentmi. Každý (rodičovský) komponent sa totiž môže odkazovať na iný komponent (potomka) pri svojom renderovaní a v tomto momente im môže predať potrebné informácie.
Properties
(slide) Predať údaje od rodiča svojmu potomkovi je možné zabezpečiť pomocou vlastností (z angl. properties alebo skrátene props). Toto predanie sa deje vo forme argumentu funkcie
props
, ktorá reprezentuje komponent (slide):function Hello(props){ return ( <Text>Hello {props.name} {props.surname}!</Text> ; ) }
(slide) Rodičovský komponent odovzdá údaje do komponentu
Hello
pri jeho renderovaní napríklad takto:return ( <View style={styles.container}> <Hello name="Sherlock" surname="Holmes"></Hello> </View> ; )
Vlastnosti
props
sú objektom, čo si môžeme jednoducho overiť tým, že ich vypíšeme do log-u:console.log(props);
Vlastnosti sa obecne používajú na prispôsobenie, resp. úpravu komponentu v dobe jeho vytvárania pomocou rozličných parametrov (z angl. properties). Väčšina komponentov rámca React Native obsahuje vlastnosti, pomocou ktorých je možné nastaviť ich vlastnosti v čase vytvárania. Napr. komponent
Image
obsahuje vlastnosťsrc
, pomocou ktorej máme možnosť špecifikovať obrázok, ktorý sa má zobraziť.(slide) Vlastnosti sú určené len na čítanie! Komponent nesmie modifikovať hodnoty vlastností! Vlastnosti sú nemenné (z angl. immutable)!
Pure Function
(slide) Takéto funkcie, ktoré nemenia svoje vstupy a vždy vrátia rovnaký výstup pri rovnakom vstupe, sa nazývajú “pure” funkcie. Dokonca samotný React, aj napriek tomu, že je pomerne flexibilný, má jedno veľmi striktné pravidlo:
All React components must act like pure functions with respect to their props.
Torch Update
V našom prípade sa jedná o vzťah rodiča, ktorým je komponent
App
a jeho potomkov, ktorými súImageSwitch
aButtonSwitch
. Údaj, ktorý je potrebné, aby každý potomok dostal, je počiatočný stav komponentu:- aby komponent
ImageSwitch
vedel, aký obrázok má zobraziť, a - aby komponent
ButtonSwitch
vedel, aký popisok má na tlačidle zobraziť.
- aby komponent
Upravíme teda renderovanie komponentu
App
, kde každému potomkovi pošleme stav komponentu pomocou vlastnostiisOn
:return ( <View style={styles.container}> <ImageSwitch isOn={isOn}></ImageSwitch> <ButtonSwitch isOn={isOn}></ButtonSwitch> </View> ; )
Na základe tejto vlastnosti upravíme komponent
ButtonSwitch
, kde na základe jej hodnoty aktualizujeme popisok tlačidla:export function ButtonSwitch(props) { const { t } = useTranslation(); return ( <Button ={null} onPress={props.isOn === true ? titlet("Turn Off") : t("Turn On")} /> ; ) }
Podobne na základe hodnoty tejto vlastnosti zvolíme správny obrázok v komponente
ImageSwitch
:export function ImageSwitch(props) { const { t } = useTranslation(); const image = props.isOn ? require("../assets/bulb_on.png") : require("../assets/bulb_off.png"); return ( <Pressable onPress={null}> <Image source={image} /> </Pressable> ; ) }
Po spustení aplikácie síce nevidíme žiadnu mimoriadnu zmenu, ale môžeme si byť istý, že aktuálne sa jej vzhľad odvíja od vlastnosti
isOn
. Zmenou jej počiatočnej hodnoty sa o tom môžeme presvedčiť.Problém odovzdávania údajov od rodiča smerom k potomkovi máme vyriešený. Ako však zabezpečiť to, aby potomok dokázal informovať o vzniknutej zmene svojho rodiča? V prípade aplikácie Torch to potrebujeme zabezpečiť vždy vtedy, keď dôjde ku stlačeniu tlačidla alebo ťapnutiu na obrázok.
Child to Parent Component
(slide) Pozrieme sa teda na druhý veľmi častý prípad odovzdávania údajov medzi komponentmi, keď potomok potrebuje informovať svojho rodiča.
Najjednoduchší spôsob, ako môže potomok kontaktovať rodiča, je pomocou tzv. callback funkcie, ktorá sa nachádza u rodiča. Túto funkciu rodič odovzdá potomkovi ako vlastnosť (z angl. property). Potomok ju následne bude volať vždy vtedy, keď dôjde u neho k zmene stavu a bude o tom potrebovať informovať svojho rodiča.
Torch Update
V našom prípade vytvoríme v komponente
App
funkciuonStateChange()
, ktorá po zavolaní zmení jeho stav na opačný:function onStateChange() { setIsOn(!isOn); }
Referenciu na túto funkciu následne odovzdáme vo forme vlastnosti
onPress
ostatným komponentom pri renderovaní komponentuApp
:return ( <View style={styles.container}> <ImageSwitch ={isOn} isOn={onStateChange}> onPress</ImageSwitch> <ButtonSwitch ={isOn} isOn={onStateChange}> onPress</ButtonSwitch> </View> ; )
Následne upravíme renderovanie tlačidla, kde hodnotu vlastnosti
props.onPress
priradíme ku vlastnostionPress
komponentu tlačidlaButton
:return ( <Button ={props.onPress} onPress={props.isOn === true ? titlet("Turn Off") : t("Turn On")} /> ; )
Podobne upravíme aj renderovanie obrázku, kde podobne nastavíme vlastnosť
onPress
komponentuPressable
:return ( <Pressable onPress={props.onPress}> <Image source={image} /> </Pressable> ; )
Ak spustíme aplikáciu teraz, “automagicky” bude všetko fungovať tak, ako má. Na začiatku bude rodič propagovať stav aplikácie všetkým svojim potomkom pomocou vlastností (props). Následne budú potomkovia v prípade vzniknutej udalosti propagovať aktuálny stav svojmu rodičovi pomocou callback funkcie.
Netreba však zabudnúť na to, že keď potomok iniciuje zmenu stavu, rodič ju následne distribuuje medzi všetkých svojich potomkov. To je dané vlastnosťami rámca React, kedy pri zmene stavu komponentu sa jeho nová hodnota automaticky propaguje všetkým ďalším potomkom, ktorí ho nejakým spôsobom zdieľajú.
Tým sme vlastne náš problém vyriešili - z jedno komponentovej aplikácie sme vytvorili viac komponentovú aplikáciu, v ktorej sme zabezpečili zdieľanie údajov medzi jednotlivými komponentmi. Aktuálne stačí len do komponentu
App
vložiť kontrolu prístupových práv ku fotoaparátu a kontrolu existencie fotoaparátu ako takého.export function App() { const [isOn, setIsOn] = useState(false); const { t } = useTranslation(); useEffect(function () { .switchState(isOn).catch(function (e) { Torch.alert( Alertt("Missing Flashlight"), t( "No camera available. Go and buy a " + "device with some (or two) and come " + "back later." , ) [ {text: t("Quit"), onPress: function () { .exitApp(); RNExitApp, }, } ]; ); }), []); } async function onStateChange() { let cameraAllowed = true; if (Platform.OS === "android") { const granted = await PermissionsAndroid.request( .PERMISSIONS.CAMERA, PermissionsAndroid {title: t("Torch Needs Camera Permission"), message: t( "Torch app uses camera flashlight " + "as torch. To make Torch work, you " + "need to allow Camera Permission." , )buttonNeutral: t("Ask Me Later"), buttonNegative: t("Cancel"), buttonPositive: t("OK"), }; ) = cameraAllowed === PermissionsAndroid.RESULTS.GRANTED; granted } if (cameraAllowed) { await Torch.switchState(!isOn); setIsOn(!isOn); } } return ( <View style={styles.container}> <ImageSwitch ={isOn} isOn={onStateChange}> onPress</ImageSwitch> <ButtonSwitch ={isOn} isOn={onStateChange}> onPress</ButtonSwitch> </View> ; ) }
Sharing Data Between Siblings
(slide) Ďalší prípad zdieľania údajov je medzi súrodencami. Komponenty sú súrodenci, ak majú spoločného predka.
Toto je vlastne aj náš prípad, kedy komponenty
ButtonSwitch
aImageSwitch
majú spoločného predkaApp
.Odporúčanie je v tomto prípade na komunikáciu medzi súrodencami použiť kombináciu predchádzajúcich stratégií, ktoré zahŕňajú spoločného predka. A ako sme mohli vidieť, pomáha v tomto prípade aj samotný rámec, ktorý zmeny stavu automaticky propaguje dotknutým komponentom.
Sharing data between not related components
(slide) Posledný prípad zdieľania údajov je medzi ľubovoľnými dvoma komponentmi aplikácie.
Tento prípad už nie je taký triviálny, ako tomu bolo doteraz, pretože doteraz sme videli, že React údaje distribuuje prostredníctvom komponentov. Rýchlosť doručenia teda silne závisí od vzdialenosti medzi komunikujúcimi komponentmi.
(slides) React totiž nemá vlastné riešenie, ktoré zabezpečuje takúto nepriamu komunikáciu. React podporuje len jednosmernú komunikáciu (z angl. unidirectional data flow) v smere od rodiča ku potomkovi. Takže doručiť údaje medzi dvoma uzlami, ktoré nie sú rodičmi, je problém.
(slide) Aj napriek tomu existuje niekoľko prístupov, ktoré je možné použiť na tento typ komunikácie medzi komponentmi:
- globálne premenné,
- použiť
Context
, alebo použiť - návrhový vzor pozorovateľ (z angl. observer)
Global Variables
Globálne premenné nie je dobré používať, ale pre rýchle prototypovanie sa občas môžu hodiť.
V tomto prípade môžete na globálne premenné použiť objekt
window
.
Context
Context
funguje podobne ako vlastnosti (props), ale s tým rozdielom, že neumožňuje poslať údaje len jednému potomkovi, ale naraz celej vetve stromu (resp. podstromu). Údaje je možné poslať len smerom od rodiča k potomkom v danej vetve.Context
sa používa v prípadoch, keď je potrebné zdieľať údaje spoločné pre komponenty danej vetvy stromu. To môžu byť údaje ako napr. prihlásený používateľ, téma prostredia, zvolený jazyk a pod.V samotnej dokumentácii autori upozorňujú na opatrné používanie
Context
-u, pretože jeho nadmerným resp. nesprávnym používaním sa znižuje znovupoužiteľnosť komponentov.
Observer Pattern
Pomocou tohto návrhového vzoru je možné kontaktovať jedným komponentom ľubovoľný počet iných komponentov. Dôležité je, aby sa tieto komponenty prihlásili, že chcú byť v prípade potreby (napr. vzniku udalosti) kontaktované, resp. notifikované. Tieto komponenty sa na odber obyčajne prihlásia v čase pripojenia (z angl. mounting) komponentu a odhlásia sa pri odpojení (z angl. unmounting), resp. odstránení komponentu. To je možné zabezpečiť pomocou hook-u
useEffect()
.(slide) Implementáciu je možné vytvoriť vlastnú, ale samozrejme existujú aj hotové knižnice, ako napr.:
- EventEmitter - EventEmitter is an implementation of the Event-based architecture in JavaScript.
- Flux - Application architecture for building user interfaces
- MobX - MobX is a battle tested library that makes state management simple and scalable by transparently applying functional reactive programming (TFRP)
- Redux - A Predictable State Container for JS Apps
Conclusion
Dnes sme úspešne rozbili náš komponent a vytvorili sme viac komponentovú aplikáciu. Ukázali sme si problém, ktorý takýto prístup tvorby komponentov so sebou prináša. Tým problémom je zdieľanie údajov medzi komponentmi. Ukázali sme si štyri prípady zdieľania údajov medzi komponentmi a mechanizmy, ktorými je možné toto zdieľanie zabezpečiť.
Tvorba vlastných komponentov má svoje výhody pri väčších aplikáciách a pri opakujúcich sa komponentoch. V našom prípade sa stále jedná o pomerne jednoduchú aplikáciu. Ak si však spomeniete na aplikáciu Dialer, ktorú ste vytvárali na cvičeniach, využitie komponentov v jej prípade je omnoho užitočnejšie.
Nabudúce sa pozrieme na Redux.