React 入门使用 (官方文档向 Part2)

文章目录

  • [用 State 响应输入](#用 State 响应输入)
    • [声明式地考虑 UI](#声明式地考虑 UI)
      • [步骤 1:定位组件中不同的视图状态](#步骤 1:定位组件中不同的视图状态)
      • [步骤 2:确定是什么触发了这些状态的改变](#步骤 2:确定是什么触发了这些状态的改变)
      • [步骤 3:通过 useState 表示内存中的 state](#步骤 3:通过 useState 表示内存中的 state)
      • [步骤 4:删除任何不必要的 state 变量](#步骤 4:删除任何不必要的 state 变量)
      • [步骤 5:连接事件处理函数以设置 state](#步骤 5:连接事件处理函数以设置 state)
  • [选择 State 结构](#选择 State 结构)
    • [构建 state 的原则](#构建 state 的原则)
  • 在组件间共享状态
    • 举例说明一下状态提升
      • [第 1 步: 从子组件中移除状态](#第 1 步: 从子组件中移除状态)
      • [第 2 步: 从公共父组件传递硬编码数据](#第 2 步: 从公共父组件传递硬编码数据)
      • [第 3 步: 为公共父组件添加状态](#第 3 步: 为公共父组件添加状态)
  • [对 state 进行保留和重置](#对 state 进行保留和重置)
    • 状态与渲染树中的位置相关
    • [相同位置的相同组件会使得 state 被保留下来](#相同位置的相同组件会使得 state 被保留下来)
    • [相同位置的不同组件会使 state 重置](#相同位置的不同组件会使 state 重置)
    • [在相同位置重置 state](#在相同位置重置 state)
  • [迁移状态逻辑至 Reducer 中](#迁移状态逻辑至 Reducer 中)
    • [使用 reducer 整合状态逻辑](#使用 reducer 整合状态逻辑)
      • [第 1 步: 将设置状态的逻辑修改成 dispatch 的一个 action](#第 1 步: 将设置状态的逻辑修改成 dispatch 的一个 action)
      • [第 2 步: 编写一个 reducer 函数](#第 2 步: 编写一个 reducer 函数)
      • 语法
      • 示例
      • 使用场景
      • 注意事项
      • 示例
      • [第 3 步: 在组件中使用 reducer](#第 3 步: 在组件中使用 reducer)
    • [对比 useState 和 useReducer](#对比 useState 和 useReducer)
  • [使用 Context 深层传递参数](#使用 Context 深层传递参数)
    • [传递 props 带来的问题](#传递 props 带来的问题)
    • [Context:传递 props 的另一种方法](#Context:传递 props 的另一种方法)
    • [写在你使用 context 之前](#写在你使用 context 之前)
    • [Context 的使用场景](#Context 的使用场景)
  • [使用 Reducer 和 Context 拓展你的应用](#使用 Reducer 和 Context 拓展你的应用)
  • 应急方案
  • [使用 ref 引用值](#使用 ref 引用值)
    • [给你的组件添加 ref](#给你的组件添加 ref)
    • [ref 和 state 的不同之处](#ref 和 state 的不同之处)
        • [useRef 内部是如何运行的?](#useRef 内部是如何运行的?)
    • [何时使用 ref](#何时使用 ref)
    • [ref 的最佳实践](#ref 的最佳实践)
  • [使用 ref 操作 DOM](#使用 ref 操作 DOM)
    • [获取指向节点的 ref](#获取指向节点的 ref)
    • [访问另一个组件的 DOM 节点](#访问另一个组件的 DOM 节点)
  • [使用 Effect 同步](#使用 Effect 同步)
  • [你可能不需要 Effect](#你可能不需要 Effect)
  • [响应式 Effect 的生命周期](#响应式 Effect 的生命周期)
    • [Effect 的生命周期](#Effect 的生命周期)

\]以下内容来自官方文档 https://zh-hans.react.dev/learn\](https://zh-hans.react.dev/learn) ## 用 State 响应输入 React 控制 UI 的方式是声明式的。你不必直接控制 UI 的各个部分,只需要声明组件可以处于的不同状态,并根据用户的输入在它们之间切换 ### 声明式地考虑 UI 你已经从上面的例子看到如何去实现一个表单了,为了更好地理解如何在 React 中思考,接下来你将会学到如何用 React 重新实现这个 UI: 1. **定位**你的组件中不同的视图状态 2. **确定**是什么触发了这些 state 的改变 3. **表示**内存中的 state(需要使用 useState) 4. **删除**任何不必要的 state 变量 5. **连接** 事件处理函数去设置 state #### 步骤 1:定位组件中不同的视图状态 在计算机科学中,你或许听过可处于多种"状态"之一的 ["状态机"](https://en.wikipedia.org/wiki/Finite-state_machine) 首先,你需要去可视化 UI 界面中用户可能看到的所有不同的"状态": * **无数据**:表单有一个不可用状态的"提交"按钮。 * **输入中**:表单有一个可用状态的"提交"按钮。 * **提交中**:表单完全处于不可用状态,加载动画出现。 * **成功时**:显示"成功"的消息而非表单。 * **错误时** :与输入状态类似,但会多错误的消息。 #### 步骤 2:确定是什么触发了这些状态的改变 你可以触发 state 的更新来响应两种输入: * **人为**输入。比如点击按钮、在表单中输入内容,或导航到链接。 * **计算机**输入。比如网络请求得到反馈、定时器被触发,或加载一张图片 你需要改变 state 以响应几个不同的输入: * **改变输入框中的文本时** (人为)应该根据输入框的内容是否是**空值** ,从而决定将表单的状态从空值状态切换到**输入中**或切换回原状态。 * **点击提交按钮时** (人为)应该将表单的状态切换到**提交中**的状态。 * **网络请求成功后** (计算机)应该将表单的状态切换到**成功**的状态。 * **网络请求失败后** (计算机)应该将表单的状态切换到**失败**的状态,与此同时,显示错误信息 * ![image.png](https://file.jishuzhan.net/article/1730377181080588289/f80a0cfccf529dbe3722e83d97e44d21.webp) #### 步骤 3:通过 useState 表示内存中的 state 如果你很难立即想出最好的办法,那就先从添加足够多的 state 开始,**确保**所有可能的视图状态都囊括其中: ```js 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 变量 在清理之后,你只剩下 3 个(从原本的 7 个!)_必要_的 state 变量: ```js const [answer, setAnswer] = useState(''); const [error, setError] = useState(null); const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success' ``` #### 步骤 5:连接事件处理函数以设置 state 练习1 ```js import {useState} from 'react' export default function Picture() { const [imgcss,setImgcss] = useState(false) function handleBgd(){ setImgcss(false) } function handleImg(e){ e.stopPropagation() setImgcss(true) } let bgdClassName = 'background' let imgClassName = 'picture' if(imgcss){ imgClassName += ' picture--active' //注意此处的空格 }else{ bgdClassName += ' background--active' //注意此处的空格 } return (

setImgcss(false)}> { e.stopPropagation(); setImgcss(true)}} className={imgClassName} alt="Rainbow houses in Kampung Pelangi, Indonesia" src="https://i.imgur.com/5qwVYb1.jpeg" />
); } ``` 练习2 ```js import {useState} from 'react' export default function EditProfile() { const [edit,setEdit]= useState(false) const [firstName,setFirstName] = useState('') const [lastName,setLastName] = useState('') return (
{ e.preventDefault() setEdit(!edit) }}>

Hello, {firstName} {lastName}!

); } ``` ## 选择 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 放到它们的公共父级,再通过 props 将 state 传递给这两个组件。这被称为"状态提升",这是编写 React 代码时常做的事 ### 举例说明一下状态提升 在这个例子中,父组件 Accordion 渲染了 2 个独立的 Panel 组件。 * Accordion * Panel * Panel 每个 Panel 组件都有一个布尔值 isActive,用于控制其内容是否可见 ```js import { useState } from 'react'; function Panel({ title, children }) { const [isActive, setIsActive] = useState(false); return (

{title}

{isActive ? (

{children}

) : ( )}
); } export default function Accordion() { return ( <>

哈萨克斯坦,阿拉木图

阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。 这个名字来自于 алма,哈萨克语中"苹果"的意思,经常被翻译成"苹果之乡"。事实上,阿拉木图的周边地区被认为是苹果的发源地,Malus sieversii 被认为是现今苹果的祖先。 ); } ``` ![image.png](https://file.jishuzhan.net/article/1730377181080588289/01b6cd673a189f105c6f504b75d0a902.webp)**假设现在您想改变这种行为,以便在任何时候只展开一个面板** 。在这种设计下,展开第 2 个面板应会折叠第 1 个面板。您该如何做到这一点呢?" 要协调好这两个面板,我们需要分 3 步将状态"提升"到他们的父组件中。 1. 从子组件中 **移除** state 。 2. 从父组件 **传递** 硬编码数据。 3. 为共同的父组件添加 state ,并将其与事件处理函数一起向下传递 #### 第 1 步: 从子组件中移除状态 你将把 Panel 组件对 isActive 的控制权交给他们的父组件。这意味着,父组件会将 isActive 作为 prop 传给子组件 Panel。我们先从 Panel 组件中 **删除下面这一行**: ```jsx const [isActive, setIsActive] = useState(false); ``` 然后,把 isActive 加入 Panel 组件的 props 中: ```js function Panel({ title, children, isActive }) { ``` #### 第 2 步: 从公共父组件传递硬编码数据 ```js import { useState } from 'react'; export default function Accordion() { return ( <>

哈萨克斯坦,阿拉木图

阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。 这个名字来自于 алма,哈萨克语中"苹果"的意思,经常被翻译成"苹果之乡"。事实上,阿拉木图的周边地区被认为是苹果的发源地,Malus sieversii 被认为是现今苹果的祖先。 ); } function Panel({ title, children, isActive }) { return (

{title}

{isActive ? (

{children}

) : ( )}
); } ``` #### 第 3 步: 为公共父组件添加状态 状态提升通常会改变原状态的数据存储类型。 在这个例子中,一次只能激活一个面板。这意味着 Accordion 这个父组件需要记录 **哪个** 面板是被激活的面板。我们可以用数字作为当前被激活 Panel 的索引,而不是 boolean 值: ```js const [activeIndex, setActiveIndex] = useState(0); ``` ```js import { useState } from 'react'; export default function Accordion() { const [activeIndex, setActiveIndex] = useState(0); return ( <>

哈萨克斯坦,阿拉木图

setActiveIndex(0)} > 阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。 setActiveIndex(1)} > 这个名字来自于 алма,哈萨克语中"苹果"的意思,经常被翻译成"苹果之乡"。事实上,阿拉木图的周边地区被认为是苹果的发源地,Malus sieversii 被认为是现今苹果的祖先。 ); } function Panel({ title, children, isActive, onShow }) { return (

{title}

{isActive ? (

{children}

) : ( )}
); } ``` ![image.png](https://file.jishuzhan.net/article/1730377181080588289/814243ff6c534cc3b8a1260f6fa54e03.webp) ##### 受控组件和非受控组件 通常我们把包含"不受控制"状态的组件称为"非受控组件"。例如,最开始带有 isActive 状态变量的 Panel 组件就是不受控制的,因为其父组件无法控制面板的激活状态。 相反,当组件中的重要信息是由 props 而不是其自身状态驱动时,就可以认为该组件是"受控组件"。这就允许父组件完全指定其行为。最后带有 isActive 属性的 Panel 组件是由 Accordion 组件控制的 练习1 ```js import { useState } from 'react'; export default function SyncedInputs() { const [text,setText]= useState('') function handleChange(e){ setText(e.target.value) } return ( <> ); } function Input({ label, onChange, text }) { //const [text, setText] = useState(''); /*function handleChange(e) { setText(e.target.value); }*/ return ( ); } ``` 练习2 ```js import { useState } from 'react'; import { foods, filterItems } from './data.js'; export default function FilterableList() { const [query,setQuery] = useState('') function handleQuery(e){ setQuery(e.target.value) } return ( <>
); } function SearchBar({text,onChange}) { /*const [query, setQuery] = useState(''); function handleChange(e) { setQuery(e.target.value); }*/ return ( ); } function List({ items,text }) { return ( {filterItems(items,text).map(food => ( ))}
{food.name} {food.description}
); } ``` ## 对 state 进行保留和重置 ### 状态与渲染树中的位置相关 只有当在树中相同的位置渲染相同的组件时,React 才会一直保留着组件的 state ```js import { useState } from 'react'; export default function App() { const [showB, setShowB] = useState(true); return (
{showB && }
); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return (
setHover(true)} onPointerLeave={() => setHover(false)} >

{score}

); } ``` 注意,当你停止渲染第二个计数器的那一刻,它的 state 完全消失了。这是因为 React 在移除一个组件时,也会销毁它的 state![image.png](https://file.jishuzhan.net/article/1730377181080588289/e25fb4b0d0190ba58e257b22fa295a11.webp) 当你重新勾选"渲染第二个计数器"复选框时,另一个计数器及其 state 将从头开始初始化(score = 0)并被添加到 DOM 中。 ![image.png](https://file.jishuzhan.net/article/1730377181080588289/eb314aa6d50f020da6689785d7ef07a9.webp) ### 相同位置的相同组件会使得 state 被保留下来 ```js import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return (
{isFancy ? ( ) : ( )}
); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return (
setHover(true)} onPointerLeave={() => setHover(false)} >

{score}

); } ``` ![image.png](https://file.jishuzhan.net/article/1730377181080588289/ac404591995535457d429007bc9eff46.webp) ### 相同位置的不同组件会使 state 重置 在这个例子中,勾选复选框会将 替换为一个 ```js import { useState } from 'react'; export default function App() { const [isPaused, setIsPaused] = useState(false); return (
{isPaused ? (

待会见!

) : ( )}
); } function Counter() { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return (
setHover(true)} onPointerLeave={() => setHover(false)} >

{score}

); } ``` ![image.png](https://file.jishuzhan.net/article/1730377181080588289/c50eb63fca94123e8782d9e78ab27405.webp)![image.png](https://file.jishuzhan.net/article/1730377181080588289/ca772d40879e28037e8f0825b2c0d584.webp)并且,**当你在相同位置渲染不同的组件时,组件的整个子树都会被重置**。要验证这一点,可以增加计数器的值然后勾选复选框 ```js import { useState } from 'react'; export default function App() { const [isFancy, setIsFancy] = useState(false); return (
{isFancy ? (
) : (
)}
); } function Counter({ isFancy }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } if (isFancy) { className += ' fancy'; } return (
setHover(true)} onPointerLeave={() => setHover(false)} >

{score}

); } ``` ![image.png](https://file.jishuzhan.net/article/1730377181080588289/5c55a57269e45c2e13a2ad7d98b21dc9.webp)![image.png](https://file.jishuzhan.net/article/1730377181080588289/4794f4cdc53fabd51d2a839fe1de46ad.webp) ### 在相同位置重置 state #### 方法一:将组件渲染在不同的位置 ```js import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return (
{isPlayerA && } {!isPlayerA && }
); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return (
setHover(true)} onPointerLeave={() => setHover(false)} >

{person} 的分数:{score}

); } ``` ![image.png](https://file.jishuzhan.net/article/1730377181080588289/180e225b05e5d0147ffea1fc3f154d73.webp) #### 方法二:使用 key 来重置 state key 不只可以用于列表!你可以使用 key 来让 React 区分任何组件。默认情况下,React 使用父组件内部的顺序("第一个计数器"、"第二个计数器")来区分组件。但是 key 可以让你告诉 React 这不仅仅是 **第一个** 或者 **第二个** 计数器,而且还是一个特定的计数器------例如,**Taylor 的** 计数器。这样无论它出现在树的任何位置, React 都会知道它是 **Taylor 的** 计数器! ```js import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return (
{isPlayerA ? ( ) : ( )}
); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return (
setHover(true)} onPointerLeave={() => setHover(false)} >

{person} 的分数:{score}

); } ``` ##### 为被移除的组件保留 state 在真正的聊天应用中,你可能会想在用户再次选择前一个收件人时恢复输入 state。对于一个不可见的组件,有几种方法可以让它的 state "活下去": * 与其只渲染现在这一个聊天,你可以把 **所有** 聊天都渲染出来,但用 CSS 把其他聊天隐藏起来。这些聊天就不会从树中被移除了,所以它们的内部 state 会被保留下来。这种解决方法对于简单 UI 非常有效。但如果要隐藏的树形结构很大且包含了大量的 DOM 节点,那么性能就会变得很差。 * 你可以进行 [状态提升](https://zh-hans.react.dev/learn/sharing-state-between-components) 并在父组件中保存每个收件人的草稿消息。这样即使子组件被移除了也无所谓,因为保留重要信息的是父组件。这是最常见的解决方法。 * 除了 React 的 state,你也可以使用其他数据源。例如,也许你希望即使用户不小心关闭页面也可以保存一份信息草稿。要实现这一点,你可以让 Chat 组件通过读取 [localStorage](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/localStorage) 对其 state 进行初始化,并把草稿保存在那里。 练习1 ```js import { useState } from 'react'; export default function App() { const [showHint, setShowHint] = useState(false); if (showHint) { return (

提示:你最喜欢的城市?

); } return (

); } function Form() { const [text, setText] = useState(''); return (