react状态管理之state

第三章 - 状态管理

随着你的应用不断变大,更有意识的去关注应用状态如何组织,以及数据如何在组件之间流动会对你很有帮助。冗余或重复的状态往往是缺陷的根源。在本节中,你将学习如何组织好状态,如何保持状态更新逻辑的可维护性,以及如何跨组件共享状态。

用state 响应输入

使用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>答对了!</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>城市测验</h2><p>哪个城市有把空气变成饮用水的广告牌?</p><form onSubmit={handleSubmit}><textareavalue={answer}onChange={handleTextareaChange}disabled={status === 'submitting'}/><br /><button disabled={answer.length === 0 ||status === 'submitting'}>提交</button>{error !== null &&<p className="Error">{error.message}</p>}</form></>);
}function submitForm(answer) {// 模拟接口请求return new Promise((resolve, reject) => {setTimeout(() => {let shouldError = answer.toLowerCase() !== 'lima'if (shouldError) {reject(new Error('猜的不错,但答案不对。再试试看吧!'));} else {resolve();}}, 1500);});
}

声明式地考虑UI

你已经从上面的例子看到如何去实现一个表单了,为了更好地理解如何在 React 中思考,接下来你将会学到如何用 React 重新实现这个 UI:

  1. 定位你的组件中不同的视图状态
  2. 确定是什么触发了这些 state 的改变
  3. 表示内存中的 state(需要使用 useState
  4. 删除任何不必要的 state 变量
  5. 连接事件处理函数去设置 state
步骤1:定位组件中不同的视图状态

在计算机科学中,你或许听过可处于多种“状态”之一的 “状态机”。如果你有与设计师一起工作,那么你可能已经见过不同“视图状态”的模拟图。正因为 React 站在设计与计算机科学的交点上,因此这两种思想都是灵感的来源。

首先,你需要去可视化 UI 界面中用户可能看到的所有不同的“状态”:

  • 无数据:表单有一个不可用状态的“提交”按钮。
  • 输入中:表单有一个可用状态的“提交”按钮。
  • 提交中:表单完全处于不可用状态,加载动画出现。
  • 成功时:显示“成功”的消息而非表单。
  • 错误时:与输入状态类似,但会多错误的消息。

像一个设计师一样,你会想要在你添加逻辑之前去“模拟”不同的状态或创建“模拟状态”。例如下面的例子,这是一个对表单可视部分的模拟。这个模拟被一个 status 的属性控制,并且这个属性的默认值为 empty

export default function Form({status = 'empty'
}) {if (status === 'success') {return <h1>That's right!</h1>}return (<><h2>City quiz</h2><p>In which city is there a billboard that turns air into drinkable water?</p><form><textarea /><br /><button>Submit</button></form></>)
}

你可以随意命名这个属性,名字并不重要。试着将 status = 'empty' 改为 status = 'success',然后你就会看到成功的信息出现。模拟可以让你在书写逻辑前快速迭代 UI。这是同一组件的一个更加充实的原型,仍然由 status 属性“控制”:

export default function Form({// Try 'submitting', 'error', 'success':status = 'empty'
}) {if (status === 'success') {return <h1>That's right!</h1>}return (<><h2>City quiz</h2><p>In which city is there a billboard that turns air into drinkable water?</p><form><textarea disabled={status === 'submitting'} /><br /><button disabled={status === 'empty' ||status === 'submitting'}>Submit</button>{status === 'error' &&<p className="Error">Good guess but a wrong answer. Try again!</p>}</form></>);
}
深入探讨 - 同时展示大量的视图状态

如果一个组件有多个视图状态,你可以很方便地将它们展示在一个页面中:

import Form from './Form.js';let statuses = ['empty','typing','submitting','success','error',
];export default function App() {return (<>{statuses.map(status => (<section key={status}><h4>Form ({status}):</h4><Form status={status} /></section>))}</>);
}// Form.js
export default function Form({ status }) {if (status === 'success') {return <h1>That's right!</h1>}return (<form><textarea disabled={status === 'submitting'} /><br /><button disabled={status === 'empty' ||status === 'submitting'}>Submit</button>{status === 'error' &&<p className="Error">Good guess but a wrong answer. Try again!</p>}</form>);
}
步骤2: 确定是什么触发了这些状态的改变

你可以触发 state 的更新来响应两种输入:

  • 人为输入。比如点击按钮、在表单中输入内容,或导航到链接。
  • 计算机输入。比如网络请求得到反馈、定时器被触发,或加载一张图片。

以上两种情况中,你必须设置 state 变量 去更新 UI。对于正在开发中的表单来说,你需要改变 state 以响应几个不同的输入:

  • 改变输入框中的文本时(人为)应该根据输入框的内容是否是空值,从而决定将表单的状态从空值状态切换到输入中或切换回原状态。
  • 点击提交按钮时(人为)应该将表单的状态切换到提交中的状态。
  • 网络请求成功后(计算机)应该将表单的状态切换到成功的状态。
  • 网络请求失败后(计算机)应该将表单的状态切换到失败的状态,与此同时,显示错误信息。
步骤3:通过 useState 表示内存中的 state

接下来你会需要在内存中通过 useState 表示组件中的视图状态。诀窍很简单:state 的每个部分都是“处于变化中的”,并且你需要让“变化的部分”尽可能的少。更复杂的程序会产生更多 bug!

先从绝对必须存在的状态开始。例如,你需要存储输入的 answer 以及用于存储最后一个错误的 error (如果存在的话):

const [answer, setAnswer] = useState('');const [error, setError] = useState(null);

接下来,你需要一个状态变量来代表你想要显示的那个可视状态。通常有多种方式在内存中表示它,因此你需要进行实验。

如果你很难立即想出最好的办法,那就先从添加足够多的 state 开始,确保所有可能的视图状态都囊括其中:

const [isEmpty, setIsEmpty] = useState(true);const [isTyping, setIsTyping] = useState(false);const [isSubmitting, setIsSubmitting] = useState(false);const [isSuccess, setIsSuccess] = useState(false);const [isError, setIsError] = useState(false);

你最初的想法或许不是最好的,但是没关系,重构 state 也是步骤中的一部分!

步骤4:删除任何不必要的state变量

你会想要避免 state 内容中的重复,从而只需要关注那些必要的部分。花一点时间来重构你的 state 结构,会让你的组件更容易被理解,减少重复并且避免歧义。你的目的是防止出现在内存中的 state 不代表任何你希望用户看到的有效 UI 的情况。(比如你绝对不会想要在展示错误信息的同时禁用掉输入框,导致用户无法纠正错误!)

这有一些你可以问自己的, 关于 state 变量的问题:

  • 这个 state 是否会导致矛盾?例如,isTypingisSubmitting 的状态不能同时为 true。矛盾的产生通常说明了这个 state 没有足够的约束条件。两个布尔值有四种可能的组合,但是只有三种对应有效的状态。为了将“不可能”的状态移除,你可以将他们合并到一个 'status' 中,它的值必须是 'typing''submitting' 以及 'success' 这三个中的一个。
  • 相同的信息是否已经在另一个 state 变量中存在?另一个矛盾:isEmptyisTyping 不能同时为 true。通过使它们成为独立的 state 变量,可能会导致它们不同步并导致 bug。幸运的是,你可以移除 isEmpty 转而用 message.length === 0
  • 你是否可以通过另一个 state 变量的相反值得到相同的信息isError 是多余的,因为你可以检查 error !== null

在清理之后,你只剩下 3 个(从原本的 7 个!)必要的 state 变量:

const [answer, setAnswer] = useState('');const [error, setError] = useState(null);const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'

正是因为你不能在不破坏功能的情况下删除其中任何一个状态变量,因此你可以确定这些都是必要的。

步骤5:连接事件处理函数以设置 state

最后,创建事件处理函数去设置 state 变量。下面是绑定好事件的最终表单:

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}><textareavalue={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);});
}
摘要
  • 声明式编程意味着为每个视图状态声明 UI 而非细致地控制 UI(命令式)。
  • 当开发一个组件时:
    1. 写出你的组件中所有的视图状态。
    2. 确定是什么触发了这些 state 的改变。
    3. 通过 useState 模块化内存中的 state。
    4. 删除任何不必要的 state 变量。
    5. 连接事件处理函数去设置 state。

选择 state 结构

构建良好的state可以让组件变得更易于修改和调试,而不会经常出错。以下是你在构建state时应该考虑的一些建议

构建state的原则

当你编写一个存有state的组件时,你需要选择使用多少个state变量以及它们都是怎么样的数据格式。尽管选择次优的state结构下也可以编写正确的程序,但有几个原则可以指导您做出更好的决策:

  1. 合并关联的state。如果你总是同时更新两个或更多的state变量,请考虑将它们合并为一个单独的state变量。
  2. 避免互相矛盾的state。当state结构中存在多个互相矛盾的state时,你就可能为此留下隐患,应尽量避免这种情况。
  3. 避免冗余的state。如果你能在渲染期间从组件的props或其现有的state变量中计算出一些信息,则不应该将这些信息放入该组件的state中。
  4. 避免重复的state。当同一数据在多个state变量之间或在多个嵌套对象中重复时,这会很难保持它们同步。应该尽可能减少重复
  5. 避免深度嵌套的state。深度分层的state更新起来不是很方便。如果可能的话,最好扁平化方式构建state。

这些原则背后的目标是使得state易于更新而不引入错误。从 state 中删除冗余和重复数据有助于确保所有部分保持同步。这类似于数据库工程师想要 “规范化”数据库结构,以减少出现错误的机会。用爱因斯坦的话说,“让你的状态尽可能简单,但不要过于简单。”

现在让我们来看看这些原则在实际中是如何应用的。

合并关联的state

有时候你可能会不确定是使用单个 state 变量还是多个 state 变量。

你会像下面这样做吗?

const [x, setX] = useState(0);const [y, setY] = useState(0);

或这样?

const [position, setPosition] = useState({ x: 0, y: 0 });

从技术上讲,你可以使用其中任何一种方法。但是,如果某两个 state 变量总是一起变化,则将它们统一成一个 state 变量可能更好。这样你就不会忘记让它们始终保持同步,就像下面这个例子中,移动光标会同时更新红点的两个坐标:

import { useState } from 'react';export default function MovingDot() {const [position, setPosition] = useState({x: 0,y: 0});return (<divonPointerMove={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>)
}

另一种情况是,你将数据整合到一个对象或一个数组中时,你不知道需要多少个state片段。例如,当你有一个用户以添加自定义字段的表单时,这将会很有帮助

陷阱

如果你的 state 变量是一个对象时,请记住,你不能只更新其中的一个字段 而不显式复制其他字段。例如,在上面的例子中,你不能写成 setPosition({ x: 100 }),因为它根本就没有 y 属性! 相反,如果你想要仅设置 x,则可执行 setPosition({ ...position, x: 100 }),或将它们分成两个 state 变量,并执行 setX(100)

避免矛盾的state

下面是带有isSending 和 isSent 两个state变量的酒店反馈表单:

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><textareadisabled={isSending}value={text}onChange={e => setText(e.target.value)}/><br /><buttondisabled={isSending}type="submit">Send</button>{isSending && <p>Sending...</p>}</form>);
}// 假装发送一条消息。
function sendMessage(text) {return new Promise(resolve => {setTimeout(resolve, 2000);});
}

尽管这段代码是有效的,但也会让一些 state “极难处理”。例如,如果你忘记同时调用 setIsSentsetIsSending,则可能会出现 isSendingisSent 同时为 true 的情况。你的组件越复杂,你就越难理解发生了什么。

因为 isSendingisSent 不应同时为 true,所以最好用一个 status 变量来代替它们,这个 state 变量可以采取三种有效状态其中之一'typing' (初始), 'sending', 和 '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><textareadisabled={isSending}value={text}onChange={e => setText(e.target.value)}/><br /><buttondisabled={isSending}type="submit">Send</button>{isSending && <p>Sending...</p>}</form>);
}// 假装发送一条消息。
function sendMessage(text) {return new Promise(resolve => {setTimeout(resolve, 2000);});
}

你仍然可以声明一些常量,以提高可读性:

const isSending = status === 'sending';const isSent = status === 'sent';

但它们不是 state 变量,所以你不必担心它们彼此失去同步。

避免冗余state

如果你能在渲染期间从组件的 props 或其现有的 state 变量中计算出一些信息,则不应该把这些信息放到该组件的 state 中。

例如,以这个表单为例。它可以运行,但你能找到其中任何冗余的 state 吗?

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:{' '}<inputvalue={firstName}onChange={handleFirstNameChange}/></label><label>Last name:{' '}<inputvalue={lastName}onChange={handleLastNameChange}/></label><p>Your ticket will be issued to: <b>{fullName}</b></p></>);
}

这个表单有三个 state 变量:firstNamelastNamefullName。然而,fullName 是多余的。在渲染期间,你始终可以从 firstNamelastName 中计算出 fullName,因此需要把它从 state 中删除。

你可以这样做:

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:{' '}<inputvalue={firstName}onChange={handleFirstNameChange}/></label><label>Last name:{' '}<inputvalue={lastName}onChange={handleLastNameChange}/></label><p>Your ticket will be issued to: <b>{fullName}</b></p></>);
}

这里的 fullName 不是 一个 state 变量。相反,它是在渲染期间中计算出的:

const fullName = firstName + ' ' + lastName;

因此,更改处理程序不需要做任何特殊操作来更新它。当你调用 setFirstNamesetLastName 时,你会触发一次重新渲染,然后下一个 fullName 将从新数据中计算出来。

深入讨论 - 不要在state中镜像 props

以下代码是体现 state 冗余的一个常见例子:

function Message({ messageColor }) {const [color, setColor] = useState(messageColor);

这里,一个 color state 变量被初始化为 messageColor 的 prop 值。这段代码的问题在于,如果父组件稍后传递不同的 messageColor 值(例如,将其从 'blue' 更改为 'red'),则 color state 变量将不会更新! state 仅在第一次渲染期间初始化。

这就是为什么在 state 变量中,“镜像”一些 prop 属性会导致混淆的原因。相反,你要在代码中直接使用 messageColor 属性。如果你想给它起一个更短的名称,请使用常量:

function Message({ messageColor }) {const color = messageColor;

这种写法就不会与从父组件传递的属性失去同步。

只有当你 想要 忽略特定 props 属性的所有更新时,将 props “镜像”到 state 才有意义。按照惯例,prop 名称以 initialdefault 开头,以阐明该 prop 的新值将被忽略:

function Message({ initialColor }) {// 这个 `color` state 变量用于保存 `initialColor` 的 **初始值**。// 对于 `initialColor` 属性的进一步更改将被忽略。const [color, setColor] = useState(initialColor);
避免重复的state

下面这个菜单列表组件可以让你在多种旅行小吃中选择一个:

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></>);
}

当前,它将所选元素作为对象存储在 selectedItem state变量中。然而,这并不好:selectedItem 的内容与 items列表中的某个项是同一个对象。这意味这关于该项本身的信息在两个地方产生了重复。

为什么这是个问题?让我们使每个项目都可以编辑:

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}><inputvalue={item.title}onChange={e => {handleItemChange(item.id, e)}}/>{' '}<button onClick={() => {setSelectedItem(item);}}>Choose</button></li>))}</ul><p>You picked {selectedItem.title}.</p></>);
}

请注意,如果你首先单击菜单上的“Choose” 然后 编辑它,输入会更新,但底部的标签不会反映编辑内容。 这是因为你有重复的 state,并且你忘记更新了 selectedItem

尽管你也可以更新 selectedItem,但更简单的解决方法是消除重复项。在下面这个例子中,你将 selectedId 保存在 state 中,而不是在 selectedItem 对象中(它创建了一个与 items 内重复的对象),然后 通过搜索 items 数组中具有该 ID 的项,以此获取 selectedItem

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}><inputvalue={item.title}onChange={e => {handleItemChange(item.id, e)}}/>{' '}<button onClick={() => {setSelectedId(item.id);}}>Choose</button></li>))}</ul><p>You picked {selectedItem.title}.</p></>);
}

state 过去常常是这样复制的:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedItem = {id: 0, title: 'pretzels'}

改了之后是这样的:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedId = 0

重复的 state 没有了,你只保留了必要的 state!

现在,如果你编辑 selected 元素,下面的消息将立即更新。这是因为 setItems 会触发重新渲染,而 items.find(...) 会找到带有更新文本的元素。你不需要在 state 中保存 选定的元素,因为只有 选定的 ID 是必要的。其余的可以在渲染期间计算。

避免深度嵌套 的state

想象一下,一个由行星、大陆和国家组成的旅行计划。你可能会尝试使用嵌套对象和数组来构建它的 state,就像下面这个例子:

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: []      }]}]
};

现在,假设你想添加一个按钮来删除一个你已经去过的地方。你会怎么做呢?更新嵌套的 state 需要从更改部分一直向上复制对象。删除一个深度嵌套的地点将涉及复制其整个父级地点链。这样的代码可能非常冗长。

如果 state 嵌套太深,难以轻松更新,可以考虑将其“扁平化”。 这里有一个方法可以重构上面这个数据。不同于树状结构,每个节点的 place 都是一个包含 其子节点 的数组,你可以让每个节点的 place 作为数组保存 其子节点的 ID。然后存储一个节点 ID 与相应节点的映射关系。

这个数据重组可能会让你想起看到一个数据库表

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: []}
};

现在 state 已经“扁平化”(也称为“规范化”),更新嵌套项会变得更加容易。

现在要删除一个地点,您只需要更新两个 state 级别:

  • 父级 地点的更新版本应该从其 childIds 数组中排除已删除的 ID。
  • 其根级“表”对象的更新版本应包括父级地点的更新版本。

下面是展示如何处理它的一个示例:

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];// 创建一个其父级地点的新版本// 但不包括子级 ID。const nextParent = {...parent,childIds: parent.childIds.filter(id => id !== childId)};// 更新根 state 对象...setPlan({...plan,// ...以便它拥有更新的父级。[parentId]: nextParent});}const root = plan[0];const planetIds = root.childIds;return (<><h2>Places to visit</h2><ol>{planetIds.map(id => (<PlaceTreekey={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 => (<PlaceTreekey={childId}id={childId}parentId={id}placesById={placesById}onComplete={onComplete}/>))}</ol>}</li>);
}

你确实可以随心所欲地嵌套 state,但是将其“扁平化”可以解决许多问题。这使得 state 更容易更新,并且有助于确保在嵌套对象的不同部分中没有重复。有时候,你也可以通过将一些嵌套 state 移动到子组件中来减少 state 的嵌套。这对于不需要保存的短暂 UI 状态非常有效,比如一个选项是否被悬停。

摘要
  • 如果两个 state 变量总是一起更新,请考虑将它们合并为一个。
  • 仔细选择你的 state 变量,以避免创建“极难处理”的 state。
  • 用一种减少出错更新的机会的方式来构建你的 state。
  • 避免冗余和重复的 state,这样您就不需要保持同步。
  • 除非您特别想防止更新,否则不要将 props 放入 state 中。
  • 对于选择类型的 UI 模式,请在 state 中保存 ID 或索引而不是对象本身。
  • 如果深度嵌套 state 更新很复杂,请尝试将其展开扁平化。

在组件间共享状态

有时候,你希望两个组件的状态始终同步更改。要实现这一点,你可以将相关state从这两个组件上移除,并把state放到它们的公共父级,在通过props将state传递给这两个组件。这被称为“状态提升”,这是编写 React 代码时常做的事。

举例说明一下状态提升

在这个例子中,父组件 Accordion 渲染了 2 个独立的 Panel 组件。

  • accordion
    • Panel
    • Panel

每个 Panel 组件都有一个布尔值 isActive,用于控制其内容是否可见。

请点击 2 个面板中的显示按钮:

import { useState } from 'react';function Panel({ title, children }) {const [isActive, setIsActive] = useState(false);return (<section className="panel"><h3>{title}</h3>{isActive ? (<p>{children}</p>) : (<button onClick={() => setIsActive(true)}>显示</button>)}</section>);
}export default function Accordion() {return (<><h2>哈萨克斯坦,阿拉木图</h2><Panel title="关于">阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。</Panel><Panel title="词源">这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中“苹果”的意思,经常被翻译成“苹果之乡”。事实上,阿拉木图的周边地区被认为是苹果的发源地,<i lang="la">Malus sieversii</i> 被认为是现今苹果的祖先。</Panel></>);
}

在这里插入图片描述

假设现在您想改变这种行为,以便在任何时候只展开一个面板。在这种设计下,展开第二个面板应会折叠第一个面板,该如何做到呢?

要协调好这两个面板,我们需要分 3 步将状态“提升”到他们的父组件中。

  1. 从子组件中 移除 state 。
  2. 从父组件 传递 硬编码数据。
  3. 为共同的父组件添加 state ,并将其与事件处理函数一起向下传递。

这样,Accordion 组件就可以控制 2 个 Panel 组件,保证同一时间只能展开一个。

第一步:从子组件中移除状态

你将把 Panel 组件对 isActive 的控制权交给他们的父组件。这意味着,父组件会将 isActive 作为 prop 传给子组件 Panel。我们先从 Panel 组件中 删除下面这一行

const [isActive, setIsActive] = useState(false);

然后,把 isActive 加入 Panel 组件的 props 中:

function Panel({ title, children, isActive }) {

现在 Panel 的父组件就可以通过 向下传递 prop 来 控制 isActive。但相反地,Panel 组件对 isActive 的值 没有控制权 —— 现在完全由父组件决定!

第二步:从公共父组件传递硬编码数据

为了实现状态提升,必须定位到你想协调的两个子组件最近的公共父组件:

  • Accordion

    (最近的公共父组件)

    • Panel
    • Panel

在这个例子中,公共父组件是 Accordion。因为它位于两个面板之上,可以控制它们的 props,所以它将成为当前激活面板的“控制之源”。通过 Accordion 组件将硬编码值 isActive(例如 true )传递给两个面板:

import { useState } from 'react';export default function Accordion() {return (<><h2>哈萨克斯坦,阿拉木图</h2><Panel title="关于" isActive={true}>阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。</Panel><Panel title="词源" isActive={true}>这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中“苹果”的意思,经常被翻译成“苹果之乡”。事实上,阿拉木图的周边地区被认为是苹果的发源地,<i lang="la">Malus sieversii</i> 被认为是现今苹果的祖先。</Panel></>);
}function Panel({ title, children, isActive }) {return (<section className="panel"><h3>{title}</h3>{isActive ? (<p>{children}</p>) : (<button onClick={() => setIsActive(true)}>显示</button>)}</section>);
}

第三步:为公共父组件添加状态

状态提升通常会改变原状态的数据存储类型。

在这个例子中,一次只能激活一个面板。这意味着 Accordion 这个父组件需要记录 哪个 面板是被激活的面板。我们可以用数字作为当前被激活 Panel 的索引,而不是 boolean 值:

const [activeIndex, setActiveIndex] = useState(0);

activeIndex0 时,激活第一个面板,为 1 时,激活第二个面板。

在任意一个 Panel 中点击“显示”按钮都需要更改 Accordion 中的激活索引值。 Panel 中无法直接设置状态 activeIndex 的值,因为该状态是在 Accordion 组件内部定义的。 Accordion 组件需要 显式允许 Panel 组件通过 将事件处理程序作为 prop 向下传递 来更改其状态:

<><PanelisActive={activeIndex === 0}onShow={() => setActiveIndex(0)}>...</Panel><PanelisActive={activeIndex === 1}onShow={() => setActiveIndex(1)}>...</Panel></>

现在 Panel 组件中的 <button> 将使用 onShow 这个属性作为其点击事件的处理程序:

import { useState } from 'react';export default function Accordion() {const [activeIndex, setActiveIndex] = useState(0);return (<><h2>哈萨克斯坦,阿拉木图</h2><Paneltitle="关于"isActive={activeIndex === 0}onShow={() => setActiveIndex(0)}>阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。</Panel><Paneltitle="词源"isActive={activeIndex === 1}onShow={() => setActiveIndex(1)}>这个名字来自于 <span lang="kk-KZ">алма</span>,哈萨克语中“苹果”的意思,经常被翻译成“苹果之乡”。事实上,阿拉木图的周边地区被认为是苹果的发源地,<i lang="la">Malus sieversii</i> 被认为是现今苹果的祖先。</Panel></>);
}function Panel({title,children,isActive,onShow
}) {return (<section className="panel"><h3>{title}</h3>{isActive ? (<p>{children}</p>) : (<button onClick={onShow}>显示</button>)}</section>);
}

这样,我们就完成了对状态的提升!将状态移至公共父组件中可以让你更好的管理这两个面板。使用激活索引值代替之前的 是否显示 标识确保了一次只能激活一个面板。而通过向下传递事件处理函数可以让子组件修改父组件的状态

在这里插入图片描述

每个状态都对应唯一的数据源

React 应用中,很多组件都有自己的状态。一些状态可能“活跃”在叶子组件(树形结构最底层的组件)附近,例如输入框。另一些状态可能在应用程序顶部“活动”。例如,客户端路由库也是通过将当前路由存储在 React 状态中,利用 props 将状态层层传递下去来实现的!

对于每个独特的状态,都应该存在且只存在于一个指定的组件中作为 state。这一原则也被称为拥有 “可信单一数据源”。它并不意味着所有状态都存在一个地方——对每个状态来说,都需要一个特定的组件来保存这些状态信息。你应该 将状态提升 到公共父级,或 将状态传递 到需要它的子级中,而不是在组件之间复制共享的状态。

你的应用会随着你的操作而变化。当你将状态上下移动时,你依然会想要确定每个状态在哪里“活跃”。这都是过程的一部分!

想了解在更多组件中的实践,请阅读 React 思维.

摘要
  • 当你想要整合两个组件时,将它们的 state 移动到共同的父组件中。
  • 然后在父组件中通过 props 把信息传递下去。
  • 最后,向下传递事件处理程序,以便子组件可以改变父组件的 state 。
  • 考虑该将组件视为“受控”(由 prop 驱动)或是“不受控”(由 state 驱动)是十分有益的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/8495.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《编译原理》阅读笔记:p1-p3

《编译原理》学习第 1 天&#xff0c;p1-p3总结&#xff0c;总计 3 页。 一、技术总结 1.compiler(编译器) p1, But, before a program can be run, it first must be translated into a form in which it can be executed by a computer. The software systems that do thi…

2023年谷歌拒了228万应用,禁了33.3万账号,开发者们应如何应对2024的挑战?

谷歌在上周一公布了去年如何应对恶意应用和恶意行为。 报告指出&#xff0c;去年谷歌在Google Play平台上&#xff0c;通过不断升级安全系统、更新政策规定、运用先进的机器学习技术&#xff0c;以及严格把关应用审核流程&#xff0c;成功阻止了高达228万个不合规的应用程序上架…

力扣41. 缺失的第一个正数

Problem: 41. 缺失的第一个正数 文章目录 题目描述思路复杂度Code 题目描述 思路 1.将nums看作为一个哈希表&#xff0c;每次我们将数字n移动到nums[n - 1]的位置(例如数字1应该存在nums[0]处…),则在实际的代码操作中应该判断nums[i]与nums[nums[i] - 1]是否相等&#xff0c;若…

【管理咨询宝藏96】企业数字化转型的中台战略培训方案

本报告首发于公号“管理咨询宝藏”&#xff0c;如需阅读完整版报告内容&#xff0c;请查阅公号“管理咨询宝藏”。 【管理咨询宝藏96】企业数字化转型的中台战略培训方案 【格式】PDF版本 【关键词】SRM采购、制造型企业转型、数字化转型 【核心观点】 - 数字化转型是指&…

Web3 ETF软件开发

开发Web3 ETF软件涉及到金融、法律和技术等多个领域的专业知识&#xff0c;因此存在以下技术难点&#xff0c;开发Web3 ETF软件是一项复杂的技术挑战&#xff0c;需要综合考虑各种因素。开发人员需要具备较强的技术能力和跨学科知识才能成功开发Web3 ETF软件。北京木奇移动技术…

WEB基础--JDBC基础

JDBC简介 JDBC概述 数据库持久化介绍 jdbc是java做数据库持久化的规范&#xff0c;持久化(persistence)&#xff1a;把数据保存到可掉电式存储设备(断电之后&#xff0c;数据还在&#xff0c;比如硬盘&#xff0c;U盘)中以供之后使用。大多数情况下&#xff0c;特别是企业级…

Jsoncpp介绍

1.简介 Jsoncpp 是一个 C 库&#xff0c;用于解析和生成 JSON 数据。它提供了一个易于使用的 DOM&#xff08;Document Object Model&#xff09;风格的 API&#xff0c;允许开发者以树形结构的方式操作 JSON 数据。 Jsoncpp 是一个C库&#xff0c;允许操作JSON值&#xff0c;…

AI Agent智能应用从0到1定制开发(wanjie)

AI Agent&#xff08;人工智能体&#xff09;是一种能够感知环境、进行决策和执行动作的智能实体。不同于传统的人工智能&#xff0c;AI Agent 具备通过独立思考、调用工具去逐步完成给定目标的能力。 「完结12章」AI Agent智能应用从0到1定制开发 AI Agent 和大模型的区别在…

【管理咨询宝藏95】SRM采购平台建设内部培训方案

本报告首发于公号“管理咨询宝藏”&#xff0c;如需阅读完整版报告内容&#xff0c;请查阅公号“管理咨询宝藏”。 【管理咨询宝藏95】SRM采购平台建设内部培训方案 【格式】PDF版本 【关键词】SRM采购、制造型企业转型、数字化转型 【核心观点】 - 重点是建设一个适应战略采…

PDF转word转ppt软件

下载地址&#xff1a;PDF转word转ppt软件.zip 平时工作生活经常要用到PDF转word转ppt软件&#xff0c;电脑自带的又要开会员啥的很麻烦&#xff0c;现在分享这款软件直接激活就可以免费使用了&#xff0c;超级好用&#xff0c;喜欢的可以下载

C++类和对象(基础篇)

前言&#xff1a; 其实任何东西&#xff0c;只要你想学&#xff0c;没人能挡得住你&#xff0c;而且其实学的也很快。那么本篇开始学习类和对象&#xff08;C的&#xff0c;由于作者有Java基础&#xff0c;可能有些东西过得很快&#xff09;。 struct在C中的含义&#xff1a; …

PyTorch模型的保存加载

一、引言 我们今天来看一下模型的保存与加载~ 我们平时在神经网络的训练时间可能会很长&#xff0c;为了在每次使用模型时避免高代价的重复训练&#xff0c;我们就需要将模型序列化到磁盘中&#xff0c;使用的时候反序列化到内存中。 PyTorch提供了两种主要的方法来保存和加…

缓存雪崩、击穿、击穿

缓存雪崩&#xff1a; 就是大量数据在同一时间过期或者redis宕机时&#xff0c;这时候有大量的用户请求无法在redis中进行处理&#xff0c;而去直接访问数据库&#xff0c;从而导致数据库压力剧增&#xff0c;甚至有可能导致数据库宕机&#xff0c;从而引发的一些列连锁反应&a…

MATLAB 基于规则格网的点云抽稀方法(自定义实现)(65)

MATLAB 基于规则格网的点云抽稀方法(自定义实现)(65) 一、算法介绍二、算法实现1.代码2.结果一、算法介绍 海量点云的处理,需要提前进行抽稀预处理,相比MATLAB预先给出的抽稀方法,这里提供一种基于规则格网的自定义抽稀方法,步骤清晰,便于理解抽稀内涵, 主要涉及到使…

springboot整合rabbitmq的不同工作模式详解

前提是已经安装并启动了rabbitmq&#xff0c;并且项目已经引入rabbitmq&#xff0c;完成了配置。 不同模式所需参数不同&#xff0c;生产者可以根据参数不同使用重载的convertAndSend方法。而消费者均是直接监听某个队列。 不同的交换机是实现不同工作模式的关键组件.每种交换…

三层交换机与防火墙连通上网实验

防火墙是一种网络安全设备&#xff0c;用于监控和控制网络流量。它可以帮助防止未经授权的访问&#xff0c;保护网络免受攻击和恶意软件感染。防火墙可以根据预定义的规则过滤流量&#xff0c;例如允许或阻止特定IP地址或端口的流量。它也可以检测和阻止恶意软件、病毒和其他威…

SlowFast报错:ValueError: too many values to unpack (expected 4)

SlowFast报错&#xff1a;ValueError: too many values to unpack (expected 4) 报错细节 File "/home/user/yuanjinmin/SlowFast/tools/visualization.py", line 81, in run_visualizationfor inputs, labels, _, meta in tqdm.tqdm(vis_loader): ValueError: too …

牛客题-链表内区间反转

链表内区间反转 这是代码 typedef struct ListNode listnode; struct ListNode* reverseBetween(struct ListNode* head, int m, int n ) {if (head NULL) {return NULL;}listnode* findhead head;listnode* findtail head;listnode* prev NULL;int count1 m;int count2…

OpenCV 入门(二)—— 车牌定位

OpenCV 入门系列&#xff1a; OpenCV 入门&#xff08;一&#xff09;—— OpenCV 基础 OpenCV 入门&#xff08;二&#xff09;—— 车牌定位 OpenCV 入门&#xff08;三&#xff09;—— 车牌筛选 OpenCV 入门&#xff08;四&#xff09;—— 车牌号识别 OpenCV 入门&#xf…

十分钟掌握Java集合之List接口

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。运营社区&#xff1a;C站/掘金/腾讯云&#xff1b;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互相学习&#xff0c;一…