React是单向数据流,state影响每一个视图刷新,state变了才会刷新视图。
State响应输入:
- ==声明式编程意味着为每个视图状态声明 UI 而非细致地控制 UI(命令式)。==
- 当开发一个组件时:
- 定位你的组件中不同的视图状态
- 确定是什么触发了这些 state 的改变。
- 通过
useState
模块化内存中的 state。 - 删除任何不必要的 state 变量。
- 连接事件处理函数去设置 state。
步骤 1:定位组件中不同的视图状态
可视化UI界面中用户可能看到的所有不同“状态”。
- 无数据:表单有一个不可用状态的“提交”按钮。
- 输入中:表单有一个可用状态的“提交”按钮。
- 提交中:表单完全处于不可用状态,加载动画出现。
- 成功时:显示“成功”的消息而非表单。
- 错误时:与输入状态类似,但会多错误的消息。 如果一个组件有多个视图状态,可以方便地将他们展示在一个页面中: 类似这样的页面通常被称作“living styleguide”或“storybook”。
App.js
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>
);
}
![[Pasted image 20240629171317.png]]
步骤 2:确定是什么触发了这些状态的改变
可以触发state更新响应的两种输入:
- 人为输入。比如点击按钮、在表单中输入内容,或导航到链接。 ==注意,人为输入通常需要 事件处理函数!==
- 计算机输入。比如网络请求得到反馈、定时器被触发,或加载一张图片。 ![[Pasted image 20240629192212.png]]
设置state变量更新UI,对于正在开发的表单,需要改变state响应不同输入:
- 改变输入框中的文本时(人为)应该根据输入框的内容是否是空值,从而决定将表单的状态从空值状态切换到输入中或切换回原状态。
- 点击提交按钮时(人为)应该将表单的状态切换到提交中的状态。
- 网络请求成功后(计算机)应该将表单的状态切换到成功的状态。
- 网络请求失败后(计算机)应该将表单的状态切换到失败的状态,与此同时,显示错误信息。 ![[Pasted image 20240629195556.png]]
步骤 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 是否会导致矛盾?例如,
isTyping
与isSubmitting
的状态不能同时为true
。矛盾的产生通常说明了这个 state 没有足够的约束条件。两个布尔值有四种可能的组合,但是只有三种对应有效的状态。为了将“不可能”的状态移除,你可以将他们合并到一个'status'
中,它的值必须是'typing'
、'submitting'
以及'success'
这三个中的一个。 - 相同的信息是否已经在另一个 state 变量中存在?另一个矛盾:
isEmpty
和isTyping
不能同时为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'
通过 reducer 来减少“不可能” state
例如一个非空的 error
当 status
的值为 success
时没有意义。为了更精确地模块化状态,你可以 将状态提取到一个 reducer 中。Reducer 可以让您合并多个状态变量到一个对象中并巩固所有相关的逻辑!
步骤 5:连接事件处理函数以设置 state
最后,创建事件处理函数去设置 state 变量。下面是绑定好事件的最终表单:
import {useState} from 'react';
export default function From(){
const[answer,setAnswer]=useState('');
const[error,setError]=useState(null);
const[states,setStates]=useState('typing');
if(states==='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);
});
}
async function
异步函数。函数体内允许使用 await
关键字,这使得我们可以更简洁地编写基于 promise 的异步代码,并且避免了显式地配置 promise 链的需要。
你也可以使用 async function
表达式来定义异步函数。
下面这几章可以直接看尝试挑战和摘要,如果有不理解的知识点再去上面理解,我有总结补充的会写上去
选择 State 结构
构建 state 的原则
1.合并关联的state: 如果两个 state 变量总是一起更新,请考虑将它们合并为一个。 2.避免互相矛盾的state: 仔细选择 state 变量,避免创建“极难处理”的 state。 3.避免冗余的state 4.避免重复的state 5.避免深度嵌套的state:不要将 props 放入 state 中。 对于选择类型的 UI 模式,请在 state 中保存 ID 或索引而不是对象本身。
- 如果深度嵌套 state 更新很复杂,请尝试将其展开扁平化。
在组件间共享状态
==状态提升:两个组件的状态同步更改,将相关state从这两个组件上移除,把state放到公共父级,通过props将state传递给两个组件==
对 state 进行保留和重置
- 只要一个组件还被渲染在 UI 树的相同位置,React 就会保留它的 state。 如果它被移除,或者一个不同的组件被渲染在相同的位置,那么 React 就会丢掉它的 state。
- 对 React 来说重要的是组件在 UI 树中的位置,而不是在 JSX 中的位置!
- 当你在相同位置渲染不同的组件时,组件的整个子树都会被重置。
- 你可以通过为一个子树指定一个不同的 key 来重置它的 state。
- 不要嵌套组件的定义,否则你会意外地导致 state 被重置。
在相同位置重置 state
import { useState } from 'react';
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA ? (
<Counter person="Taylor" />
) : (
<Counter person="Sarah" />
)}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
下一位玩家!
</button>
</div>
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{person} 的分数:{score}</h1>
<button onClick={() => setScore(score + 1)}>
加一
</button>
</div>
);
}
方法一:将组件渲染在不同的位置
import { useState } from 'react';
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA &&
<Counter person="Taylor" />
}
{!isPlayerA &&
<Counter person="Sarah" />
}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
下一位玩家!
</button>
</div>
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{person} 的分数:{score}</h1>
<button onClick={() => setScore(score + 1)}>
加一
</button>
</div>
);
}
方法二:使用 key 来区分组件
key 不是全局唯一的。它们只能指定 父组件内部 的顺序。
import { useState } from 'react';
export default function Scoreboard() {
const [isPlayerA, setIsPlayerA] = useState(true);
return (
<div>
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
<button onClick={() => {
setIsPlayerA(!isPlayerA);
}}>
下一位玩家!
</button>
</div>
);
}
function Counter({ person }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{person} 的分数:{score}</h1>
<button onClick={() => setScore(score + 1)}>
加一
</button>
</div>
);
}
尝试一些挑战
1.“修复文本框”:为什么输入消息后按下按钮文本框会重置? 组件要渲染在UI树的相同位置,React才会保留state 可以把它合并成一个组件或者调整到组件的同一位置(在前面或者后面加null) 2.“交换表单字段”:它们在父组件中的位置并不足以实现功能。有没有办法告诉 React 如何匹配多次重新渲染中的 state? 为 if
和 else
分支中的两个 <Field>
组件都指定一个 key
。这样可以告诉 React 如何为两个 <Field>
“匹配”正确的状态——即使它们在父组件中的顺序会发生变化:
(前面的还没整理,回来再整理我先跳了)
迁移状态逻辑至 Reducer 中
Q:为什么要用Reducer函数? A:对于多种状态更新逻辑的组件,过于分散的事件处理函数很麻烦,可以将组件所有状态更新逻辑整合到一个外部函数reducer中。
使用 reducer 整合状态逻辑
例如:TaskApp组件有一个数组类型的状态tasks,通过三个不同的时间处理程序实现任务添加、删除、修改。 ![[Pasted image 20240630130850.png]]
import{useState}from'react';
import AddTask from './AddTask.js'
import TaskList from './TaskList.js'
export default function TaskApp(){
const [tasks,setTasks]=useState(initialTasks);
function handleAddTask(text){
setTasks([//创建状态更新函数
...tasks,//数组解构,表示将tasks数组的所有元素展开并作为新数组的一部分
{//添加新的任务对象,包含三个属性
id:nextId++,
text:text,//任务描述文本
done:false,//布尔值表示任务是否完成,初始化为false
},
]);
}
function handleChangeTask(task){
setTasks(
tasks.map((t)=>{
if(t.id===task.id){
return tasks;
}else{
return t;
}
})
);
}
function handleDeleteTask(taskId){
setTasks(tasks.filter((t)=>t.id !==taskId));
}
return(
<>
<h1>布拉格的行程安排</h1>
<AddTask onAddTask= {handleAddTask} />
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
let nextId = 3;
const initialTasks = [
{id: 0, text: '参观卡夫卡博物馆', done: true},
{id: 1, text: '看木偶戏', done: false},
{id: 2, text: '打卡列侬墙', done: false},
];
这个组件的每个事件处理程序都通过 setTasks
来更新状态。随着这个组件的不断迭代,其状态逻辑也会越来越多。为了降低这种复杂度,并让所有逻辑都可以存放在一个易于理解的地方,你可以将这些状态逻辑移到组件之外的一个称为 reducer 的函数中。
Reducer 是处理状态的另一种方式。你可以通过三个步骤将 useState
迁移到 useReducer
: 1.将设置状态逻辑修改成dispatch的一个action; 2.编写一个reducer函数; 3.在你的组件中使用reducer。
第 1 步: 将设置状态的逻辑修改成 dispatch 的一个 action
移除所有的状态设置逻辑。只留下三个事件处理函数:
handleAddTask(text)
在用户点击 “添加” 时被调用。handleChangeTask(task)
在用户切换任务或点击 “保存” 时被调用。handleDeleteTask(taskId)
在用户点击 “删除” 时被调用。
使用reducers管理状态与直接设置状态不同。它不是通过设置状态来告诉React“要做什么”,而是通过时间处理程序 dispatch 一个 action 来指明用户“刚刚做了什么”。(而状态更新逻辑则保存在其他地方)。