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 projekty

    • aktuá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/.

  • Vytvoríme teda priečinok src/ a presunieme do neho všetko, čo sme vytvorili včetne priečinku assets/:

    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úboru index.js, v ktorom exportujeme komponent App zo súboru App.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.

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.
  • V štruktúre projektu v priečinku src/ preto vytvoríme ďalší priečinok s názvom components/, do ktorého tieto komponenty umiestnime - každý do samostatného súboru.

  • Zatiaľ vytvoríme len kostru týchto komponentov a postupne opätovne vytvoríme celú aplikáciu. Komponent ImageSwitch bude uložený v súbore ImageSwitch.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úbore ButtonSwitch.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
                onPress={null}
                title={t("Turn On")}
            />
        );
    }

    Komponent App bude uložený v súbore App.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.

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.

    Parent to Child [@y23]

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 a ButtonSwitch. Ú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ť.
  • Upravíme teda renderovanie komponentu App, kde každému potomkovi pošleme stav komponentu pomocou vlastnosti isOn:

    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
                onPress={null}
                title={props.isOn === true ? 
                    t("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.

    Child to Parent [@y23]
  • 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 funkciu onStateChange(), 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í komponentu App:

    return (
        <View style={styles.container}>
          <ImageSwitch 
              isOn={isOn} 
              onPress={onStateChange}>
          </ImageSwitch>
          <ButtonSwitch 
              isOn={isOn} 
              onPress={onStateChange}>
          </ButtonSwitch>
      </View>
    );
  • Následne upravíme renderovanie tlačidla, kde hodnotu vlastnosti props.onPress priradíme ku vlastnosti onPress komponentu tlačidla Button:

    return (
        <Button
          onPress={props.onPress}
          title={props.isOn === true ? 
              t("Turn Off") : t("Turn On")}
      />
    );
  • Podobne upravíme aj renderovanie obrázku, kde podobne nastavíme vlastnosť onPress komponentu Pressable:

    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 () {
            Torch.switchState(isOn).catch(function (e) {
                Alert.alert(
                    t("Missing Flashlight"),
                    t(
                        "No camera available. Go and buy a " +
                        "device with some (or two) and come " +
                        "back later."
                    ),
                    [
                        {
                            text: t("Quit"),
                            onPress: function () {
                                RNExitApp.exitApp();
                            },
                        },
                    ]
                );
            });
        }, []);
    
        async function onStateChange() {
            let cameraAllowed = true;
    
            if (Platform.OS === "android") {
                const granted = await PermissionsAndroid.request(
                    PermissionsAndroid.PERMISSIONS.CAMERA,
                    {
                        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 = 
                    granted === PermissionsAndroid.RESULTS.GRANTED;
            }
    
            if (cameraAllowed) {
                await Torch.switchState(!isOn);
                setIsOn(!isOn);
            }
        }
    
        return (
            <View style={styles.container}>
                <ImageSwitch 
                    isOn={isOn} 
                    onPress={onStateChange}>
                </ImageSwitch>
                <ButtonSwitch 
                    isOn={isOn} 
                    onPress={onStateChange}>
                </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.

    Sibling to Sibling [@y23]
  • Toto je vlastne aj náš prípad, kedy komponenty ButtonSwitch a ImageSwitch majú spoločného predka App.

  • 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.

  • (slide) Posledný prípad zdieľania údajov je medzi ľubovoľnými dvoma komponentmi aplikácie.

    Any to Any [@y23]
  • 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.

    The Problem [@y31]
  • (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.