Ionic + React – Základy komponent a stavu

Následující text obsahuje: popis struktury projektu Ionic + React; ukázku vytvoření první komponenty; zadání.

Po každém kroku aplikaci uložte a vyzkoušejte.

Užitečné odkazy pro další studium:

1. Vytvoření projektu a struktura souborů

Vytvoření projektu přes WebNative

Nový projekt vytvoříte přes rozšíření WebNative ve VS Code. V postranním panelu klikněte na ikonu WebNative a zvolte Start a new project. Jako framework vyberte React+Ionic, jako template Blank. Rozšíření automaticky vygeneruje celou strukturu projektu včetně závislostí.

Aplikaci spustíte tlačítkem Run v panelu WebNative.

Struktura vygenerovaného projektu

Zajímají nás dvě složky: pages/ (celé obrazovky) a components/ (části UI, které se mohou opakovat).

src/
  components/
    ExploreContainer.tsx  ← výchozí placeholder, ignorujte ho
  pages/
    Home.tsx  ← sem budeme psát
  App.tsx  ← routing (v rámci tohoto návodu se nebude měnit)
  main.tsx  ← vstupní bod (v rámci tohoto se návodu nebude měnit)

Stránka je jako plátno – obsahuje záhlaví a scrollovatelný obsah.
Komponenta je znovupoužitelný blok UI – jednou napsat, použít kolikrát chceme.

2. Krok 1 – Vytvořte prázdnou komponentu

Vytvořte soubor src/components/MyCard.tsx. Komponenta zatím zobrazí jen kartu se dvěma tlačítky.

// src/components/MyCard.tsx

import { IonCard, IonCardContent, IonCardHeader,
         IonCardTitle, IonButton } from '@ionic/react';

// Komponenta je funkce, která vrací JSX (HTML-like kód)
function MyCard() {
  return (
    <IonCard>
      <IonCardHeader>
        <IonCardTitle>My Card</IonCardTitle>
      </IonCardHeader>
      <IonCardContent>

        <IonButton expand="block">Calculate</IonButton>
        <IonButton expand="block">Reset</IonButton>

      </IonCardContent>
    </IonCard>
  );
};

// Bez exportu by soubor nešlo importovat odjinud
export default MyCard;

Kód uvnitř return () vypadá jako HTML, ale je to JSX – speciální syntaxe Reactu pro popis UI. Přípona souboru .tsx přesně říká co soubor obsahuje: TypeScript + X (JSX). Každá Ionic komponenta (IonCard, IonButton apod.) musí být naimportována na začátku souboru.

3. Krok 2 – Vložte komponentu do Home.tsx

Otevřete src/pages/Home.tsx. Odstraňte výchozí <ExploreContainer /> a přidejte svoji komponentu.

// src/pages/Home.tsx

import { IonContent, IonHeader, IonPage,
         IonTitle, IonToolbar } from '@ionic/react';

// Import naší komponenty – cesta je relativní od tohoto souboru
import MyCard from '../components/MyCard';

function Home() {
  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonTitle>My App</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent className="ion-padding">

        {/* Vložíme komponentu jako self-closing tag */}
        <MyCard />

      </IonContent>
    </IonPage>
  );
};

export default Home;

Po uložení uvidíte kartu se dvěma tlačítky. Zkuste <MyCard /> napsat dvakrát – zobrazí se dvě karty ze stejného kódu. To je smysl komponent.

4. Krok 3 – Props: data zvenku

Aby byly komponenty opravdu znovupoužitelné, musíme do nich umět poslat data zvenku. Tomu říkáme props. Typ props popíšeme pomocí interface (TypeScript popis toho, co komponenta očekává).

// src/components/MyCard.tsx

import { IonCard, IonCardContent, IonCardHeader,
         IonCardTitle, IonButton } from '@ionic/react';

// Interface popisuje, jaké props komponenta přijímá
interface MyCardProps {
  calcText: string;
  resetText: string;
}

// Props přijmeme jako parametr a rozbalíme je ({ ... })
function MyCard({ calcText, resetText }: MyCardProps) {
  return (
    <IonCard>
      <IonCardHeader>
        <IonCardTitle>My Card</IonCardTitle>
      </IonCardHeader>
      <IonCardContent>
        {/* Složené závorky = výraz v JSX – vypíšeme hodnotu proměnné */}
        <IonButton expand="block">{calcText}</IonButton>
        <IonButton expand="block">{resetText}</IonButton>
      </IonCardContent>
    </IonCard>
  );
};

export default MyCard;

V Home.tsx předáme text jako atributy:

<MyCard calcText="Calculate BMI" resetText="Reset" />

Zkuste zadat jiné texty nebo přidat třetí prop. Každé vložení komponenty může mít jiné hodnoty.

5. Krok 4 – Proč nestačí obyčejná proměnná?

Přidáme výsledek a zkusíme ho měnit pomocí obyčejné proměnné. Spoiler: nebude to fungovat – a to je důvod, proč existuje useState.

// src/components/MyCard.tsx – ŠPATNÁ varianta (záměrně)

function MyCard({ calcText, resetText }: MyCardProps) {

  let result = 0; // Obyčejná proměnná

  // Lze zapsat i jako arrow function: const handleCalc = () => {
  function handleCalc() {
    result = Math.round(Math.random() * 40 + 10);
    console.log('New value:', result); // V konzoli se změní...
  }

  // Lze zapsat i jako arrow function: const handleReset = () => {
  function handleReset() {
    result = 0;
  }

  return (
    <IonCard>
      <IonCardContent>
        <IonButton expand="block" onClick={handleCalc}>{calcText}</IonButton>
        <IonButton expand="block" onClick={handleReset}>{resetText}</IonButton>
        <p>Result: {result}</p> {/* ...ale tady se nic nezmění! */}
      </IonCardContent>
    </IonCard>
  );
};
Vyzkoušejte: Otevřete konzoli prohlížeče (F12 → záložka Console) a klikejte na tlačítko. V konzoli uvidíte nová čísla – ale na obrazovce zůstane stále „Result: 0". React totiž neví, že se proměnná změnila, a stránku nepřekreslí. K tomu potřebujeme useState.

6. Krok 5 – Oprava pomocí useState

Nahradíme obyčejnou proměnnou hookem useState. Ten je propojený s Reactem – při změně hodnoty se komponenta automaticky překreslí.

const [result, setResult] = useState(0);
//     ↑         ↑              ↑
//  čteme      měníme     výchozí hodnota
// src/components/MyCard.tsx – SPRÁVNÁ varianta

import { useState } from 'react';                     // ← přidat import
import { IonCard, IonCardContent, IonButton } from '@ionic/react';

interface MyCardProps {
  calcText: string;
  resetText: string;
}

function MyCard({ calcText, resetText }: MyCardProps) {

  // let result = 0;              ← toto nefungovalo
  const [result, setResult] = useState(0); // ← toto funguje

  // Lze zapsat i jako arrow function: const handleCalc = () => {
  function handleCalc() {
    const newResult = Math.round(Math.random() * 40 + 10);
    setResult(newResult); // Zavolání setResult spustí překreslení
  }

  // Lze zapsat i jako arrow function: const handleReset = () => {
  function handleReset() {
    setResult(0);
  }

  return (
    <IonCard>
      <IonCardContent>
        <IonButton expand="block" onClick={handleCalc}>{calcText}</IonButton>
        <IonButton expand="block" onClick={handleReset}>{resetText}</IonButton>
        <p>Result: {result}</p>
      </IonCardContent>
    </IonCard>
  );
};

export default MyCard;
Vyzkoušejte: Klikněte na „Calculate" – hodnota se nyní mění na obrazovce. „Reset" ji vrátí na 0. Stav si pamatuje hodnotu mezi kliknutími, protože žije mimo tělo funkce komponenty.

7. Krok 6 – Přenos výsledku do rodiče (callback)

Co když chceme výsledek zobrazit v Home.tsx, ne uvnitř karty? Potomek nemůže přímo měnit stav rodiče – místo toho mu rodič předá funkci (callback), kterou potomek zavolá s výsledkem.

Stav přesuneme z MyCard do Home. Komponenta se postará jen o výpočet a předání hodnoty nahoru.

MyCard.tsx – odešle výsledek přes callback

// src/components/MyCard.tsx

import { IonCard, IonCardContent, IonButton } from '@ionic/react';

interface MyCardProps {
  calcText: string;
  resetText: string;
  onResult: (value: number) => void; // Callback prop – funkce kam pošleme číslo
}

function MyCard({ calcText, resetText, onResult }: MyCardProps) {

  // Lze zapsat i jako arrow function: const handleCalc = () => {
  function handleCalc() {
    const newResult = Math.round(Math.random() * 40 + 10);
    onResult(newResult); // Pošleme výsledek rodiči
  }

  // Lze zapsat i jako arrow function: const handleReset = () => {
  function handleReset() {
    onResult(0);
  }

  return (
    <IonCard>
      <IonCardContent>
        <IonButton expand="block" onClick={handleCalc}>{calcText}</IonButton>
        <IonButton expand="block" onClick={handleReset}>{resetText}</IonButton>
      </IonCardContent>
    </IonCard>
  );
};

export default MyCard;

Home.tsx – vlastní stav, zobrazuje výsledek

// src/pages/Home.tsx

import { useState } from 'react';
import { IonContent, IonHeader, IonPage,
         IonTitle, IonToolbar } from '@ionic/react';
import MyCard from '../components/MyCard';

function Home() {

  // Stav žije tady – Home rozhoduje, co s výsledkem udělá
  const [result, setResult] = useState(0);

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonTitle>My App</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent className="ion-padding">

        {/* onResult dostane setResult – MyCard ho zavolá s vypočtenou hodnotou */}
        <MyCard
          calcText="Calculate"
          resetText="Reset"
          onResult={setResult}
        />

        {/* Výsledek zobrazujeme zde v rodiči */}
        <p>Result: {result}</p>

      </IonContent>
    </IonPage>
  );
};

export default Home;
Shrnutí: MyCard neví nic o tom, co se s výsledkem stane – jen zavolá funkci, kterou dostala jako prop s názvem onResult. Jinými slovy: funkce setResult z rodiče nyní v potomkovi vystupuje pod zapůjčeným jménem onResult. Rodič (Home) si tak rozhoduje, co s číslem udělá. Stav žije vždy v rodičovské komponentě, dítě ho mění přes tuto propůjčenou funkci (tzv. callback prop). Tomuto vzoru se v Reactu říká lifting state up.

8. Shrnutí + Zadání

Navazující úkoly:

Nápověda - 1: Práce se vstupy (IonInput) v Reactu
Při použití klasického Reactu a prvků HTML je k získávání dat běžně využívána událost onChange a hodnota je vyčítána z e.target.value. V Ionicu je však u komponent pro vkládání definována událost onIonInput a samotná hodnota je vždy zabalena do vnitřního objektu detail.

Následujícím způsobem lze zcela obsloužit změnu hodnoty:
onIonInput={(e) => setMyPhoneNumber(e.detail.value || '')}
Nápověda - 2: Aktualizace polí ve stavu v Reactu
V Reactu se stavové pole vytváří a následně aktualizuje (rozšiřuje o nový prvek) následovně:
const [history, setHistory] = useState<number[]>([]); // 1. Vytvoření
setHistory([...history, novyVysledek]); // 2. Aktualizace
Více informací lze najít v oficiální dokumentaci:
React Dokumentace – Updating Arrays in State