React Native Academy · Oppitunti

Tilan nostaminen ylemmälle tasolle

Siirtäkää jaettu tila lähimpään yhteiseen yläkomponenttiin ja välittäkää sekä arvo että päivityscallback props-ominaisuuksina, jotta sisarkomponentit pysyvät synkronissa.

Oppitunti 3/413 vaihetta

Tilan nostaminen ylemmälle tasolle on ilmainen React Native Academy-oppitunti CoddyKitissä. Tämä on oppitunti 3/4. Voit lukea tästä oppimispolusta kokonaan mitkä tahansa 3 oppituntia ilmaiseksi — sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä käytännön harjoittelun sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. Oppitunti kuuluu React Native Academy-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. React Native Academy-kurssilla on yhteensä 4 oppituntia.

Ongelma: sisaruskomponentit tarvitsevat yhteisen tilan

Ajatellaan kahta sisaruskomponenttia, joiden täytyy jakaa tietoja — esimerkiksi hakusyötekenttäkomponenttia ja hakukyselyn perusteella suodatettavaa tulosluetteloa. Jos hakutila sijaitsee hakusyötekenttäkomponentin sisällä, tulosluettelolla ei ole tapaa käyttää sitä. Kumpikaan sisaruskomponentti ei voi nähdä suoraan toisen tilaa. Ratkaisu on nostaa tila ylemmälle tasolle lähimpään yhteiseen edeltäjäkomponenttiin — komponenttiin, joka renderöi molemmat sisaruskomponentit. Tämän jälkeen vanhempi välittää tila-arvon toiselle lapselle ja päivityksen takaisinkutsun toiselle.

// PROBLEM: SearchInput holds state that ResultsList needs
// but siblings can't communicate directly

function SearchInput() {
  const [query, setQuery] = useState(''); // stuck here
  return <TextInput value={query} onChangeText={setQuery} />;
}

function ResultsList() {
  // How do we access 'query' from the sibling? We can't!
  return <FlatList data={/* needs query */} />;
}

Tilan nostaminen yhteiseen edeltäjäkomponenttiin

Siirtäkää jaettu tila lähimpään yhteiseen edeltäjäkomponenttiin — komponenttiin, joka renderöi molemmat sisaruskomponentit. Vanhempi omistaa nyt tilan ja välittää sen propseina lapsille: hakukysely välitetään tulosluettelolle ja päivitysfunktio hakusyötekentälle. Sisaruskomponentit viestivät vanhemman kautta: hakusyötekenttä kutsuu propina saatua päivitysfunktiota, joka päivittää vanhemman tilan, minkä jälkeen uusi hakukyselyn arvo välittyy tulosluettelolle uutena propina. Tämä on Reactin tietovirran perusmalli.

import { useState } from 'react';
import { View, TextInput, Text } from 'react-native';

// Parent owns the shared state
export default function SearchScreen() {
  const [query, setQuery] = useState('');
  const results = ITEMS.filter(item =>
    item.toLowerCase().includes(query.toLowerCase())
  );

  return (
    <View style={{ flex: 1, padding: 16 }}>
      <SearchInput query={query} onQueryChange={setQuery} />
      <ResultsList results={results} />
    </View>
  );
}

const ITEMS = ['Apple', 'Banana', 'Blueberry', 'Cherry', 'Avocado'];

Lapsikomponenttien kirjoittaminen

Kun tila on nostettu ylemmälle tasolle, lapsikomponenteista tulee yksinkertaisia ja selkeästi rajattuja. SearchInput-komponentti vastaanottaa propseina query-arvon (nykyinen arvo) ja onQueryChange-funktion (arvon päivittämistä varten). ResultsList vastaanottaa suodatetun tulostaulukon. Kumpikaan lapsi ei hallitse tilaa, vaan ne ovat hallittuja komponentteja, joiden toiminta määräytyy kokonaan propsien perusteella. Tämän ansiosta molemmat komponentit on helppo testata erikseen: välittäkää niille propsit ja tarkistakaa renderöity tulos.

import { TextInput, FlatList, Text, View, StyleSheet } from 'react-native';

// Controlled: receives value and updater as props
function SearchInput({ query, onQueryChange }) {
  return (
    <TextInput
      value={query}
      onChangeText={onQueryChange}
      placeholder='Search...'
      style={styles.input}
    />
  );
}

// Pure display: receives filtered data as prop
function ResultsList({ results }) {
  return (
    <FlatList
      data={results}
      keyExtractor={item => item}
      renderItem={({ item }) => (
        <Text style={styles.item}>{item}</Text>
      )}
    />
  );
}

const styles = StyleSheet.create({
  input: { borderWidth: 1, borderColor: '#ccc', borderRadius: 8, padding: 12, marginBottom: 12 },
  item: { padding: 12, borderBottomWidth: 1, borderBottomColor: '#eee' },
});

Lähimmän yhteisen edeltäjäkomponentin löytäminen

Keskeinen kysymys tilaa nostettaessa on: ”Mikä on puun alin komponentti, joka renderöi kaikki tätä tilaa tarvitsevat komponentit?” Nostakaa tila juuri tähän komponenttiin — älkää sitä ylemmäs. Tilan nostaminen liian korkealle (esimerkiksi aina App-komponenttiin asti) aiheuttaa tarpeettomia uudelleenrenderöintejä toisiinsa liittymättömissä komponenteissa. Lähin yhteinen edeltäjäkomponentti tasapainottaa saavutettavuuden (kaikki käyttäjät voivat saada tilan propseina) ja suorituskyvyn (vain tilaa tarvitseva alipuu renderöidään uudelleen). Piirtäkää komponenttipuu ja seuratkaa polkuja kustakin käyttäjästä ylöspäin löytääksenne kohdan, jossa ne yhdistyvät.

// Component tree:
// App
// └── TabBar (selected tab)
//     ├── HomeTab
//     │   └── Feed
//     └── ProfileTab
//
// If 'selected tab' is needed by TabBar AND by Feed to filter content,
// lift it to TabBar (nearest common ancestor of both).
// DO NOT lift all the way to App unnecessarily.

function TabBar() {
  const [activeTab, setActiveTab] = useState('home');
  return (
    <View>
      <TabButtons activeTab={activeTab} onChange={setActiveTab} />
      {activeTab === 'home' ? <HomeTab /> : <ProfileTab />}
    </View>
  );
}

Useat sisaruskomponentit jakavat tilan

Kun vähintään kolme sisaruskomponenttia tarvitsee samaa tilaa, nostakaa tila kerran niiden yhteiseen vanhempaan ja välittäkää se jokaiselle sitä tarvitsevalle sisarukselle. Tuotenäkymässä voivat olla esimerkiksi ImageGallery, VariantPicker ja AddToCartButton. Valitun vaihtoehdon tila sijaitsee näkymän vanhemmassa komponentissa ja välittyy kaikille kolmelle. VariantPicker saa päivitysfunktion valinnan muuttamista varten. ImageGallery näyttää valitun vaihtoehdon kuvat. AddToCartButton lisää juuri valitun vaihtoehdon ostoskoriin.

import { useState } from 'react';
import { View } from 'react-native';

const VARIANTS = ['Red / S', 'Red / M', 'Blue / S', 'Blue / M'];

export default function ProductDetailScreen() {
  const [selectedVariant, setSelectedVariant] = useState(VARIANTS[0]);

  return (
    <View style={{ flex: 1 }}>
      <ImageGallery variant={selectedVariant} />
      <VariantPicker
        variants={VARIANTS}
        selected={selectedVariant}
        onSelect={setSelectedVariant}
      />
      <AddToCartButton variant={selectedVariant} />
    </View>
  );
}

Takaisinkutsupropsit ja jaettu tila

Joskus komponentit jakavat toiminnon tila-arvon sijaan. Yleinen esimerkki on luettelosivu, jonka otsikossa on ostoskorikuvake: minkä tahansa luettelokohteen ”Lisää ostoskoriin” -painikkeen painamisen pitäisi päivittää otsikon ilmoitusmerkin lukumäärä. Luettelokohteet ja otsikko ovat sisaruskomponentteja saman näkymän vanhemman alla. Vanhempi säilyttää ostoskorin lukumäärän tilassa, välittää lukumäärän propseina otsikolle ja onAddToCart-takaisinkutsun luettelolle. Kohteet kutsuvat takaisinkutsua, vanhempi päivittää tilansa ja otsikon ilmoitusmerkki renderöidään uudelleen uuden lukumäärän mukaisesti.

import { useState } from 'react';
import { View } from 'react-native';

export default function ShopScreen() {
  const [cartCount, setCartCount] = useState(0);

  function addToCart(item) {
    // Business logic lives in the parent
    setCartCount(prev => prev + 1);
    console.log('Added:', item.name);
  }

  return (
    <View style={{ flex: 1 }}>
      {/* Sibling 1: displays count from parent state */}
      <ShopHeader cartCount={cartCount} />
      {/* Sibling 2: triggers parent state update */}
      <ProductList onAddToCart={addToCart} />
    </View>
  );
}

Milloin tilan nostamisesta tulee propseilla välittämistä

Jos yhteinen edeltäjäkomponentti on usean tasoportaan päässä tilaa tarvitsevista komponenteista, tuloksena on propsien välittäminen ketjussa — propsien kuljettaminen monien sellaisten välikomponenttien läpi, jotka eivät itse käytä niitä. Jos yhteinen edeltäjäkomponentti on esimerkiksi App-juuri ja App-komponentin sekä käyttäjien välillä on viisi komponenttitasoa, jokaisesta välikomponentista tulee viestinvälittäjä. Syvään jakamiseen ratkaisu on Context API (tiedoille, joita monet komponentit tarvitsevat) tai tilanhallintakirjasto, kuten Zustand tai Redux. Nyrkkisääntö: nostakaa tila ylemmälle tasolle, mutta jos se kulkee yli 2–3 tason läpi ilman käyttöä matkan varrella, harkitkaa Contextin käyttöä.

// When prop drilling gets too deep, switch to Context
// Instead of:
function App() {
  const [user, setUser] = useState(null);
  return <Nav user={user}>   // not used
    <Drawer user={user}>     // not used
      <Screen user={user}>  // not used
        <ProfileCard user={user} />  // finally used
      </Screen>
    </Drawer>
  </Nav>;
}

// Use Context:
const UserContext = React.createContext(null);
function App() {
  const [user, setUser] = useState(null);
  return (
    <UserContext.Provider value={user}>
      <Nav><Drawer><Screen><ProfileCard /></Screen></Drawer></Nav>
    </UserContext.Provider>
  );
}

Käytännön esimerkki: lämpötilamuunnin

Klassinen esimerkki tilan nostamisesta ylemmälle tasolle on kaksi synkronoitua lämpötilan syötekenttää (Celsius ja Fahrenheit). Kumpikin syötekenttä vastaanottaa nykyisen lämpötilan ja onChange-takaisinkutsun. Vanhempi säilyttää lämpötilan yhdessä yksikössä (esimerkiksi Celsius-asteina) ja muuntaa sen toisen kentän yksiköksi. Kun jompikumpi kenttä muuttuu, vanhempi muuntaa arvon ja päivittää tilansa. Molemmat kentät saavat oikean arvon vanhemman tilasta, joten ne pysyvät täydellisesti synkronoituina jaetun vanhemman tilan kautta.

import { useState } from 'react';
import { View, Text, TextInput, StyleSheet } from 'react-native';

function TempInput({ label, value, onChangeText }) {
  return (
    <View style={styles.row}>
      <Text style={styles.label}>{label}</Text>
      <TextInput
        value={value}
        onChangeText={onChangeText}
        keyboardType='decimal-pad'
        style={styles.input}
      />
    </View>
  );
}

export default function TemperatureConverter() {
  const [celsius, setCelsius] = useState('');

  const fahrenheit = celsius !== '' ? (parseFloat(celsius) * 9/5 + 32).toFixed(1) : '';

  function handleFahrenheitChange(f) {
    const c = f !== '' ? ((parseFloat(f) - 32) * 5/9).toFixed(1) : '';
    setCelsius(c);
  }

  return (
    <View style={{ padding: 24 }}>
      <TempInput label='Celsius' value={celsius} onChangeText={setCelsius} />
      <TempInput label='Fahrenheit' value={fahrenheit} onChangeText={handleFahrenheitChange} />
    </View>
  );
}

const styles = StyleSheet.create({
  row: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 },
  label: { width: 100, fontSize: 16 },
  input: { flex: 1, borderWidth: 1, borderColor: '#ccc', borderRadius: 8, padding: 10 },
});

Sijoittakaa tila lähelle sen käyttöpaikkaa

Tilan nostamisen vastaperiaate on pitää tila mahdollisimman alhaalla. Jos jokin tila tarvitaan vain yhdessä komponentissa, pitäkää se kyseisen komponentin sisällä — älkää nostako sitä ylemmälle tasolle tarpeettomasti. Esimerkiksi avattavan valikon avoin/suljettu-tila ei tarvitse sijaita vanhemmassa näkymässä, ellei vanhemman tarvitse reagoida siihen. Paikallisen tilan pitäminen paikallisena parantaa suorituskykyä (harvemmat komponentit renderöidään uudelleen) ja helpottaa koodin ymmärtämistä (tila ja sitä käyttävä käyttöliittymä ovat samassa paikassa). Nostakaa tila ylemmälle tasolle vain, kun vähintään kaksi komponenttia todella tarvitsee samoja tietoja.

// Local state: Accordion open/close is private to Accordion
function Accordion({ title, children }) {
  const [open, setOpen] = useState(false); // stays local — parent doesn't care
  return (
    <View>
      <TouchableOpacity onPress={() => setOpen(prev => !prev)}>
        <Text>{title} {open ? '▲' : '▼'}</Text>
      </TouchableOpacity>
      {open && <View style={{ paddingLeft: 16 }}>{children}</View>}
    </View>
  );
}

// Only lift open state to parent IF the parent needs to:
// - Know which accordion is open to close others
// - Save/restore the open state on navigation
// Otherwise, keep it local!

Hallitsemattomat ja hallitut komponentit

Komponentti on hallittu, kun sen tilaa ohjataan kokonaan propsien avulla ja sitä muutetaan takaisinkutsujen kautta — kuten hakusyötekenttäesimerkissä. Komponentti on hallitsematon, kun se hallitsee omaa sisäistä tilaansa välittämättä sitä vanhemmalle. Reactissa käytetään useimmiten hallittuja komponentteja, koska niiden toiminta on ennakoitavaa ja niitä on helppo testata. Hallitsemattomia syötekenttiä (joiden arvoja luetaan ref-viittauksen avulla) käytetään joskus suorituskyvyn vuoksi (jotta jokainen näppäinpainallus ei aiheuta uudelleenrenderöintiä) tai integroitavaksi Reactin ulkopuolisen koodin kanssa. React Nativen TextInput-komponentin oletuslomake-elementit ovat hallittuja, kun välitätte niille value- ja onChangeText-propsit.

import { TextInput, useRef } from 'react-native';

// Uncontrolled: parent reads value on demand via ref
function UncontrolledInput({ inputRef }) {
  return (
    <TextInput
      ref={inputRef}
      // No value prop: TextInput manages its own text
      defaultValue='Initial text'
    />
  );
}

// Parent reads the value when the form is submitted:
function Form() {
  const inputRef = useRef(null);
  function handleSubmit() {
    // Use _lastNativeText (not recommended) or store value in ref.current
    console.log('Value:', inputRef.current?.props?.value);
  }
}

Käytännön esimerkki: suodatin ja tulospaneeli

Käytännön esimerkki tilan nostamisesta on hakunäkymä, jossa suodatinpalkki (luokan valitsin, hintavälin liukusäädin ja lajittelun valitsin) on tulosluettelon yläpuolella. Jokainen suodattimen arvo sijaitsee ylemmässä SearchScreen-komponentissa. Suodatinpalkki vastaanottaa nykyiset suodattimien arvot ja päivitysfunktiot propseina. Tulosluettelo vastaanottaa nykyiset suodattimet ja renderöidään uudelleen aina, kun jokin suodatin muuttuu. Ylempi komponentti hakee myös uudet tulokset aina suodattimien muuttuessa (käyttämällä useEffect-hookia suodatinriippuvuuksien kanssa). Kun tämä koordinointi tehdään yhden ylemmän komponentin kautta, hakuominaisuus toimii ennakoitavasti ja on helppo testata.

import { useState, useEffect } from 'react';
import { View } from 'react-native';

export default function SearchScreen() {
  // All filter state lifted here
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState('all');
  const [sortBy, setSortBy] = useState('relevance');
  const [results, setResults] = useState([]);

  // Fetch results whenever any filter changes
  useEffect(() => {
    fetchResults({ query, category, sortBy }).then(setResults);
  }, [query, category, sortBy]);

  return (
    <View style={{ flex: 1 }}>
      <FilterBar
        query={query} onQueryChange={setQuery}
        category={category} onCategoryChange={setCategory}
        sortBy={sortBy} onSortChange={setSortBy}
      />
      <ResultsList results={results} />
    </View>
  );
}

Pikatarkistus

Testatkaa, miten hyvin ymmärrätte tämän oppitunnin React Native -mobiilikehityksen käsitteet.

Oppitunnin yhteenveto

Tässä oppitunnissa opitte, että tilan nostaminen lähimpään yhteiseen esi-isään antaa sisarkomponenteille mahdollisuuden jakaa tietoja ylemmän komponentin kautta, kontrolloidut komponentit vastaanottavat arvonsa ja päivitysfunktionsa propseina, mikä tekee niiden toiminnasta ennakoitavaa ja testattavaa, ja että tila kannattaa pitää mahdollisimman alhaalla — nostakaa se ylemmäs vain, kun useampi komponentti todella tarvitsee sitä. Seuraavaksi rakennamme vuorovaikutteisen laskurisovelluksen, jossa yhdistyvät kaikki nämä käsitteet.

Aloita maksutta

Opi JavaScript tekoälytuutorin avulla — ilmaiseksi

Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.

Kurssit
30
Oppitunnit
120

Usein kysytyt kysymykset

Onko oppitunti ”Tilan nostaminen ylemmälle tasolle” ilmainen?

Kyllä — voit lukea täällä verkossa kokonaan ilmaiseksi mitkä tahansa React Native Academy-oppimispolun 3 oppituntia, myös oppitunnin “Tilan nostaminen ylemmälle tasolle”. Sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä interaktiiviset harjoitukset sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. React Native Academy-kurssilla on yhteensä 4 oppituntia.

Mitä opin oppitunnilla ”Tilan nostaminen ylemmälle tasolle”?

Siirtäkää jaettu tila lähimpään yhteiseen yläkomponenttiin ja välittäkää sekä arvo että päivityscallback props-ominaisuuksina, jotta sisarkomponentit pysyvät synkronissa. Harjoittelet React Native Academy-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.

Tarvitsenko kokemusta aloittaakseni React Native Academy-opiskelun?

Aiempi kokemus ei ole tarpeen. CoddyKitin React Native Academy-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 3/4.

Kuinka kauan ”Tilan nostaminen ylemmälle tasolle”-oppitunnin suorittaminen kestää?

Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.

Voinko kirjoittaa ja suorittaa koodia tällä React Native Academy-oppitunnilla?

Kyllä. Jokainen React Native Academy-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.

Kaikki tämän kurssin oppitunnit

  1. Datan välittäminen props-ominaisuuksilla
  2. Komponentin tilan hallinta useStatella
  3. Tilan nostaminen ylemmälle tasolle
  4. Vuorovaikutteisen laskurisovelluksen rakentaminen
← Takaisin: React Native Academy