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ä.
Tässä kappaleessa
- Miten ajatella käyttöliittymän muutoksia tilan muuttuessa
- Miten tila järjestetään hyvin
- Miten “tila nostetaan ylös” jotta se voidaan jakaa komponenttien välillä
- Miten hallita, säilyykö tila vai nollataanko se
- Miten siirretään monimutkainen tilalogiikka funktioon
- Miten annetaan tietoa ilman “propsien porausta”
- Miten skaalata tilaa sovelluksen kasvaessa
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?