Components and State
komponenty, stav a vlastnosti komponentov, základné komponenty
Záznam z prednášky
Project Torch Overview
(slide) Na minulej prednáške sme si predstavili rámec na vývoj natívnych mobilných aplikácií React Native a začali sme vyvíjať aplikáciu Torch. Dnes budeme vo vývoji tejto aplikácie pokračovať a popri tom sa budeme zoznamovať s vlastnosťami tohto rámca.
(slide) Táto aplikácia bude jednoduchou baterkou na svietenie, kde na svetlo využijeme blesk zadnej kamery/fotoaparátu.
Expo SDK 39 is Now Available
(slide) Keďže sme toho naposledy veľa nestihli a medzičasom bola vydaná nová verzia Expa s podporou SDK 39, ktorá podporuje React Native 0.63, začneme opäť rýchlym vytvorením aplikácie Torch pomocou nástroja
expo-cli
. Aktualizácia je potrebná, pretože v našom projekte budeme používať komponenty, ktoré v SDK 38 ešte nie sú podporované.Projekt teda vytvoríme znova:
$ expo-cli init torch # select blank $ cd torch $ code . $ npm start
React Native vs React
(slide) Ak chceme zvládnuť prácu v rámci React Native, nemôžeme zabudnúť na rámec React, na ktorom je React Native postavený, a na ktorom je závislý. Obecne môžeme povedať, že rámec React Native sa používa na tvorbu (nie len) mobilných aplikácií pomocou React-u a schopností/možností natívnej platformy. Túto závislosť si môžete všimnúť aj v súbore
App.js
, kde sa React importuje riadkom:import React from 'react';
Pre zvládnutie rámca React Native je teda dobré poznať aspoň základy rámca React.
(slide) Takže - čo je React? React je knižnica pre JavaScript na tvorbu používateľských rozhraní. Tie sa vytvárajú pomocou malých izolovaných častí, ktoré sa nazývajú komponenty.
Komponenty je možné navzájom prepájať, čím je možné vytvárať komplexné používateľské rozhrania. Toto prepojenie je možné vizualizovať pomocou stromu, v ktorom existuje jeden koreňový komponent a každý ďalší komponent sa stáva jeho samostatnou vetvou. Každá ďalšia vetva môže mať samozrejme ďalšie podvetvy. Príklad takéhoto stromu komponentov (z angl. component tree) sa nachádza na nasledujúcom obrázku:
Components
Komponenty sú základných stavebným prvkom týchto rámcov. Pozrime sa teda zblízka na to, čo je vlastne komponent.
V našej aplikácii máme zatiaľ jediný komponent, ktorý je súčasne aj koreňovým komponentom aplikácie. Tento komponent sa nachádza v súbore
App.js
a vyzerá takto:export default function App() { return ( <View style={styles.container}> <Text>Open up App.js to start working on your app!</Text> <StatusBar style="auto" /> </View> ; ) }
Komponent je teda definovaný ako funkcia. Čokoľvek, čo táto funkcia vráti, je vykreslené ako React element, ktorý definuje, čo bude viditeľné na obrazovke zariadenia. Definujeme vlastne pohľad (view) komponentu.
Obecne je každý komponent definovaný svojim stavom a vlastnosťami. O nich si však porozprávame trochu neskôr.
Views
Pri vývoji aplikácií pre Android alebo iOS sú pohľady (views) záladným stavebným blokom pre tvorbu používateľského rozhrania. Obecne sa jedná o elementy obdĺžnikového tvaru, pomocou ktorých vieme zobrazovať text, obrázky, vedia reagovať na vstup od používateľa.
Pohľad (view) je vlastne všetko, čo je možné zobraziť na obrazovke zariadenia. Niektoré pohľady môžu obsahovať aj iné pohľady.
(slide) Rámec React Native obsahuje generické (core) komponenty (resp. pohľady), pomocou ktorých je možné vyskladať ľubovoľné používateľské rozhranie pre všetky podporované platformy. Následne pri spustení rovnakého kódu na konkrétnej platforme sú tieto pohľady nahradené natívnymi komponentami (resp. pohľadmi). Túto situáciu ilustruje nasledujúci obrázok.
(slide) React Native má mnoho Core komponentov. Ich kompletný zoznam nájdete v dokumentácii API. Najčastejšie však budete pracovať s komponentami, ktoré sú uvedené v nasledujúcej tabuľke.
Prehľad najčastejších komponentov React Native [@y1] React Native UI Component Android View iOS View Web Analog Description <View>
<ViewGroup>
<UIView>
A non-scrollling <div>
A container that supports layout with flexbox, style, some touch handling, and accessibility controls <Text>
<TextView>
<UITextView>
<p>
Displays, styles, and nests strings of text and even handles touch events <Image>
<ImageView>
<UIImageView>
<img>
Displays different types of images <ScrollView>
<ScrollView>
<UIScrollView>
<div>
A generic scrolling container that can contain multiple components and views <TextInput>
<EditText>
<UITextField>
<input type="text">
Allows the user to enter text
JSX
(slide) Keď sa však pozrieme bližšie na to, čo funkcia v skutočnosti vracia, nie je to ani reťazec a nie je to ani HTML. React totiž používa syntax JSX (JavaScript XML), čo je XML/HTML rozšírenie pre kód jazyka JavaScript. React nie je na technológii JSX závislý, ale jeho použitím je tvorba komponentov jednoduchšia, prehľadnejšia a hlavne prirodzenejšia. Za syntaxou JSX stojí rovnako spoločnosť Facebook.
Príklad jednoduchého
Hello, world!
komponentu môže vyzerať takto:function HelloWorld() { return <Text>Hello, world!</Text>; }
Aby React (a vlastne JavaScript) rozumel JSX, je potrebné použiť transpiler Babel, ktorý dokáže preložiť tento značkovací jazyk do jazyka JavaScript. Ten okrem toho pozná aj všetky novinky, ktoré priniesol ES6 (ECMAScript 2015).
Button Component
Do UI našej aplikácie potrebujeme pridať tlačidlo, pomocou ktorého budeme baterku ovládať. Na to použijeme komponent
Button
, ktorý sa podľa potreby vyrenderuje na každej platforme.Komponent bude vyzerať nasledovne:
<Button onPress={onPressButton} title="Turn On" />
Ako je možné vidieť, komponent tlačidla má dva povinné atribúty:
title
- text, ktorý sa na tlačidle zobrazíonPress
- funkcia, ktorá bude zavolaná po stlačení tlačidla.
Z komponentu
App
môžeme vypustiť komponent<StatusBar>
a pridáme do neho komponent<Button>
:export default function App() { return ( <View style={styles.container}> <Text> .js to start working on your app! Open up App</Text> <Button ={function () { onPressconsole.log("Button was pressed"); }}="Turn On" title/> </View> ; ) }
Ak tlačidlo stlačíme, v konzole Metro Bundler-a alebo v nástroji Visual Studio Code uvidíme vypísaný text.
Component State
Stlačením tlačidla chceme, aby sa baterka
- rozsvietila, ak je zhasnutá, alebo
- zhasla, ak je rozsvietená.
Našu aplikáciu by sme teda mohli nazvať aj stavovým strojom, pretože v ktoromkoľvek momente vieme povedať, že sa bude nachádzať v jednom dvoch stavov:
- zhasnutá, a
- rozsvietená.
V rámci React Native môze mať každý komponent definovaný aj svoj vlastný stav, ktorý si bude následne pamätať. Stav sa môže v čase meniť napr. na základe interakcie používateľa (stlačením tlačidla na baterke).
Definig State in React Native
(slide) Pridať stav do komponentu je možné pomocou volani hook-u
useState()
nasledovne:const [<getter>, <setter>] = useState(<initialValue>);
kde:
getter
reprezentuje premennú, ktorá bude stav komponentu reprezentovať,setter
reprezentuje názov funkcie, pomocou ktorej bude možné zmeniť stav, ainitialValue
je hodnota, ktorá reprezentuje počiatočný stav komponentu.
Refactoring
V našom prípade môžeme nazvať stavovú premennú
isOn
, ktorá bude inicializovaná na hodnotufalse
(nesvieti). Funkcia na zmenu stavu sa bude volaťsetIsOn
:const [isOn, setIsOn] = useState(false);
Kód komponentu bude teda vyzerať nasledovne:
export default function App() { const [isOn, setIsOn] = useState(false); return ( <View style={styles.container}> <Text> .js to start working on your app! Open up App</Text> <Button ={function () { onPressconsole.log("Button was pressed"); }}="Turn On" title/> </View> ; ) }
Stav sa bude meniť pri stlačení tlačidla. Upravíme teda anonymnú funkciu v komponente
Button
, ktorá sa zavolá pri stlačení tlačidla:return ( <View style={styles.container}> <Text> .js to start working on your app! Open up App</Text> <Button ={function () { onPresssetIsOn(!isOn); console.log(isOn); }}="Turn On" title/> </View> ; )
Zmenu stavu môžeme teraz vidieť vypísanú v konzole, kde sa bude stlačením tlačidla striedať hodnota
false
atrue
.Na zmenu stavu môžeme zareagovať aj priamo v aplikácií zmenou textu v komponente
Text
a zmenou názvu tlačidla v komponenteButton
:export default function App() { const [isOn, setIsOn] = useState(false); return ( <View style={styles.container}> <Text>{isOn === true ? "On" : "Off"}</Text> <Button ={function () { onPresssetIsOn(!isOn); console.log(isOn); }}={isOn === true ? "Turn Off" : "Turn On"} title/> </View> ; ) }
Poznámka
Ak budete pozorne sledovať, všimnete si, že pri ošetrení stlačenia tlačidla nedôjde k aktualizovaní hodnoty stavu okamžite. To je spôsobené tým, že funkcia
setIsOn()
pridá zmenu stavu ako úlohu do fronty, ktorá sa vyprázdňuje postupne. Ak chcete ale zareagovať okamžite, funkciasetIsOn()
môže ako parameter dostať funkciu, ktorá vráti nový stav. Aktualizovaný komponentButton
môže vyzerať nasledovne:<Button ={function () { onPressconsole.log(`>> before: ${isOn}`); setIsOn(function(state){ = !state; state console.log(`>> after: ${state}`); return state; ; }) }}={isOn === true ? "Turn Off" : "Turn On"} title/>
Image Component
(slide) Jediné, čo nám aktuálne chýba do používateľského rozhrania baterky, je obrázok reprezentujúci stav baterky. Ten vytvoríme pomocou komponentu
Image
.Komponent
Image
je možné použiť na zobrazenie obrázkov z rôznych zdrojov, ako- obrázky umiestnené v internete/sieti,
- statické obrázky,
- dočasné lokálne obrázky, a
- obrázky z lokálneho disku.
Pre rozličné typy obrázkov je možné použiť rozličné atribúty. Preto komponent
Image
nemá povinné props, ako tomu bolo v prípade komponentuButton
.Pre naše potreby budeme pracovať s obrázkami, ktoré budú statickou súčasťou aplikácie. Tieto obrázky, ktoré reprezentujú zasvietenú a zhasnutú žiarovku, uložíme do priečinku
assets/
Ak ich budeme chcieť použiť v komponente
Image
, použijeme propertysource
, kde sa na príslušný obrázok odkážeme pomocou volaniarequire()
. Upravíme teda pohľad komponentuApp
pridaním komponentuImage
medzi text a tlačidlo s obrázkom zhasnutej žiarovky:<Image source={require("./assets/bulb_off.png")}></Image>
Tu je však potrebné si uvedomiť niekoľko vecí:
- výsledkom volania funkcie
require()
je len číslo zdroja, ktoré bundler zabalil do projektu - funkcia
require()
sa volá pri balení aplikácie a teda nemôže obsahovať dynamicky konštruované reťazce
- výsledkom volania funkcie
Upravíme teda kód komponentu tak, aby sa potrebný obrázok vybral mimo komponentu:
export default function App() { const [isOn, setIsOn] = useState(false); var image = isOn ? require("./assets/bulb_on.png") : require("./assets/bulb_off.png"); console.log(image); return ( <View style={styles.container}> <Text>{isOn === true ? "On" : "Off"}</Text> <Image source={image}></Image> <Button ={function () { onPressconsole.log(`>> before: ${isOn}`); setIsOn(function (state) { = !state; state console.log(`>> after: ${state}`); return state; ; }) }}={isOn === true ? "Turn Off" : "Turn On"} title/> </View> ; ) }
Pressable Image
Jediné, čo nám aktuálne chýba v našej aplikácii, je možnosť zmeniť jej stav stlačením obrázku. Ak sa však pozrieme na vlastnosti komponentu
Image
, nenájdeme v zozname nič, čo by pripomínalo možnosť ošetriť stlačenie podobne, ako tomu bolo v prípade obrázka. V tomto prípade treba postupovať ináč.(slide) Na použitie obrázku ako tlačidla použijeme komponent
Pressable
. Ten funguje ako istý kontajner, do ktorého zabalíme komponent, ktorý má byť stlačiteľným.Poznámka
Komponent
Pressable
je novinkou v rámci React Native. Pred jeho zavedením sa používali komponenty s prefixomTouchable*
, ako napr.TouchableHighlight
,TouchableOpacity
aTouchableWithoutFeedback
. KomponentPressable
ich má postupne nahradiť, takže sa dá očakávať, že uvedené komponenty zmiznú z budúcich verzií rámca React Native.Do komponentu
Pressable
teda zabalíme komponentImage
:<Pressable ={function () { onPressconsole.log("Image was pressed."); }}> <Image source={image} /> </Pressable>
Keďže po stlačení obrázka očakávame rovnaké správanie ako po stlačení tlačidla, vytvoríme samostatnú funkciu, ktorú budeme volať v prípade stlačenia oboch komponentov. Upravený komponent
App
bude vyzerať takto:export default function App() { const [isOn, setIsOn] = useState(false); var image = isOn ? require("./assets/bulb_on.png") : require("./assets/bulb_off.png"); const onPressed = function () { console.log(`>> before: ${isOn}`); setIsOn(function (state) { = !state; state console.log(`>> after: ${state}`); return state; ; }); } return ( <View style={styles.container}> <Text>{isOn === true ? "On" : "Off"}</Text> <Pressable onPress={onPressed}> <Image source={image} /> </Pressable> <Button ={onPressed} onPress={isOn === true ? "Turn Off" : "Turn On"} title/> </View> ; ) }
Complete Solution
- Po drobnom refaktorovaní bude výsledné riešenie vyzerať nasledovne:
import React, { useState } from "react";
import {
StyleSheet, View, Button, Image, Pressable
from "react-native";
}
export default function App() {
const [isOn, setIsOn] = useState(false);
var image = isOn
? require("./assets/bulb_on.png")
: require("./assets/bulb_off.png");
const toggleState = function () {
setIsOn(!isOn);
;
}
return (
<View style={styles.container}>
<Pressable onPress={toggleState}>
<Image source={image} />
</Pressable>
<Button
={toggleState}
onPress={isOn === true ? "Turn Off" : "Turn On"}
title/>
</View>
;
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
,
}; })
Conclusion
- Dnes sme teda vytvorili kostru používateľského rozhrania aplikácie baterka. Popri tom sme sa zoznámili s niektorými komponentmi rámca React Native a predstavili sme si ich vlastnosti.
- Nabudúce budeme pokračovať a pokúsime sa rozsvietiť blesk fotoaparátu vo vašom zariadení. Samozrejme len v prípade, že ho zariadenie obsahuje vo svojej výbave.