Skip to content

React是单向数据流,state影响每一个视图刷新,state变了才会刷新视图。

State响应输入:

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

步骤 1:定位组件中不同的视图状态

可视化UI界面中用户可能看到的所有不同“状态”。

  • 无数据:表单有一个不可用状态的“提交”按钮。
  • 输入中:表单有一个可用状态的“提交”按钮。
  • 提交中:表单完全处于不可用状态,加载动画出现。
  • 成功时:显示“成功”的消息而非表单。
  • 错误时:与输入状态类似,但会多错误的消息。 如果一个组件有多个视图状态,可以方便地将他们展示在一个页面中: 类似这样的页面通常被称作“living styleguide”或“storybook”。
jsx
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 (如果存在的话):

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

接下来,你需要一个==状态变量==来代表你想要显示的那个可视状态。

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

jsx
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 变量:
jsx
const [answer, setAnswer] = useState('');  
const [error, setError] = useState(null);  
const [status, setStatus] = useState('typing'); 
// 'typing', 'submitting', or 'success'

通过 reducer 来减少“不可能” state 

例如一个非空的 errorstatus 的值为 success 时没有意义。为了更精确地模块化状态,你可以 将状态提取到一个 reducer 中。Reducer 可以让您合并多个状态变量到一个对象中并巩固所有相关的逻辑!

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

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

jsx
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

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

方法一:将组件渲染在不同的位置

jsx
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 不是全局唯一的。它们只能指定 父组件内部 的顺序。

jsx
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? 为 ifelse 分支中的两个 <Field> 组件都指定一个 key。这样可以告诉 React 如何为两个 <Field> “匹配”正确的状态——即使它们在父组件中的顺序会发生变化:

(前面的还没整理,回来再整理我先跳了)

迁移状态逻辑至 Reducer 中

Q:为什么要用Reducer函数? A:对于多种状态更新逻辑的组件,过于分散的事件处理函数很麻烦,可以将组件所有状态更新逻辑整合到一个外部函数reducer中。

使用 reducer 整合状态逻辑

例如:TaskApp组件有一个数组类型的状态tasks,通过三个不同的时间处理程序实现任务添加、删除、修改。 ![[Pasted image 20240630130850.png]]

jsx
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 来指明用户“刚刚做了什么”。(而状态更新逻辑则保存在其他地方)。

Copyright © 2024-present LofiSu