随着应用程序的增长,有必要更加刻意地组织状态以及数据如何在组件之间流动。冗余或重复的状态是错误的常见来源。在本章中,您将学习如何很好地构建状态、如何保持状态更新逻辑的可维护性以及如何在远程组件之间共享状态。
本章内容
使用状态响应输入
使用 React,你不会直接从代码中修改 UI。例如,你不会编写诸如“禁用按钮”,“启用按钮”,“显示成功消息”之类的命令。相反,你将描述你希望看到 UI 在组件的不同视觉状态下的情况(“初始状态”,“输入状态”,“成功状态”),然后根据用户输入触发状态更改。这类似于设计师如何思考 UI。
这是使用 React 构建的测验表单。请注意,它如何使用 status
状态变量来确定是启用还是禁用提交按钮,以及是显示成功消息还是显示其他信息。
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); }); }
选择状态结构
构建良好的状态结构可以决定一个组件是容易修改和调试还是不断出现错误。最重要的原则是状态不应该包含冗余或重复的信息。如果存在不必要的状态,很容易忘记更新它并引入错误!
例如,此表单具有 冗余 fullName
状态变量
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> </> ); }
你可以删除它并通过在组件渲染时计算 fullName
来简化代码
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> </> ); }
这看起来可能是一个很小的改变,但 React 应用程序中的许多错误都是通过这种方式修复的。
在组件之间共享状态
有时,你希望两个组件的状态始终一起改变。为此,请从这两个组件中删除状态,将其移动到它们最近的共同父组件,然后通过道具将其传递给它们。这被称为“向上提升状态”,这是编写 React 代码时最常见的事情之一。
在此示例中,一次只能有一个面板处于活动状态。为了实现这一点,而不是将活动状态保留在每个单独的面板内,父组件持有状态并指定其子组件的道具。
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> ); }
保留和重置状态
当你重新渲染一个组件时,React 需要决定树的哪些部分需要保留(并更新),以及哪些部分需要丢弃或从头开始重新创建。在大多数情况下,React 的自动行为已经足够好了。默认情况下,React 会保留与之前渲染的组件树“匹配”的树的部分。
但是,有时这不是你想要的。在这个聊天应用程序中,输入消息然后切换收件人不会重置输入。这会导致用户意外向错误的人发送消息。
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: '[email protected]' }, { name: 'Alice', email: '[email protected]' }, { name: 'Bob', email: '[email protected]' } ];
React 允许你覆盖默认行为,并通过传递不同的 key
(如 <Chat key={email} />
)来强制组件重置其状态。这告诉 React,如果收件人不同,则应将其视为一个不同的 Chat
组件,需要使用新数据(以及 UI,如输入)从头开始重新创建。现在,在收件人之间切换会重置输入字段,即使你渲染的是同一个组件。
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: '[email protected]' }, { name: 'Alice', email: '[email protected]' }, { name: 'Bob', email: '[email protected]' } ];
将状态逻辑提取到 reducer 中
拥有许多跨越多个事件处理程序的状态更新的组件可能会变得难以管理。对于这些情况,您可以在组件之外的单个函数(称为“reducer”)中合并所有状态更新逻辑。您的事件处理程序变得简洁,因为它们只指定用户“操作”。在文件底部,reducer 函数指定状态应该如何响应每个操作进行更新!
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 } ];
使用上下文深度传递数据
通常,您将通过 props 将信息从父组件传递到子组件。但是,如果您需要将某个 prop 传递给多个组件,或者多个组件需要相同的信息,则传递 props 可能会变得不方便。上下文允许父组件将某些信息提供给树中其下方的任何组件(无论它有多深)——无需通过 props 明确传递它。
在这里,Heading
组件通过“询问”最近的 Section
确定其标题级别。每个 Section
通过询问父 Section
并加 1 来跟踪它自己的级别。每个 Section
都向其下方的所有组件提供信息,而无需传递 props——它是通过上下文实现的。
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> ); }
使用 reducer 和上下文进行扩展
Reducers 允许您合并组件的状态更新逻辑。上下文允许您将信息深度传递到其他组件。您可以将 reducers 和上下文结合起来以管理复杂屏幕的状态。
使用这种方法,具有复杂状态的父组件通过 reducer 来管理它。树中任何深处的其他组件都可以通过上下文读取其状态。他们还可以调度操作以更新该状态。
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> ); }