Tilarakenteen päättäminen
Structuring state well can make a difference between a component that is pleasant to modify and debug, and one that is a constant source of bugs. Here are some tips you should consider when structuring state.
Tulet oppimaan
- Milloin käyttää yhtä vs useita tilamuuttujia
- Mitä välttää kun tilaa järjestetään
- Miten korjata yleisiä ongelmia tilarakenteessa
Tilarakenteen periaatteet
Kun kirjoitat komponenttia, joka ylläpitää jotain tilaa, sinun täytyy tehdä valintoja siitä miten monta tilamuuttujaa käytät ja missä muodossa niiden tietojen tulisi olla. Vaikka on mahdollista kirjoittaa oikeita ohjelmia epäoptimaalisella tilarakenteella, on muutamia periaatteita, jotka ohjaavat tekemään parempia valintoja:
- Ryhmitä toisiin liittyvä tila. Jos aina päivität kahta tai useampaa tilamuuttujaa yhdessä samanaikaisesti, harkitse näiden yhdistämistä yhteen tilamuuttujaan.
- Vältä tilan ristiriitoja. Kun tila on rakennettu tavalla, jossa useat tilan palaset saattavat olla ristiriidassa ja “olla eri mieltä” toistensa kanssa, jätät tilaa virheille. Koita välttää tätä.
- Vältä tarpeetonta tilaa. Jos voit laskea jotain tietoa komponentin propseista tai sen olemassa olevista tilamuuttujista renderöinnin aikana, sinun ei tulisi asettaa tätä tietoa komponentin tilaan.
- Vältä tilan toistoa. Kun jokin data on toistettuna useiden tilamuuttujien kesken, tai syvällä olioiden sisässä, on hankalaa pitää ne synkronoituna. Vähennä toistoa kun pystyt.
- Vältä syvään sisennettyä tilaa. Syvästi hierarkista tilaa ei ole kovin kätevä päivittää. Kun mahdollista, rakenna tila tasaisella tavalla.
Näiden periaatteiden taustalla oleva tavoite on tehdä tilasta helppoa päivittää tuottamatta virheitä. Tarpeettoman ja toistuvan datan poistaminen tilasta auttaa varmistamaan, että kaikki sen palaset pysyvät synkronoituna. Tämä on samankaltaista kuin se, miten tietokantasuunnittelija saattaa haluta “normalisoida” tietokannan rakenteen vähentääkseen bugien mahdollisuuksia. Albert Einsteinia lainatakseen, “Tee tilastasi niin yksinkertaista kuin mahdollista—mutta ei yksinkertaisempaa.”
Katsotaan nyt miten nämä periaatteet toimivat käytännössä.
Ryhmitä toisiin liittyvä tila
Saatat joskus olla epävarma yhden vai useamman tilamuuttujan käytöstä.
Pitäisikö tehdä näin?
const [x, setX] = useState(0);
const [y, setY] = useState(0);
Vai näin?
const [position, setPosition] = useState({ x: 0, y: 0 });
Teknisesti ottaen, voit käyttää kumpaa tahansa tapaa. Mutta jos aina päivität kaksi tai useampaa tilamuuttujaa samanaikaisesti, kannattaa yhdistää ne yhteen tilamuuttujaan. Siten et unohda pitää niitä synkronoituna, kuten tässä esimerkissä, jossa kursorin liikuttaminen päivittää molempia punaisen pisteen koordinaatteja:
import { useState } from 'react'; export default function MovingDot() { const [position, setPosition] = useState({ x: 0, y: 0 }); return ( <div onPointerMove={e => { setPosition({ x: e.clientX, y: e.clientY }); }} style={{ position: 'relative', width: '100vw', height: '100vh', }}> <div style={{ position: 'absolute', backgroundColor: 'red', borderRadius: '50%', transform: `translate(${position.x}px, ${position.y}px)`, left: -10, top: -10, width: 20, height: 20, }} /> </div> ) }
Toinen tilanne, jossa ryhmität tietoa olioon tai taulukkoon tapahtuu kun et tiedä miten monta eri tilan palasta tarvitset. Esimerkiksi, se käy hyödylliseksi kun tarvitset lomakkeen missä käyttäjä voi lisätä omia kenttiä.
Vältä tilan ristiriitoja
Tässä on hotellin palautelomake, jossa on isSending
ja isSent
tilamuuttujat:
import { useState } from 'react'; export default function FeedbackForm() { const [text, setText] = useState(''); const [isSending, setIsSending] = useState(false); const [isSent, setIsSent] = useState(false); async function handleSubmit(e) { e.preventDefault(); setIsSending(true); await sendMessage(text); setIsSending(false); setIsSent(true); } if (isSent) { return <h1>Thanks for feedback!</h1> } return ( <form onSubmit={handleSubmit}> <p>How was your stay at The Prancing Pony?</p> <textarea disabled={isSending} value={text} onChange={e => setText(e.target.value)} /> <br /> <button disabled={isSending} type="submit" > Send </button> {isSending && <p>Sending...</p>} </form> ); } // Pretend to send a message. function sendMessage(text) { return new Promise(resolve => { setTimeout(resolve, 2000); }); }
Vaikka tämä koodi toimii, se jättää oven auki “mahdottomille” tiloille. Esimerkiksi, jos unohdat kutsua setIsSent
ja setIsSending
yhdessä, saatat joutua tilanteeseen, jossa isSending
ja isSent
ovat true
samanaikaisesti. Mitä monimutkaisempi komponenttisi on, sitä hankalampi on ymmärtää mitä tapahtui.
Kerta isSending
ja isSent
eivät voi olla true
samanaikaisesti, on parempi korvata ne yhdellä status
tilamuuttujalla, joka hoitaan yhden kolmesta kelvollisesta tilast: 'typing'
(oletus), 'sending'
, ja 'sent'
:
import { useState } from 'react'; export default function FeedbackForm() { const [text, setText] = useState(''); const [status, setStatus] = useState('typing'); async function handleSubmit(e) { e.preventDefault(); setStatus('sending'); await sendMessage(text); setStatus('sent'); } const isSending = status === 'sending'; const isSent = status === 'sent'; if (isSent) { return <h1>Thanks for feedback!</h1> } return ( <form onSubmit={handleSubmit}> <p>How was your stay at The Prancing Pony?</p> <textarea disabled={isSending} value={text} onChange={e => setText(e.target.value)} /> <br /> <button disabled={isSending} type="submit" > Send </button> {isSending && <p>Sending...</p>} </form> ); } // Pretend to send a message. function sendMessage(text) { return new Promise(resolve => { setTimeout(resolve, 2000); }); }
Voit silti määritellä vakoita luettavuutta varten:
const isSending = status === 'sending';
const isSent = status === 'sent';
Mutta ne eivät ole tilamuuttujia, joten sinun ei tarvitse huolehtia niiden joutumista ristiriitaan.
Vältä tarpeetonta tilaa
Jos voit laskea jotain tilaa kompoenntin propseista tai sen olemassa olevista tilamuuttujista renderöinnin aikana, sinun ei tulisi laittaa tätä tietoa komponentin tilaan.
Esimerkiksi, katso tätä lomaketta. Se toimii, mutta pystytkö löytämään tarpeetonta tilaa siitä?
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> </> ); }
Lomakkeessa on kolme tilamuuttujaa: firstName
, lastName
, ja fullName
. Kuitenkin, fullName
on tarpeeton. Voit aina laskea fullName
:n firstName
ja lastName
muuttujista renderöinnin aikana, joten poista se tilasta.
Tässä miten voit tehdä sen:
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ässä, fullName
ei ole tilamuuttuja. Sen sijaan se lasketaan renderöinnin aikana:
const fullName = firstName + ' ' + lastName;
Näin ollen Tapahtumankäsittelijöiden ei tarvitse tehdä mitään erityistä sen päivittämiseksi. Kun kutsut setFirstnName
tai setLastName
funktioita, kutsut uudelleenrenderöinnin, ja seuraavan kerran fullName
tullaan laskemaan uusiksi uuden datan pohjalta.
Syväsukellus
Yleinen esimerkki tarpeettomasta tilasta on seuraavanlainen koodi:
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
Tässä color
tilamuuttuja on alustettu messageColor
propin perusteella. Ongelma on, jos pääkomponentti palauttaa uuden arvon messageColor
propsille myöhemmin (esimerkiksi, 'red'
:n sijaan arvon 'blue'
), color
tilamuuttujaa ei tulla päivittämään! Tila alustetaan vain ensimmäisen renderöinnin aikana.
Tämän takia propsien “peilaaminen” tilamuuttujaan voi aiheuttaa hämmennystä. Sen sijaan käytä messageColor
propsia suoraan koodissasi. Jos haluat antaa sille lyhyemmän nimen, käytä vakiota:
function Message({ messageColor }) {
const color = messageColor;
Näin se ei pääse epäsynkronoitumaan yläkomponentilta välitetyn propsin kanssa.
Propsien “peilaaminen” tilaan on järkevää vain kun haluat välttää tietyn propsin kaikki tilamuutokset. Aloita propsin nimi periaatteen mukaan sanoilla initial
tai default
selventääksesi, että sen uuden arvot jätetään huomioimatta:
function Message({ initialColor }) {
// The `color` state variable holds the *first* value of `initialColor`.
// Further changes to the `initialColor` prop are ignored.
const [color, setColor] = useState(initialColor);
Vältä tilan toistoa
Tämä menu listakomponentti antaa sinun valita yhden matkaherkun useista vaihtoehdoista:
import { useState } from 'react'; const initialItems = [ { title: 'pretzels', id: 0 }, { title: 'crispy seaweed', id: 1 }, { title: 'granola bar', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedItem, setSelectedItem] = useState( items[0] ); return ( <> <h2>What's your travel snack?</h2> <ul> {items.map(item => ( <li key={item.id}> {item.title} {' '} <button onClick={() => { setSelectedItem(item); }}>Choose</button> </li> ))} </ul> <p>You picked {selectedItem.title}.</p> </> ); }
Tällä hetkellä, se tallentaa valitun kohteen oliona selectedItem
tilamuuttujaan: Kuitenkaan tämä ei ole hyvä: selectedItem
muuttujan sisältö on sama olio kuin yksi items
muuttujan sisällä. Tämä tarkoittaa, että tieto kohteesta itsestään on toistettuna kahteeen eri paikkaan.
Miksi tämä on ongelmallista? Tehdään kohteesta muokattava:
import { useState } from 'react'; const initialItems = [ { title: 'pretzels', id: 0 }, { title: 'crispy seaweed', id: 1 }, { title: 'granola bar', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedItem, setSelectedItem] = useState( items[0] ); function handleItemChange(id, e) { setItems(items.map(item => { if (item.id === id) { return { ...item, title: e.target.value, }; } else { return item; } })); } return ( <> <h2>What's your travel snack?</h2> <ul> {items.map((item, index) => ( <li key={item.id}> <input value={item.title} onChange={e => { handleItemChange(item.id, e) }} /> {' '} <button onClick={() => { setSelectedItem(item); }}>Choose</button> </li> ))} </ul> <p>You picked {selectedItem.title}.</p> </> ); }
Huomaa miten jos ensiksi klikkaat “Choose” kohteesta ja sitten muokkaat sitä, syöttökenttä päivittyy, mutta alhaalla oleva teksti ei vastaa muutoksia. Tämä tapahtuu, koska sinulla on toistettua tilaa, ja unohdit päivittää selectedItem
tilamuuttujaa.
Vaikka voisit päivittää selectedItem
muuttujaa myöskin, helpompi tapa korjata tämä on poistamalla toisto. Tässä esimerkissä, selectedItem
olion sijaan (joka luo toistoa items
muuttujan sisällä olevista kohteista) pidät selectedId
:n tilassa, ja sitten haet selectedItem
:n hakemalla items
taulusta kohteen, jolla on sama ID:
import { useState } from 'react'; const initialItems = [ { title: 'pretzels', id: 0 }, { title: 'crispy seaweed', id: 1 }, { title: 'granola bar', id: 2 }, ]; export default function Menu() { const [items, setItems] = useState(initialItems); const [selectedId, setSelectedId] = useState(0); const selectedItem = items.find(item => item.id === selectedId ); function handleItemChange(id, e) { setItems(items.map(item => { if (item.id === id) { return { ...item, title: e.target.value, }; } else { return item; } })); } return ( <> <h2>What's your travel snack?</h2> <ul> {items.map((item, index) => ( <li key={item.id}> <input value={item.title} onChange={e => { handleItemChange(item.id, e) }} /> {' '} <button onClick={() => { setSelectedId(item.id); }}>Choose</button> </li> ))} </ul> <p>You picked {selectedItem.title}.</p> </> ); }
(Vaihtoehtoisesti, voisit pitää valitun indeksin tilasa.)
Tila oli aiemmin monistettu näin:
items = [{ id: 0, title: 'pretzels'}, ...]
selectedItem = {id: 0, title: 'pretzels'}
Mutta muutosten jälkeen se on näin:
items = [{ id: 0, title: 'pretzels'}, ...]
selectedId = 0
Toisto on nyt poistettu ja pidät vain tarpeelliset asiat tilassa!
Nyt jos haluat muokata valittua kohdetta, viesti alhaalla päivittyy välittömästi. Tämä tapahtuu koska setItems
kutsuu uudelleenrenderöinnin ja items.find(...)
hakee kohteen päivitetyllä otsikolla. Sinun ei täytynyt pitää valittua kohdetta tilassa, koska vain valitun kohteen ID on tarpeellinen. Loput voidaan laskea renderöinnin aikana.
Vältä syvään sisennettyä tilaa
Kuvittele matkasuunnitelma, joka koostuu planeetoista, maanosista ja maista. Saatat haluta jäsentää tilan käyttämällä sisäkkäisiä objekteja ja taulukkoja, kuten tässä esimerkissä:
export const initialTravelPlan = { id: 0, title: '(Root)', childPlaces: [{ id: 1, title: 'Earth', childPlaces: [{ id: 2, title: 'Africa', childPlaces: [{ id: 3, title: 'Botswana', childPlaces: [] }, { id: 4, title: 'Egypt', childPlaces: [] }, { id: 5, title: 'Kenya', childPlaces: [] }, { id: 6, title: 'Madagascar', childPlaces: [] }, { id: 7, title: 'Morocco', childPlaces: [] }, { id: 8, title: 'Nigeria', childPlaces: [] }, { id: 9, title: 'South Africa', childPlaces: [] }] }, { id: 10, title: 'Americas', childPlaces: [{ id: 11, title: 'Argentina', childPlaces: [] }, { id: 12, title: 'Brazil', childPlaces: [] }, { id: 13, title: 'Barbados', childPlaces: [] }, { id: 14, title: 'Canada', childPlaces: [] }, { id: 15, title: 'Jamaica', childPlaces: [] }, { id: 16, title: 'Mexico', childPlaces: [] }, { id: 17, title: 'Trinidad and Tobago', childPlaces: [] }, { id: 18, title: 'Venezuela', childPlaces: [] }] }, { id: 19, title: 'Asia', childPlaces: [{ id: 20, title: 'China', childPlaces: [] }, { id: 21, title: 'India', childPlaces: [] }, { id: 22, title: 'Singapore', childPlaces: [] }, { id: 23, title: 'South Korea', childPlaces: [] }, { id: 24, title: 'Thailand', childPlaces: [] }, { id: 25, title: 'Vietnam', childPlaces: [] }] }, { id: 26, title: 'Europe', childPlaces: [{ id: 27, title: 'Croatia', childPlaces: [], }, { id: 28, title: 'France', childPlaces: [], }, { id: 29, title: 'Germany', childPlaces: [], }, { id: 30, title: 'Italy', childPlaces: [], }, { id: 31, title: 'Portugal', childPlaces: [], }, { id: 32, title: 'Spain', childPlaces: [], }, { id: 33, title: 'Turkey', childPlaces: [], }] }, { id: 34, title: 'Oceania', childPlaces: [{ id: 35, title: 'Australia', childPlaces: [], }, { id: 36, title: 'Bora Bora (French Polynesia)', childPlaces: [], }, { id: 37, title: 'Easter Island (Chile)', childPlaces: [], }, { id: 38, title: 'Fiji', childPlaces: [], }, { id: 39, title: 'Hawaii (the USA)', childPlaces: [], }, { id: 40, title: 'New Zealand', childPlaces: [], }, { id: 41, title: 'Vanuatu', childPlaces: [], }] }] }, { id: 42, title: 'Moon', childPlaces: [{ id: 43, title: 'Rheita', childPlaces: [] }, { id: 44, title: 'Piccolomini', childPlaces: [] }, { id: 45, title: 'Tycho', childPlaces: [] }] }, { id: 46, title: 'Mars', childPlaces: [{ id: 47, title: 'Corn Town', childPlaces: [] }, { id: 48, title: 'Green Hill', childPlaces: [] }] }] };
Oletetaan, että haluat lisätä painikkeen, jolla voit poistaa paikan, jossa olet jo käynyt. Miten se onnistuisi? Sisäkkäisen olion päivittäminen kopioi objektit aina muuttuneesta osasta ylöspäin. Syvällä olevan kohteen poistaminen edellyttäisi sen koko vanhemmuusketjun kopioimista. Tällainen koodi voi olla hyvin laajamittaista.
Jos tila on liian sisäkkäistä helposti päivitettäväksi, harkitse sen muuttamista “tasaiseksi”. Tässä on yksi tapa, jolla voit järjestää tämän datan. Puumaisen rakenteen sijaan, jossa kullakin place
:lla on taulukko sen alapaikoista, voit sen sijaan pitää hallussaan joukon alapaikkojen ID:itä. Tämän jälkeen voit tallentaa kunkin paikan ID:n vastaamaan tiettyä paikkaa
This data restructuring might remind you of seeing a database table:
export const initialTravelPlan = { 0: { id: 0, title: '(Root)', childIds: [1, 42, 46], }, 1: { id: 1, title: 'Earth', childIds: [2, 10, 19, 26, 34] }, 2: { id: 2, title: 'Africa', childIds: [3, 4, 5, 6 , 7, 8, 9] }, 3: { id: 3, title: 'Botswana', childIds: [] }, 4: { id: 4, title: 'Egypt', childIds: [] }, 5: { id: 5, title: 'Kenya', childIds: [] }, 6: { id: 6, title: 'Madagascar', childIds: [] }, 7: { id: 7, title: 'Morocco', childIds: [] }, 8: { id: 8, title: 'Nigeria', childIds: [] }, 9: { id: 9, title: 'South Africa', childIds: [] }, 10: { id: 10, title: 'Americas', childIds: [11, 12, 13, 14, 15, 16, 17, 18], }, 11: { id: 11, title: 'Argentina', childIds: [] }, 12: { id: 12, title: 'Brazil', childIds: [] }, 13: { id: 13, title: 'Barbados', childIds: [] }, 14: { id: 14, title: 'Canada', childIds: [] }, 15: { id: 15, title: 'Jamaica', childIds: [] }, 16: { id: 16, title: 'Mexico', childIds: [] }, 17: { id: 17, title: 'Trinidad and Tobago', childIds: [] }, 18: { id: 18, title: 'Venezuela', childIds: [] }, 19: { id: 19, title: 'Asia', childIds: [20, 21, 22, 23, 24, 25], }, 20: { id: 20, title: 'China', childIds: [] }, 21: { id: 21, title: 'India', childIds: [] }, 22: { id: 22, title: 'Singapore', childIds: [] }, 23: { id: 23, title: 'South Korea', childIds: [] }, 24: { id: 24, title: 'Thailand', childIds: [] }, 25: { id: 25, title: 'Vietnam', childIds: [] }, 26: { id: 26, title: 'Europe', childIds: [27, 28, 29, 30, 31, 32, 33], }, 27: { id: 27, title: 'Croatia', childIds: [] }, 28: { id: 28, title: 'France', childIds: [] }, 29: { id: 29, title: 'Germany', childIds: [] }, 30: { id: 30, title: 'Italy', childIds: [] }, 31: { id: 31, title: 'Portugal', childIds: [] }, 32: { id: 32, title: 'Spain', childIds: [] }, 33: { id: 33, title: 'Turkey', childIds: [] }, 34: { id: 34, title: 'Oceania', childIds: [35, 36, 37, 38, 39, 40, 41], }, 35: { id: 35, title: 'Australia', childIds: [] }, 36: { id: 36, title: 'Bora Bora (French Polynesia)', childIds: [] }, 37: { id: 37, title: 'Easter Island (Chile)', childIds: [] }, 38: { id: 38, title: 'Fiji', childIds: [] }, 39: { id: 40, title: 'Hawaii (the USA)', childIds: [] }, 40: { id: 40, title: 'New Zealand', childIds: [] }, 41: { id: 41, title: 'Vanuatu', childIds: [] }, 42: { id: 42, title: 'Moon', childIds: [43, 44, 45] }, 43: { id: 43, title: 'Rheita', childIds: [] }, 44: { id: 44, title: 'Piccolomini', childIds: [] }, 45: { id: 45, title: 'Tycho', childIds: [] }, 46: { id: 46, title: 'Mars', childIds: [47, 48] }, 47: { id: 47, title: 'Corn Town', childIds: [] }, 48: { id: 48, title: 'Green Hill', childIds: [] } };
Nyt kun tila on “tasainen” (tunnetaan myös nimellä “normalisoitu”), sisennettyjen kohteiden päivittäminen on helpompaa.
Kun haluat poistaa paikan, sinun tarvitsee vain päivittää kaksi tasoa tilasta:
- Päivitetyn version pääkohteesta tulisi poistaa poistettu ID sen
childIds
taulukosta. - Päivitetyn version juuri “taulukko” olio tulisi päivittää sisällyttämään päivitetty versio sen pääkohteesta.
Tässä on esimerkki siitä, miten voisit toimia:
import { useState } from 'react'; import { initialTravelPlan } from './places.js'; export default function TravelPlan() { const [plan, setPlan] = useState(initialTravelPlan); function handleComplete(parentId, childId) { const parent = plan[parentId]; // Create a new version of the parent place // that doesn't include this child ID. const nextParent = { ...parent, childIds: parent.childIds .filter(id => id !== childId) }; // Update the root state object... setPlan({ ...plan, // ...so that it has the updated parent. [parentId]: nextParent }); } const root = plan[0]; const planetIds = root.childIds; return ( <> <h2>Places to visit</h2> <ol> {planetIds.map(id => ( <PlaceTree key={id} id={id} parentId={0} placesById={plan} onComplete={handleComplete} /> ))} </ol> </> ); } function PlaceTree({ id, parentId, placesById, onComplete }) { const place = placesById[id]; const childIds = place.childIds; return ( <li> {place.title} <button onClick={() => { onComplete(parentId, id); }}> Complete </button> {childIds.length > 0 && <ol> {childIds.map(childId => ( <PlaceTree key={childId} id={childId} parentId={id} placesById={placesById} onComplete={onComplete} /> ))} </ol> } </li> ); }
Voit sisentää tilaa niin paljon kuin haluat, mutta sen muuttaminen “tasaiseksi” voi ratkaista useita ongelmia. Se tekee tilan päivittämisestä helpompaa ja auttaa varmistamaan, että tilassa ei ole toistoa eri tasojen välillä.
Syväsukellus
Ihannetapauksessa poistaisit myös poistetut kohteet (ja niiden lapset!) “taulukko” oliosta muistin parantamiseksi. Tämä versio tekee myös tämän. Se myös käyttää Immeriä tehdäkseen tilan päivityslogiikan tiiviimmäksi.
import { useImmer } from 'use-immer'; import { initialTravelPlan } from './places.js'; export default function TravelPlan() { const [plan, updatePlan] = useImmer(initialTravelPlan); function handleComplete(parentId, childId) { updatePlan(draft => { // Remove from the parent place's child IDs. const parent = draft[parentId]; parent.childIds = parent.childIds .filter(id => id !== childId); // Forget this place and all its subtree. deleteAllChildren(childId); function deleteAllChildren(id) { const place = draft[id]; place.childIds.forEach(deleteAllChildren); delete draft[id]; } }); } const root = plan[0]; const planetIds = root.childIds; return ( <> <h2>Places to visit</h2> <ol> {planetIds.map(id => ( <PlaceTree key={id} id={id} parentId={0} placesById={plan} onComplete={handleComplete} /> ))} </ol> </> ); } function PlaceTree({ id, parentId, placesById, onComplete }) { const place = placesById[id]; const childIds = place.childIds; return ( <li> {place.title} <button onClick={() => { onComplete(parentId, id); }}> Complete </button> {childIds.length > 0 && <ol> {childIds.map(childId => ( <PlaceTree key={childId} id={childId} parentId={id} placesById={placesById} onComplete={onComplete} /> ))} </ol> } </li> ); }
Joskus voit myös vähentää tilan sisennystä siirtämällä tilan osia sen alakomponentteihin. Tämä toimii hyvin lyhytaikaiseen käyttöliittymätilaan, jota ei tarvitse tallentaa, kuten jos kursori on kohteen päällä.
Kertaus
- Jos kaksi tilamuuttujaa päivittyvät yhdessä, harkitse niiden yhdistämistä yhdeksi.
- Valitse tilamuuttujasi huolellisesti välttääksesi “mahdottomia” tiloja.
- Järjestä tilasi tavalla, joka vähentää mahdollisuuksiasi tehdä ongelmia sitä päivittäessä.
- Vältä toistuvaa ja turhaa tilaa, jotta sitä ei tarvitse pitää synkronoituna.
- Älä välitä propseja suoraan tilaan, ellet nimenomaan halua välttää päivityksiä.
- Käyttöliittymämalleihin kuten valintoihin, pidä ID tai indeksi tilassa koko kohteen olion sijaan.
- Jos syvään sisäkkäisen olion päivittäminen on sekavaa, kokeile sen tasaamista.
Haaste 1 / 4: Korjaa komponentti, joka ei päivity
Tämä Clock
komponentti vastaanottaa kaksi propsia: color
ja time
. Kun valitset eri värin valintaruudusta, Clock
komponentti vastaanottaa eri color
propsin sen pääkomponentilta. Kuitenkin jostain syystä näytetty väri ei päivity. Miksi? Korjaa ongelma.
import { useState } from 'react'; export default function Clock(props) { const [color, setColor] = useState(props.color); return ( <h1 style={{ color: color }}> {props.time} </h1> ); }