文章目录

[]以下内容来自官方文档 https://zhhans.react.dev/learn](https://zhhans.react.dev/learn)

用 State 响应输入

React 控制 UI 的方式声明式的。你不必直接控制 UI 的各个部分,只需要声明组件可以处于的不同状态,并根据用户输入在它们之间切换

声明式地考虑 UI

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

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

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

计算机科学中,你或许听过可处于多种“状态”之一的 “状态机”
首先,你需要可视化 UI 界面用户可能看到的所有不同的“状态”:

步骤 2:确定是什么触发了这些状态的改变

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

需要改变 state响应几个不同的输入:

步骤 3:通过 useState 表示内存中的 state

如果你很难立即想出最好的办法,那就先从添加足够多的 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 变量

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

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

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

const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'

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

练习1

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 (
    <div className={bgdClassName} onClick={()=>setImgcss(false)}>
      <img
        onClick={e => {
          e.stopPropagation();
          setImgcss(true)}}
        className={imgClassName}
        alt="Rainbow houses in Kampung Pelangi, Indonesia"
        src="https://i.imgur.com/5qwVYb1.jpeg"
      />
    </div>
  );
}

练习2

import {useState} from 'react'
export default function EditProfile() {
  const [edit,setEdit]= useState(false)
  const [firstName,setFirstName] = useState('')
  const [lastName,setLastName] = useState('')
  return (
    <form onSubmit={(e)=>{
      e.preventDefault()
      setEdit(!edit)
    }}>
      <label>
        First name:{' '}
        {edit? <input onChange={(e)=>{
          setFirstName(e.target.value)
        }} value={firstName}/>:<b>{firstName}</b>}
      </label>
      <label>
        Last name:{' '}
        {edit? <input onChange={(e)=>{
          setLastName(e.target.value)
        }} value={lastName}/>:<b>{lastName}</b>}
      </label>
      <button type="submit">
        {edit? 'save ': 'edit '} Profile
      </button>
      <p><i>Hello, {firstName} {lastName}!</i></p>
    </form>
  );
}

选择 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,用于控制其内容是否可见

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

image.png假设现在您想改变这种行为以便在任何时候只展开一个面板。在这种设计下,展开第 2 个面板应会折叠第 1 个面板。您该如何做到这一点呢?”
要协调好这两个面板我们需要分 3 步将状态“提升”到他们的父组件中。

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

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

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

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

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

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

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

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

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

状态提升通常会改变原状态的数据存储类型
在这个例子中,一次只能激活一个面板。这意味着 Accordion 这个父组件需要记录 哪个 面板是被激活的面板。我们可以用数字作为当前被激活 Panel 的索引,而不是 boolean 值:

const [activeIndex, setActiveIndex] = useState(0);
import { useState } from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <h2>哈萨克斯坦,阿拉木图</h2>
      <Panel
        title="关于"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        阿拉木图人口约200万,是哈萨克斯坦最大的城市。它在 1929 年到 1997 年间都是首都。
      </Panel>
      <Panel
        title="词源"
        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>
  );
}

image.png

受控组件和非受控组件

通常我们包含“不受控制”状态的组件称为“非受控组件”。例如,最开始带有 isActive 状态变量的 Panel 组件就是不受控制的,因为其父组件无法控制面板的激活状态。
相反,当组件中的重要信息是由 props 而不是其自身状态驱动时,就可以认为该组件是“受控组件”。这就允许父组件完全指定行为最后带有 isActive 属性的 Panel 组件是由 Accordion 组件控制
练习1

import { useState } from 'react';

export default function SyncedInputs() {
  const [text,setText]= useState('')
  function handleChange(e){
    setText(e.target.value)
  }
  return (
    <>
      <Input label="第一个输入框" onChange={handleChange} text={text}/>
      <Input label="第二个输入框" onChange={handleChange} text={text}/>
    </>
  );
}

function Input({ label, onChange, text }) {
  //const [text, setText] = useState('');

  /*function handleChange(e) {
    setText(e.target.value);
  }*/

  return (
    <label>
      {label}
      {' '}
      <input
        value={text}
        onChange={onChange}
      />
    </label>
  );
}

练习2

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 (
    <>
      <SearchBar text={query} onChange={handleQuery}/>
      <hr />
      <List items={foods} text={query}/>
    </>
  );
}

function SearchBar({text,onChange}) {
  /*const [query, setQuery] = useState('');

  function handleChange(e) {
    setQuery(e.target.value);
  }*/

  return (
    <label>
      搜索{' '}
      <input
        value={text}
        onChange={onChange}
      />
    </label>
  );
}

function List({ items,text }) {
  return (
    <table>
      <tbody>
        {filterItems(items,text).map(food => (
          <tr key={food.id}>
            <td>{food.name}</td>
            <td>{food.description}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

对 state 进行保留和重置

状态与渲染树中的位置相关

只有当在树中相同的位置渲染相同的组件时,React 才会一直保留着组件的 state

import { useState } from 'react';

export default function App() {
  const [showB, setShowB] = useState(true);
  return (
    <div>
      <Counter />
      {showB &amp;&amp; <Counter />} 
      <label>
        <input
          type="checkbox"
          checked={showB}
          onChange={e => {
            setShowB(e.target.checked)
          }}
        />
        渲染第二个计数器
      </label>
    </div>
  );
}

function Counter() {
  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>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        加一
      </button>
    </div>
  );
}

注意,当你停止渲染第二个计数器的那一刻,它的 state 完全消失了。这是因为 React 在移除一个组件时,也会销毁它的 stateimage.png
当你重新勾选“渲染第二个计数器复选框时,另一个计数器及其 state 将从头开始初始化(score = 0)并被添加到 DOM 中。
image.png

相同位置的相同组件会使得 state 被保留下来

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <Counter isFancy={true} /> 
      ) : (
        <Counter isFancy={false} /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        使用好看的样式
      </label>
    </div>
  );
}

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 (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        加一
      </button>
    </div>
  );
}

image.png

相同位置的不同组件会使 state 重置

在这个例子中,勾选复选框会将 替换为一个

import { useState } from 'react';

export default function App() {
  const [isPaused, setIsPaused] = useState(false);
  return (
    <div>
      {isPaused ? (
        <p>待会见!</p> 
      ) : (
        <Counter /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isPaused}
          onChange={e => {
            setIsPaused(e.target.checked)
          }}
        />
        休息一下
      </label>
    </div>
  );
}

function Counter() {
  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>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        加一
      </button>
    </div>
  );
}

image.pngimage.png并且,当你在相同位置渲染不同的组件时,组件的整个子树都会被重置。要验证这一点,可以增加计数器的值然后勾选复选框

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <div>
          <Counter isFancy={true} /> 
        </div>
      ) : (
        <section>
          <Counter isFancy={false} />
        </section>
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        使用好看的样式
      </label>
    </div>
  );
}

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 (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        加一
      </button>
    </div>
  );
}

image.pngimage.png

在相同位置重置 state

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

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA &amp;&amp;
        <Counter person="Taylor" />
      }
      {!isPlayerA &amp;&amp;
        <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>
  );
}

image.png

方法二:使用 key 来重置 state

key 不只可以用于列表!你可以使用 key 来让 React 区分任何组件。默认情况下,React 使用父组件内部顺序(“第一个计数器”、“第二个计数器”)来区分组件。但是 key 可以让你告诉 React 这不仅仅是 第一个 或者 第二个 计数器,而且还是一个特定的计数器——例如,Taylor 的 计数器。这样无论它出现在树的任何位置, React 都会知道它是 Taylor 的 计数器!

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

为被移除的组件保留 state

在真正的聊天应用中,你可能会想在用户再次选择前一个收件人时恢复输入 state。对于一个不可见的组件,有几种方法可以让它的 state “活下去”:

练习1

import { useState } from 'react';

export default function App() {
  const [showHint, setShowHint] = useState(false);
  if (showHint) {
    return (
      <div>
        <p><i>提示:你最喜欢的城市?</i></p>
        <Form />
        <button onClick={() => {
          setShowHint(false);
        }}>隐藏提示</button>
      </div>
    );
  }
  return (
    <div>
      <p></p>
      <Form />
      <button onClick={() => {
        setShowHint(true);
      }}>显示提示</button>
    </div>
  );
}

function Form() {
  const [text, setText] = useState('');
  return (
    <textarea
      value={text}
      onChange={e => setText(e.target.value)}
    />
  );
}

练习2

import { useState } from 'react';

export default function App() {
  const [reverse, setReverse] = useState(false);
  let checkbox = (
    <label>
      <input
        type="checkbox"
        checked={reverse}
        onChange={e => setReverse(e.target.checked)}
      />
      调换顺序
    </label>
  );
  if (reverse) {
    return (
      <>
        <Field label="姓氏" key='姓氏' /> 
        <Field label="名字" key='名字'/>
        {checkbox}
      </>
    );
  } else {
    return (
      <>
        <Field label="名字" key='名字'/> 
        <Field label="姓氏" key='姓氏'/>
        {checkbox}
      </>
    );    
  }
}

function Field({ label }) {
  const [text, setText] = useState('');
  return (
    <label>
      {label}<input
        type="text"
        value={text}
        placeholder={label}
        onChange={e => setText(e.target.value)}
      />
    </label>
  );
}

练习3,4,5

迁移状态逻辑至 Reducer 中

使用 reducer 整合状态逻辑

随着组件复杂度的增加,你将很难一眼看清所有的组件状态更新逻辑。例如,下面的 TaskApp 组件有一个数类型的状态 tasks,并通过三个不同的事件处理程序实现任务添加删除修改

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,
      {
        id: nextId++,
        text: text,
        done: false,
      },
    ]);
  }

  function handleChangeTask(task) {
    setTasks(
      tasks.map((t) => {
        if (t.id === task.id) {
          return task;
        } 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},
];

你可以通过三个步骤将 useState 迁移到 useReducer

第 1 步: 将设置状态的逻辑修改dispatch 的一个 action

移除所有的状态设置逻辑。只留下三个事件处理函数

使用 reducers 管理状态与直接设置状态略有不同。它不是通过设置状态来告诉 React “要做什么”,而是通过事件处理程序 dispatch 一个 “action” 来指明 “用户刚刚做了什么”。(而状态更新逻辑保存在其他地方!)因此,我们不再通过事件处理器直接 “设置 task”,而是 dispatch 一个 “添加/修改/删除任务” 的 action。这更加符合用户的思维

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

第 2 步: 编写一个 reducer 函数

reducer 函数就是你放置状态逻辑的地方。它接受两个参数,分别为当前 state 和 action 对象,并且返回的是更新后的 state
在这个例子中,要将状态设置逻辑从事件处理程序移到 reducer 函数中,你需要:

  1. 声明当前状态(tasks)作为第一个参数
  2. 声明 action 对象作为第二个参数
  3. reducer 返回 下一个 状态(React 会将旧的状态设置为这个最新的状态)
function tasksReducer(tasks, action) {
  if (action.type === 'added') {
    return [
      ...tasks,
      {
        id: action.id,
        text: action.text,
        done: false,
      },
    ];
  } else if (action.type === 'changed') {
    return tasks.map((t) => {
      if (t.id === action.task.id) {
        return action.task;
      } else {
        return t;
      }
    });
  } else if (action.type === 'deleted') {
    return tasks.filter((t) => t.id !== action.id);
  } else {
    throw Error('未知 action: ' + action.type);
  }
}

上面的代码使用了 if/else 语句,但是在 reducers 中使用 switch 语句 是一种惯例。两种方式结果是相同的,但 switch 语句读起来一目了然

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('未知 action: ' + action.type);
    }
  }
}

Reduce()
reduce() 是 JavaScript 中数组高阶函数之一,用于数组中的元素归纳为单个值。它接受一个回调函数作为参数,这个回调函数可以进行累积操作

语法

array.reduce(callback(accumulator, currentValue, currentIndex, array), initialValue);

示例

// 计算数组元素的和
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
console.log(sum); // 输出 15

// 找出数组中的最大值
const max = numbers.reduce((accumulator, currentValue) => Math.max(accumulator, currentValue));
console.log(max); // 输出 5

使用场景

注意事项

  • 没有提供 initialValue 的情况下,reduce 将从数组的第二个元素开始调用回调函数。
  • 如果数组为空没有提供 initialValuereduce 将抛出 TypeError
  • 回调函数在数组有元素时至少会被调用一次
  • reduce 不会改变原数组。

示例

// 使用 reduce 计算数组中的平均值
const numbers = [10, 20, 30, 40, 50];
const average = numbers.reduce((accumulator, currentValue, index, array) => {
  accumulator += currentValue;
  if (index === array.length - 1) {
    return accumulator / array.length;
  } else {
    return accumulator;
  }
}, 0);

console.log(average); // 输出 30

在这个示例中,reduce 被用于计算数组 numbers 中所有元素的平均值

第 3 步: 在组件中使用 reducer

你需要将 tasksReducer 导入到组件中。记得先从 React 中导入 useReducer Hook

import { useReducer } from 'react';

接下来,你就可以替换掉之前的 useState:

const [tasks, setTasks] = useState(initialTasks);

只需要像下面这样使用 useReducer:

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

useReducer 和 useState 很相似——你必须给它传递一个初始状态,它会返回一个有状态的值和一个设置该状态的函数(在这个例子中就是 dispatch 函数)。但是,它们两个之间还是有点差异的。
useReducer 钩子接受 2 个参数

  1. 一个 reducer 函数
  2. 一个初始的 state

返回如下内容:

  1. 一个有状态的值
  2. 一个 dispatch 函数(用来 “派发” 用户操作给 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>布拉格的行程安排</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('未知 action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  {id: 0, text: '参观卡夫卡博物馆', done: true},
  {id: 1, text: '看木偶戏', done: false},
  {id: 2, text: '打卡列侬墙', done: false}
];

对比 useState 和 useReducer

  • 代码体积: 通常,在使用 useState 时,一开始只需要编写少量代码。而 useReducer 必须提前编写 reducer 函数和需要调度actions。但是,当多个事件处理程序以相似的方式修改 state 时,useReducer 可以减少代码量。
  • 可读性: 当状态更新逻辑足够简单时,useState 的可读性还行。但是,一旦逻辑变得复杂起来,它们会使组件变得臃肿且难以阅读。在这种情况下,useReducer 允许你将状态更新逻辑与事件处理程序分离开来。
  • 调试性: 当使用 useState 出现问题时, 你很难发现具体原因以及为什么。 而使用 useReducer 时, 你可以在 reducer 函数中通过打印日志方式来观察每个状态的更新,以及为什么要更新(来自哪个 action)。 如果所有 action 都没问题,你就知道问题出在了 reducer 本身的逻辑中。 然而,与使用 useState 相比,你必须单步执行更多的代码
  • 测试性: reducer 是一个不依赖于组件的纯函数。这就意味着你可以单独对它进行测试。一般来说,我们最好是在真实环境测试组件,但对于复杂的状态更新逻辑,针对特定的初始状态和 action,断言 reducer 返回的特定状态会很有帮助。
  • 个人偏好: 并不是所有人都喜欢用 reducer,没关系,这是个人偏好问题。你可以随时在 useState 和 useReducer 之间切换,它们能做的事情是一样的

练习1,2,3,4 暂时搁置

使用 Context 深层传递参数

通常来说,你会通过 props 将信息从父组件传递到子组件。但是,如果你必须通过许多中间组件向下传递 props,或是在你应用中的许多组件需要相同的信息,传递 props 会变的十分冗长和不便。Context 允许父组件向其下层无论多深的任何组件提供信息,而无需通过 props 显式传递

传递 props 带来的问题

image.png

Context:传递 props 的另一种方法

Context 让父组件可以为它下面的整个组件树提供数据
你可以通过以下三个步骤实现它:

  1. 创建 一个 context。(你可以将其命名为 LevelContext, 因为它表示的是标题级别。)
  2. 在需要数据的组件内 使用 刚刚创建context。(Heading 将会使用 LevelContext。)
  3. 指定数据的组件中 提供 这个 context。 (Section 将会提供 LevelContext。

image.png

写在你使用 context 之前

使用 Context 看起来非常诱人!然而,这也意味着它也太容易被过度使用了。如果你只想把一些 props 传递到多个层级中,这并不意味着你需要把这些信息放到 context
在使用 context 之前,你可以考虑以下几种替代方案:

  1. 传递 props 开始。 如果你的组件看起来不起眼,那么通过十几个组件向下传递一堆 props 并不罕见。这有点像是在埋头苦干,但是这样做可以让哪些组件用了哪些数据变得十分清晰!维护你代码的人会很高兴你用 props 让数据流变得更加清晰。
  2. 抽象组件并 将 JSX 作为children传递 给它们。 如果你通过很多层不使用该数据的中间组件(并且只会向下传递)来传递数据,这通常意味着你在此过程中忘记了抽象组件。举个例子,你可能想传递一些像 posts 的数据 props 到不会直接使用这个参数的组件,类似 。取而代之的是,让 Layout 把 children 当做一个参数,然后渲染 。这样就减少了定义数据的组件和使用数据的组件之间的层级

Context 的使用场景

传递 Context 的方法:

  1. 通过 export const MyContext = createContext(defaultValue) 创建导出 context。
  2. 在无论层级多深的任何子组件中,把 context 传递给 useContext(MyContext) Hook读取它。
  3. 在父组件中把 children 包在 <MyContext.Provider value={…}> 中来提供 context。

使用 Reducer 和 Context 拓展你的应用

下面将介绍如何结合使用 reducer 和 context:

  1. 创建 context。
  2. 将 state 和 dispatch 放入 context。
  3. 在组件树的任何地方 使用 context

应急方案

使用 ref 引用

当你希望组件“记住”某些信息,但又不想让这些信息 触发新的渲染 时,你可以使用 ref

给你的组件添加 ref

你可以通过从 React 导入 useRef Hook 来为你的组件添加一ref

import { useRef } from 'react';

在你的组件内,调用 useRef Hook 并传入你想要引用初始值作为唯一参数。例如,这里ref 引用的值是“0”:

const ref = useRef(0);

useRef 返回一个这样的对象:

{ 

  current: 0 // 你向 useRef 传入的值

}
import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert('你点击了 ' + ref.current + ' 次!');
  }

  return (
    <button onClick={handleClick}>
      点击我!
    </button>
  );
}

组件不会在每次递增时重新渲染。 与 state 一样,React 会在每次重新渲染之间保留 ref。但是,设置 state 会重新渲染组件,更改 ref 不会!

ref 和 state 的不同之处

image.png

useRef 内部是如何运行的?

尽管 useState 和 useRef 都是由 React 提供的,原则上 useRef 可以在 useState的基础上 实现。 你可以想象在 React 内部,useRef 是这样实现的:

// React 内部

function useRef(initialValue) {

  const [ref, unused] = useState({ current: initialValue });

  return ref;

}

何时使用 ref

ref 的最佳实践

遵循这些原则将使你的组件更具可预测性:

使用 ref 操作 DOM

获取指向节点的 ref

访问由 React 管理的 DOM 节点,首先,引入 useRef Hook:

import { useRef } from 'react';

然后,在你的组件中使用它声明一个 ref:

const myRef = useRef(null);

Finally, pass your ref as the ref attribute to the JSX tag for which you want to get the DOM node:

<div ref={myRef}>

useRef Hook 返回一个对象,该对象有一个名为 current 的属性。最初,myRef.current 是 null。当 React 为这个

创建一个 DOM 节点时,React 会把对该节点引用放入 myRef.current。然后,你可以从
事件处理器 访问此 DOM 节点,并使用在其上定义内置
浏览器 API

// 你可以使用任意浏览器 API,例如:

myRef.current.scrollIntoView();

在上面的例子中,ref 的数量是预先确定的。但有时候,你可能需要为列表中的每一项都绑定 ref ,而你又不知道会有多少项。像下面这样做是行不通的

<ul>

  {items.map((item) => {

  // 行不通!

  const ref = useRef(null);

  return <li ref={ref} />;

})}

</ul>

这是因为 Hook 只能在组件的顶层被调用。不能在循环语句条件语句或 map() 函数中调用 useRef

访问另一个组件的 DOM 节点

默认情况下,React 不允许组件访问其他组件的 DOM 节点。甚至自己的子组件也不行
想要 暴露其 DOM 节点的组件必须选择该行为。一个组件可以指定将它的 ref “转发”给一个子组件。下面是 MyInput 如何使用 forwardRef API:

const MyInput = forwardRef((props, ref) => {

  return <input {...props} ref={ref} />;

});

使用 Effect 同步

有些组件需要与外部系统同步。例如,你可能希望根据 React state 控制非 React 组件、设置服务器连接或在组件出现在屏幕上时发送分析日志。Effects 会在渲染后运行一些代码,以便可以将组件与 React 之外的某些系统同步
本文和后续文本中,Effect 在 React 中是专有定义——由渲染引起的副作用。为了指代更广泛的编程概念,也可以将其称为“副作用(side effect)

不要随意在你的组件中使用 Effect记住,Effect 通常用于暂时“跳出” React 代码并与一些 外部 系统进行同步。这包括浏览器 API、第三方部件,以及网络等等。如果你想用 Effect 仅根据其他状态调整某些状态,那么 你可能不需要 Effect

如何编写 Effect

第一步:声明 Effect

首先在 React 中引入 useEffect Hook

import { useEffect } from 'react';

然后,在组件顶部调用它,并传入在每次渲染时都需要执行的代码:

function MyComponent() {

  useEffect(() => {

    // 每次渲染后都会执行此处的代码

  });

  return <div />;

}

一般来说,Effect 会在 每次 渲染后执行而以下代码会陷入死循环

const [count, setCount] = useState(0);

useEffect(() => {

  setCount(count + 1);

});

每次渲染结束都会执行 Effect;而更新 state 会触发重新渲染。但是新一轮渲染时又会再次执行 Effect,然后 Effect 再次更新 state……如此周而复始,从而陷入死循环

第二步:指定 Effect 依赖

一般来说,Effect 会在 每次 渲染时执行。但更多时候,并不需要每次渲染的时候都执行 Effect

 useEffect(() => {
    if (isPlaying) { // isPlaying 在此处使用……
      // ...
    } else {
      // ...
    }
  }, [isPlaying]); // ……所以它必须在此处声明!

指定 [isPlaying] 会告诉 React,如果 isPlaying 在上一次渲染时与当前相同,它应该跳过重新运行 Effect
没有依赖数组作为第二个参数,与依赖数组位空数组 [] 的行为是不一致的:

useEffect(() => {

  // 这里的代码会在每次渲染后执行

});



useEffect(() => {

  // 这里的代码只会在组件挂载后执行

}, []);



useEffect(() => {

  //这里的代码只会在每次渲染后,并且 a 或 b 的值与上次渲染不一致时执行

}, [a, b]);

第三部:按需添加清理(cleanup)函数

可以在 Effect 中返回一个 清理(cleanup) 函数。

  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, []);

每次重新执行 Effect 之前,React 都会调用清理函数;组件被卸载时,也会调用清理函数

订阅事件

如果 Effect 订阅了某些事件,清理函数应该退订这些事件:

useEffect(() => {

  function handleScroll(e) {

    console.log(window.scrollX, window.scrollY);

  }

  window.addEventListener('scroll', handleScroll);

  return () => window.removeEventListener('scroll', handleScroll);

}, []);

触发动画

如果 Effect 对某些内容加入动画清理函数应将动画重置:

useEffect(() => {

  const node = ref.current;

  node.style.opacity = 1; // 触发动画

  return () => {

    node.style.opacity = 0; // 重置为初始值

  };

}, []);

初始化应用时不需要使用 Effect 的情形

某些逻辑应该只在应用程序启动运行一次。比如验证登陆状态和加载本地程序数据。你可以将其放在组件之外:

if (typeof window !== 'undefined') { // 检查是否浏览器运行

  checkAuthToken();

  loadDataFromLocalStorage();

}



function App() {

  // ……

}

你可能不需要 Effect

Effect 是 React 范式中的一个逃脱方案。它们让你可以 “逃出” React 并使组件和一些外部系统同步,比如非 React 组件、网络浏览器 DOM。如果没有涉及到外部系统(例如,你想根据 props 或 state 的变化来更新一个组件的 state),你就不应该使用 Effect

响应式 Effect 的生命周期

Effect 的生命周期

每个 React 组件都经历相同的生命周期

  • 当组件被添加到屏幕上时,它会进行组件的 挂载
  • 当组件接收到新的 props 或 state 时,通常是作为对交互的响应,它会进行组件的 更新
  • 当组件从屏幕上移除时,它会进行组件的 卸载

原文地址:https://blog.csdn.net/yirenDM/article/details/134633353

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_14511.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除!

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注