Component Composition
štruktúra projektu, kompozícia komponentov, odovzdávanie údajov medzi komponentami, properties, Context
Záznam z prednášky
Tips
.gitignore
pre React Native projektyaktuálne posielate do git-u množstvo vecí, ktoré tam nemajú čo robiť
priamo od facebook-u na Github-e
Pri vytváraní projektu ale dôjde k vytvoreniu aj priečinku
.git
a aj súboru.gitignore
. Ak zmažete priečinok.git
, spustite ešte tento príkaz:$ git rm --cached priecinok.s.projektorm -f
Introduction
(slide) je to neuveriteľné, ale stále pracujeme na baterke
baterka svieti
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 sa nestratiť v projekte jeho vhodným štruktúrovaním a urobíme z baterky viackomponentovú aplikáciu.
Project Structure
(slide) React a React Native sú rámce, ktoré nám nijakým spôsobom nepredpisujú štruktúru projektu alebo aplikácie. Aktuálne je náš projekt jednoduchý, ale s novými funkciami, obrazovkami, komponentmi, prekladmi sa počet súborov, z ktorých sa skladá, rapídne zväčší. Ako teda organizovať súbory tak, aby sme sa dokázali v projekte jednoducho orientovať aj napriek veľkému počtu súborov?
Existuje niekoľko spôsobov, ako organizovať štruktúru projektu. Spoločné majú hlavne logické zoskupovanie súborov podľa ich typu alebo funkcionality (tzv. type vs feature). Ukážka jedného z týchto prístupov sa nachádza napr. v projekte React Native Easy Starter.
Nebudeme rozoberať a upravovať štruktúru nášho projektu pre veci, ktoré nemáme, takže urobíme niečo, čo majú všetky štruktúry spoločné: súbory, ktoré tvoria náš projekt a nie sú automaticky vygenerované nástrojmi rámca, umiestnime do osobitného priečinku s názvom
src/
.Poznámka
V Expo projekte sa dá stretnúť s názvom tohto priečinku
app/
.Vytvoríme teda priečinok
src/
a presunieme do neho všetko, čo sme vytvorili včetne priečinkuassets/
:src/ ├── App.js ├── assets/ │ ├── bulb_off.png │ ├── bulb_on.png │ └── torch-icon.png ├── i18n.js └── locales/ ├── index.js └── sk.json
Prostredie Visual Studio Code urobí potrebné úpravy v súboroch projektu za nás, takže projekt by mal fungovať bez prípadných zásahov. Môžeme však z priečinku
src/
vytvoriť rovno modul pridaním súboruindex.js
, v ktorom exportujeme komponentApp
zo súboruApp.js
:import App from './App'; export default App;
Pre istotu môžeme overiť ešte súbor
index.js
v koreňovom priečinku projektu a v prípade potreby upraviť import:import App from './src';
A okrem toho aj odkazy na ikony v súbore
app.json
.Upozornenie
Pravdepodobne bude potrebné zmazať aj výsledné zostavenie Android projektu v priečinku
android/
:$ cd android $ ./gradlew clean $ cd ..
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 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 a 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í by sa aplikácia mala spustiť, ale tentokrát po ťapnutí na jeden alebo druhý komponent nedôjde k žiadnej zmene. To preto, že stav zostal izolovaný len v komponente
App
.
Share Data Between the Components
(slide) Ešte predtým, ako sa však vrhneme do samotnej implementácie, sa pozrieme na problém, ktorému sa vo viac komponentových aplikáciách rozhodne nevyhneme. Tým problémom je zdieľanie údajov medzi komponentmi.
A nevyhneme sa mu ani v našom prípade aj napriek tomu, že vytvoríme len tri komponenty. Identifikovali sme stav našej aplikácie, ktorý 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 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 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ť, ktorým smerom chceme údaje zdieľať:
- 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:function Hello(props){ return ( <Text>Hello {props.name} {props.surname}!</Text> ; ) }
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ť.Takže problém odovzdávania údajov od rodiča smerom k potomkovi máme vyriešený. Ako však teraz zabezpečiť to, aby potomok informoval o zmene svojho rodiča? A to v našom prípade hlavne 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ú rodič odovzdá potomkovi ako vlastnosť (z angl. property). Táto funkcia sa pritom bude nachádzať u rodiča.
Torch Update
V našom prípade teda v komponente
App
vytvoríme 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á. 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ú. Napr. vo forme vlastnosti.
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()
.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ť.
Nabudúce sa pozrieme na Redux.