Man mano che la tua applicazione cresce, è utile essere più intenzionali su come viene organizzato il tuo state e come i dati fluiscono tra i tuoi componenti. Lo state ridondante o duplicato è una fonte comune di bug. In questo capitolo, imparerai come strutturare bene il tuo state, come mantenere la logica di aggiornamento dello state manutenibile e come condividere lo state tra componenti distanti.
In questo capitolo
- Come pensare ai cambiamenti della UI come cambiamenti dello state
- Come strutturare bene lo state
- Come “alzare lo state” per condividerlo tra i componenti
- Come controllare se lo state viene preservato o ripristinato
- Come consolidare la logica complessa dello state in una funzione
- Come passare informazioni senza “prop drilling”
- Come scalare la gestione dello state man mano che l’applicazione cresce
Reagire all’input con lo state
Con React, non modificherai direttamente l’interfaccia utente dal codice. Ad esempio, non scriverai comandi come “disabilita il pulsante”, “abilita il pulsante”, “mostra il messaggio di successo”, ecc. Invece, descriverai l’interfaccia utente che desideri vedere per i diversi stati visivi del tuo componente (“state iniziale”, “state di digitazione”, “state di successo”) e quindi attiverai i cambiamenti di state in risposta all’input dell’utente. Questo è simile al modo in cui i designer pensano all’interfaccia utente.
Ecco un modulo di quiz costruito con React. Nota come utilizza la variabile di state status
per determinare se abilitare o disabilitare il pulsante di invio e se mostrare invece il messaggio di successo.
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); }); }
Ready to learn this topic?
Leggi Reagire all’input con lo state per imparare come affrontare le interazioni con una mentalità basata sullo state.
Read MoreScelta della struttura dello state
Strutturare bene lo state può fare la differenza tra un componente piacevole da modificare e debuggare e uno che è una fonte costante di bug. Il principio più importante è che lo state non dovrebbe contenere informazioni ridondanti o duplicate. Se c’è state non necessario, è facile dimenticarsi di aggiornarlo e introdurre bug!
Ad esempio, questo modulo ha una variabile di state fullName
ridondante :
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> </> ); }
Puoi rimuoverla e semplificare il codice calcolando fullName
durante il rendering del componente:
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> </> ); }
Questo potrebbe essere un cambiamento di poco conto, ma molti bug nella applicazioni React vengono risolti in questo modo.
Ready to learn this topic?
Leggi Scegliere la struttura dello state per imparare come progettare la forma dello state per evitare bug.
Read MoreCondivisione dello state tra i componenti
A volte, desideri che lo state di due componenti cambi sempre insieme. Per farlo, rimuovi lo state da entrambi i componenti, spostalo al loro genitore comune più vicino e quindi passalo loro tramite le props. Questo è noto come “alzare lo state” ed è una delle cose più comuni che farai scrivendo codice React.
In questo esempio, solo un pannello dovrebbe essere attivo alla volta. Per ottenere questo risultato, anziché mantenere lo state attivo all’interno di ciascun pannello individuale, il componente genitore tiene traccia dello state e specifica le props per i suoi figli.
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> ); }
Ready to learn this topic?
Leggi Condivisione dello State tra i Componenti per imparare come alzare lo state e mantenere i componenti sincronizzati.
Read MorePreservazione e ripristino dello state
Quando si ridisegna un componente, React deve decidere quali parti dell’albero mantenere (e aggiornare) e quali parti scartare o ricreare da zero. Nella maggior parte dei casi, il comportamento automatico di React funziona abbastanza bene. Per impostazione predefinita, React conserva le parti dell’albero che “corrispondono” all’albero dei componenti precedentemente renderizzato.
Tuttavia, a volte questo non è ciò che si desidera. In questa chat app, digitare un messaggio e quindi passare a un altro destinatario non resetta l’input. Ciò può portare l’utente a inviare accidentalmente un messaggio alla persona sbagliata:
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' } ];
React ti consente di sovrascrivere il comportamento predefinito e forzare un componente a ripristinare il suo state passando una key
, diversa, ad esempio <Chat key={email} />
. Ciò indica a React che, se il destinatario è diverso, il componente Chat
deve essere considerato un componente diverso e che deve essere ricreato da zero con i nuovi dati (e l’interfaccia utente come gli input). Ora, passando da un destinatario all’altro, l’input viene ripristinato, anche se si sta renderizzando lo stesso componente.
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' } ];
Ready to learn this topic?
Leggi Preservare e Reimpostare lo State per apprendere la durata dello state e come controllarlo.
Read MoreEstrarre la logica dello state in un reducer
I componenti con molte aggiornamenti dello state distribuiti su numerosi gestori di eventi possono diventare complessi. Per questi casi, puoi consolidare tutta la logica di aggiornamento dello state al di fuori del tuo componente in una singola funzione, chiamata “riduttore” (reducer). I tuoi gestori di eventi diventano concisi perché specificano solo le “azioni” dell’utente. In fondo al file, la funzione del riduttore specifica come lo state dovrebbe aggiornarsi in risposta a ciascuna azione!
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 } ];
Ready to learn this topic?
Leggi Estrarre la Logica dello State in un Reducer per apprendere come consolidare la logica nel reducer della funzione.
Read MorePassaggio di dati in profondità con il context
Di solito, passerai le informazioni da un componente genitore a un componente figlio tramite le props. Tuttavia, il passaggio delle props può diventare scomodo se devi passare una determinata prop attraverso molti componenti o se molti componenti necessitano delle stesse informazioni. Il context consente al componente genitore di rendere disponibili alcune informazioni a tutti i componenti nell’albero sottostante, indipendentemente dalla profondità, senza passarle esplicitamente tramite le props.
In questo esempio, il componente Heading
determina il livello del titolo “chiedendo” al componente Section
più vicino il suo livello. Ogni Section
tiene traccia del proprio livello chiedendo al Section
genitore e aggiungendo uno. Ogni Section
fornisce informazioni a tutti i componenti sottostanti senza passare le props in modo esplicito: lo fa attraverso il context.
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> ); }
Ready to learn this topic?
Leggi Passaggio di Dati in Profondità con il Context per apprendere come utilizzare il context come alternativa al passaggio di props.
Read MoreScalare con reducer e context
I reducer ti permettono di consolidare la logica di aggiornamento dello state di un componente. Il contesto ti permette di passare informazioni in profondità ad altri componenti. Puoi combinare i reducer e il context insieme per gestire lo state di una schermata complessa.
Con questo approccio, un componente genitore con uno state complesso lo gestisce con un riduttore. Altri componenti situati in qualsiasi punto dell’albero possono leggere il suo state tramite il contesto. Possono anche inviare azioni per aggiornare tale state.
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> ); }
Ready to learn this topic?
Leggi Scalare con Reducer e Context per apprendere come la gestione dello state si scala in un’applicazione in crescita.
Read MoreCosa fare dopo?
Vai su Reacting to Input with State per iniziare a leggere questo capitolo pagina per pagina!
Oppure, se sei già familiare con questi argomenti, perché non leggere su Soluzioni alternative?