Tilan hallinta

Keskitaso

Sovelluksesi kasvaessa, kannattaa olla tietoinen miten tilasi on järjestetty sekä miten tieto kulkee komponenttien välilä. Turha tai toistettu tila on yleinen bugien lähde. Tässä luvussa opit miten tila järjestetään hyvin, miten tilapäivityksen logiikka pidetään ylläpidettävänä, sekä miten tila jaetaan kaukaisten komponettien välillä.

Reagointi syötteeseen tilalla

Reactissa et muokkaa käyttöliittymäkoodia suoraan. Esimerkiksi, et kirjoita komentoja kuten “poista painike käytöstä”, “ota painike käyttöön”, “näytä onnistumisviesti”, jne. Sen sijaan kerrot käyttöliittymän, jonka haluat nähdä erilaisissa komponentin tiloissa (“alkutila”, “kirjoitetaan -tila”, “onnistumistila”), ja sitten vaihdat tilaa käyttäjän syötteen pohjalta. Tämä vastaa samaa kuin miten suunnittelijat ajattelevat käyttöliittymiä.

Tässä on Reactilla rakennettu tietokilpailulomake. Huomaa miten siinä käytetään status tilamuuttujaa päättämään mikäli lähetä -painike on käytössä vai ei, ja mikäli onnistumisviesti näytetään.

import {useState} from 'react';

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return <h1>That's right!</h1>;
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('submitting');
    try {
      await submitForm(answer);
      setStatus('success');
    } catch (err) {
      setStatus('typing');
      setError(err);
    }
  }

  function handleTextareaChange(e) {
    setAnswer(e.target.value);
  }

  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={answer}
          onChange={handleTextareaChange}
          disabled={status === 'submitting'}
        />
        <br />
        <button disabled={answer.length === 0 || status === 'submitting'}>
          Submit
        </button>
        {error !== null && <p className="Error">{error.message}</p>}
      </form>
    </>
  );
}

function submitForm(answer) {
  // Pretend it's hitting the network.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let shouldError = answer.toLowerCase() !== 'lima';
      if (shouldError) {
        reject(new Error('Good guess but a wrong answer. Try again!'));
      } else {
        resolve();
      }
    }, 1500);
  });
}

Oletko valmis oppimaan tämän aiheen?

Lue Reagointi syötteeseen tilalla oppiaksesi lähestymään vuorovaikutusta tilalähtöisellä ajattelutavalla.

Lue lisää

Tilarakenteen päättäminen

Hyvän tilarakenteen päättäminen voi tehdä suuren eron komponenttien välillä, sellaisen jota on miellyttävä muokata ja korjata, ja sellaisen joka on jatkuva virheiden lähde. Tärkein periaate on se, että tilan ei kuuluisi sisältää tarpeetonta tai toistettua tietoa. Mikäli tilassa on tarpeetonta tietoa, on sen päivitys helppo unohtaa ja aiheuttaa bugeja!

Esimerkiksi tässä lomakkeessa on tarpeeton fullName tilamuuttuja:

import {useState} from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + ' ' + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name: <input value={firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Last name: <input value={lastName} onChange={handleLastNameChange} />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

Voit poistaa sen ja yksinkertaistaa koodia laskemalla fullName muuttujan komponentin renderöinnin aikana:

import {useState} from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name: <input value={firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Last name: <input value={lastName} onChange={handleLastNameChange} />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

Tämä saattaa vaikuttaa pieneltä muutokselta, mutta moni bugi Reactissa on korjattu tällä tavoin.

Oletko valmis oppimaan tämän aiheen?

Lue Tilarakenteen päättäminen oppiaksesi miten tilan rakenne suunnitellaan bugien välttämiseksi.

Lue lisää

Tilan jakaminen komponenttien välillä

Joskus haluat, että kahden komponentin tila muuttuu yhdessä. Tämän tehdäksesi, poista tila molemmista komponenteista ja siirrä se lähmimpään pääkomponenttiin, ja välitä tila alas proppeja käyttäen. Tätä kutsutaan “tilan nostamiseksi ylös”, ja se on yksiä yleisimmistä asioista joita tulet tekemään React koodia kirjoittaessasi.

Tässä esimerkissä vain yhden paneelin pitäisi olla aktiivinen kerrallaan. Tämän saavuttamiseksi sen sijaan, että aktiivinen tila säilyisi jokaisen yksittäisen paneelin sisällä, pääkomponentti pitää tilan ja välittää lapsikomponenteilleen tarvittavat propsit.

import {useState} from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel
        title="About"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}>
        With a population of about 2 million, Almaty is Kazakhstan's largest
        city. From 1929 to 1997, it was its capital city.
      </Panel>
      <Panel
        title="Etymology"
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}>
        The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for
        "apple" and is often translated as "full of apples". In fact, the region
        surrounding Almaty is thought to be the ancestral home of the apple, and
        the wild <i lang="la">Malus sieversii</i> is considered a likely
        candidate for the ancestor of the modern domestic apple.
      </Panel>
    </>
  );
}

function Panel({title, children, isActive, onShow}) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? <p>{children}</p> : <button onClick={onShow}>Show</button>}
    </section>
  );
}

Oletko valmis oppimaan tämän aiheen?

Lue Tilan jakaminen komponenttien välillä oppiaksesi miten tila nostetaan ylös ja komponentit pidetään synkronoituna.

Lue lisää

Tilan säilyttäminen ja nollaus

Kun uudelleenrenderöit komponenttia, Reactin täytyy päättää mitkä osat puusta pitää (ja päivittää), ja mitkä osat häivittää tai luoda uudelleen alusta alkaen. Useimmissa tapauksissa Reactin automaattinen käyttäytyminen toimii tarpeeksi hyvin. Oletuksena React ylläpitää osat puusta, jotka “vastaavat” aiemmin renderöityä komponettipuuta.

Kuitenkin, joskus tämä ei ole sitä mitä haluat. Tässä sovelluksessa viestin kirjoittaminen ja käyttäjän vaihtaminen ei tyhjää syötettä. Tämä voi saada käyttäjän vahingossa lähettämään viestin väärälle henkilölle:

import {useState} from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={(contact) => setTo(contact)}
      />
      <Chat contact={to} />
    </div>
  );
}

const contacts = [
  {name: 'Taylor', email: 'taylor@mail.com'},
  {name: 'Alice', email: 'alice@mail.com'},
  {name: 'Bob', email: 'bob@mail.com'},
];

Reactilla voit ohittaa oletuskäytännön pakottamalla komponentin tyhjäämään sen tila antamalla sille eri key propsin, kuten <Chat key={email} />. Tämä kertoo Reactille, että mikäli vastaanottaja on eri, pitäisi Chat komponentin olla eri komponentti, joka täytyy luoda uudelleen alusta alkaen uusilla tiedoilla (kuten käyttöliittymän syöttökentät). Nyt vastaanottajien vaihtaminen nollaa syöttökentän—vaikka renderöit saman komponentin.

import {useState} from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={(contact) => setTo(contact)}
      />
      <Chat key={to.email} contact={to} />
    </div>
  );
}

const contacts = [
  {name: 'Taylor', email: 'taylor@mail.com'},
  {name: 'Alice', email: 'alice@mail.com'},
  {name: 'Bob', email: 'bob@mail.com'},
];

Oletko valmis oppimaan tämän aiheen?

Lue Tilan säilyttäminen ja nollaus oppiaksesi tilan elämänkaari ja miten sitä hallitaan.

Lue lisää

Tilalogiikan siirtäminen reduceriin

Komponentit, joissa on useita tilapäivityksiä, jotka on hajallaan useisiin Tapahtumankäsittelijöihin, voivat olla hankalia ymmärtää. Näihin tapauksiin voit tiivistää kaikki tilamuutoksen logiikan komponentin ulkopuolelle yhteen funktioon, jota kutsutaan “reduceriksi”. Tapahtumankäsittelijöistäsi tulee tiivitä, koska ne määrittelevät ainoastaan käyttäjän “toiminnot”. Tiedoston lopussa reducer funktio määrittelee miten tila kuuluisi päivittää kuhunkin tapahtumaan nähden!

import {useReducer} from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task,
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId,
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: 'Visit Kafka Museum', done: true},
  {id: 1, text: 'Watch a puppet show', done: false},
  {id: 2, text: 'Lennon Wall pic', done: false},
];

Oletko valmis oppimaan tämän aiheen?

Lue Tilalogiikan siirtäminen reduceriin oppiaksesi miten logiikkaa tiivistetään reducer funktioon.

Lue lisää

Tiedon välittäminen syvälle kontekstilla

Usein täytyy antaa tietoa pääkomponentista lapsikomponettiin propsien avulla. Mutta propsien välittämisestä saattaa tulla epämukavaa jos proppeja täytyy antaa useiden komponenttien läpi, tai jos moni komponentti tarvitsee samaa tietoa. Kontekstin avulla pääkomponenti voi asettaa tietyn tiedon saataville kaikkiin komponentteihin pääkomponentin sisällä-riippumatta siitä miten syvällä se on-ilman, että sitä annetaan propsien kautta.

Tässä Heading komponentti päättelee sen otsikointitason “kysymällä” sen lähimmältä Section komponentilta sen tason. Jokainen Section seuraa sen omaa tasoa kysymällä sitä Section pääkomponentilta ja lisäämällä siihen yhden. Jokainen Section tarjoaa tiedon saataville kaikille sen alakomponenteille ilman, että proppeja täytyy antaa—se tekee sen käyttämällä kontekstia.

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading>Title</Heading>
      <Section>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Section>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

Oletko valmis oppimaan tämän aiheen?

Lue Tiedon välittäminen syvälle kontekstilla oppiaksesi miten käytetään kontekstia propsien välittämisen sijaan.

Lue lisää

Skaalaus reduktorin ja kontekstin avulla

Reduktorin avulla voit yhdistää komponentin tilanpäivityslogiikan. Kontekstin avulla voit antaa tietoa syvälle muihin komponentteihin. Voit yhdistää reduktoreita ja konteksteja yhteen hallitaksesi monimutkaisen ruudun tilaa.

Tällä lähestymistavalla monimutkaisen tilan omaava pääkomponentti hallitsee sitä reduktorilla. Muut komponentit syvällä komponenttipuussa voivat lukea tilaa kontekstilla. Ne voivat myös dispatchata eli lähettää toimintoja päivittääkseen tilaa.

import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import {TasksProvider} from './TasksContext.js';

export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

Oletko valmis oppimaan tämän aiheen?

Lue Skaalaus reduktorin ja kontekstin avulla oppiaksesi miten tilan hallinta skaalautuu kasvavassa sovelluksessa.

Lue lisää

Mitä seuraavaksi

Siirry seuraavaksi sivulle Tilan reagointi syötteeseen lukeaksesi tämän luvun sivu kerrallaan!

Tai, jos aiheet ovat tuttuja, mikset lukisi Escape Hatches?