Skip to content
  • 可以通过将函数作为 prop 传递给元素如 <button> 来处理事件。
  • 必须传递事件处理函数,而非函数调用! onClick={handleClick} ,不是 onClick={handleClick()}
  • 可以单独或者内联定义事件处理函数。
  • 事件处理函数在组件内部定义,所以它们可以访问 props。
  • 可以在父组件中定义一个事件处理函数,并将其作为 prop 传递给子组件。
  • 可以根据特定于应用程序的名称定义事件处理函数的 prop。
  • 事件会向上传播。通过事件的第一个参数调用 e.stopPropagation() 来防止这种情况。
  • 事件可能具有不需要的浏览器默认行为。调用 e.preventDefault() 来阻止这种情况。
  • 从子组件显式调用事件处理函数 prop 是事件传播的另一种优秀替代方案。

1.添加事件处理函数:

事件处理函数(自定义函数):在响应交互(如点击、悬停、表单输入框获得焦点等)时触发。

通常将事件处理程序命名为 handle,后接事件名。你会经常看到 onClick={handleClick}onMouseEnter={handleMouseEnter} 等。

先定义一个函数,然后将其作为prop传入合适的JSX标签。

按照如下三个步骤,即可让它在用户点击时显示消息:

  1. 在 Button 组件 内部 声明一个名为 handleClick 的函数。
  2. 实现函数内部的逻辑(使用 alert 来显示消息)。
  3. 添加 onClick={handleClick} 到 <button> JSX 中。
jsx
export default function Button(){
  function handleClick(){
  alert('你点击了我');
  }
  return(
     <button  onClick={handleClick}>
     点我
     </button>
  );
}

或者,你也可以在 JSX 中定义一个内联的事件处理函数:
<button onClick={function handleClick() {  alert('你点击了我!');}}>

或者,直接使用更为简洁箭头函数:
<button onClick={() => {  alert('你点击了我!');}}>

陷阱

传递给事件处理函数的函数应直接传递,而非调用。

传递一个函数(正确)调用一个函数(错误)
<button onClick={handleClick}><button onClick={handleClick()}>

区别很微妙。在第一个示例中,handleClick 函数作为 onClick 事件处理函数传递。这会让 React 记住它,并且只在用户点击按钮时调用你的函数。

在第二个示例中,handleClick() 中最后的 () 会在 渲染 过程中 立即 触发函数,即使没有任何点击。这是因为在 JSX {} 之间的 JavaScript 会立即执行。 如果按如下方式传递内联代码,并不会在点击时触发,而是会在每次组件渲染时触发:

jsx
<button onClick={alert('你点击了我!')}>
// 这个 alert 在组件渲染时触发,而不是点击时触发!

如果你想要定义内联事件处理函数,请将其包装在匿名函数中,如下所示:

jsx
<button onClick={(这是匿名函数) => alert('你点击了我!')}>

在这两种情况下,你都应该传递一个函数:

  • <button onClick={handleClick}> 传递了 handleClick 函数。
  • <button onClick={() => alert('...')}> 传递了 () => alert('...') 函数。

了解更多箭头函数的信息

jsx
1.事件处理函数声明于组件内部,可以直接访问组件的props。

  function AlertButton({ message, children }) {
  return (
    <button onClick={() => alert(message)}>
      {children}
    </button>
  );
}
export default function Toolbar() {
  return (
    <div>
      <AlertButton message="正在播放!">
        播放电影
      </AlertButton>
      <AlertButton message="正在上传!">
        上传图片
      </AlertButton>
    </div>
  );
}

2.可以将事件处理函数作为props传递
//子组件Button
function Button({onClick,children}){
  return(
    <button onClick={onClick}>
       {children}
       </button>
  );
}
//子组件播放按钮
function PlayButton({movieName}){
   function handlePlayClick(){
   alert('正在播放${movieName}!')}
   //ES6字符串模版实现变量和字符串的拼接

   return(
   <Button onClick={handlePlayClick}>
   播放"{movieName}"
   </Button>
   );
}
//子组件上传按钮
function UploadButton(){
  return(
  <Button  onClick={()=>alert('正在上传')}>
  上传图片
  </Button>
  );
}
//根组件
export default function Toolbar(){
  return(
  <div>
     <PlayButton  movieName="怦然心动" />
     <UploadButton />
   </div>
   );
}

Toolbar 组件渲染了一个 PlayButton 组件和 UploadButton 组件:

  • PlayButton 将 handlePlayClick 作为 onClick prop 传入 Button 组件内部。
  • UploadButton 将 () => alert('正在上传!') 作为 onClick prop 传入 Button 组件内部。

最后,你的 Button 组件接收一个名为 onClick 的 prop。它直接将这个 prop 以 onClick={onClick} 方式传递给浏览器内置的 <button>。当点击按钮时,React 会调用传入的函数。

==命名:事件处理函数 props 应该以 on 开头,后跟一个大写字母。== 例如:Button 组件的 onClick prop 本来也可以被命名为 onSmash

jsx
function Button ({onSmash,children}){
   return(
   <button onClick ={onSmash}>
   //浏览器内置的button需要用onClick,自定义的Button组件prop可以自定义
   {children}
   </button>
   );
}
export default function App(){
return(
   <div>
     <Button onSmash={()=>alert('正在播放')}>
     </Button>
     <Button onSmash={()=>alert('正在播放')}>
     </Button>
   </div>
)
}

2.事件传播:

事件处理函数将捕获任何子组件的事件。事件会沿着树向上冒泡和传播。 如果你点击任一按钮,它自身的 onClick 将首先执行,然后父级 <div>onClick 会接着执行。因此会出现两条消息。如果你点击 toolbar 本身,将只有父级 <div>onClick 会执行。在 React 中所有事件都会传播,除了 onScroll,它仅适用于你附加到的 JSX 标签。

jsx
export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('你点击了 toolbar !');
    }}>
      <button onClick={() => alert('正在播放!')}>
        播放电影
      </button>
      <button onClick={() => alert('正在上传!')}>
        上传图片
      </button>
    </div>
  );
}

阻止传播

事件处理函数接收一个 事件对象 作为唯一的参数。按照惯例,它通常被称为 e ,代表 “event”(事件)。使用此对象来读取有关事件的信息。

这个事件对象还允许你阻止传播。如果你想阻止一个事件到达父组件,你需要像下面 Button 组件那样调用 e.stopPropagation()

jsx
function Button({ onClick, children }) {
  return (
    <button onClick={e => {
      e.stopPropagation();
      onClick();
    }}>
      {children}
    </button>
  );
}

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('你点击了 toolbar !');
    }}>
      <Button onClick={() => alert('正在播放!')}>
        播放电影
      </Button>
      <Button onClick={() => alert('正在上传!')}>
        上传图片
      </Button>
    </div>
  );
}

当你点击按钮时:

  1. React 调用了传递给 <button> 的 onClick 处理函数。
  2. 定义在 Button 中的处理函数执行了如下操作:
    • 调用 e.stopPropagation(),阻止事件进一步冒泡。
    • 调用 onClick 函数,它是从 Toolbar 组件传递过来的 prop。
  3. 在 Toolbar 组件中定义的函数,显示按钮对应的 alert。
  4. 由于传播被阻止,父级 <div> 的 onClick 处理函数不会执行。

由于调用了 e.stopPropagation(),点击按钮现在将只显示一个 alert(来自 <button>),而并非两个(分别来自 <button> 和父级 toolbar <div>)。点击按钮与点击周围的 toolbar 不同,因此阻止传播对这个 UI 是有意义的。

阻止默认行为

某些浏览器事件具有与事件相关联的默认行为。例如,点击 <form> 表单内部的按钮会触发表单提交事件,默认情况下将重新加载整个页面:

jsx
export default function Signup() {
  return (
    <form onSubmit={() => alert('提交表单!')}>
      <input />
      <button>发送</button>
    </form>
  );
}

你可以调用事件对象中的 e.preventDefault() 来阻止这种情况发生:

jsx
export default function Signup() {
  return (
    <form onSubmit={e => {
      e.preventDefault();
      alert('提交表单!');
    }}>
      <input />
      <button>发送</button>
    </form>
  );
}

不要混淆 e.stopPropagation()e.preventDefault()。它们都很有用,但二者并不相关:

stopPropagation可以以这个为模版: ![[Pasted image 20240626134334.png]]

事件处理函数包含副作用

与渲染函数(组件)不同,事件处理函数不需要是 纯函数,因此它是用来 更改 某些值的绝佳位置。例如,更改输入框的值以响应键入,或者更改列表以响应按钮的触发。但是,为了更改某些信息,你首先需要某种方式存储它。在 React 中,这是通过 state(组件的记忆) 来完成的。

State:组件记忆

  • 当一个组件需要在多次渲染间“记住”某些信息时使用 state 变量。
  • State 变量是通过调用 useState Hook 来声明的。
  • Hook 是以 use 开头的特殊函数。它们能让你 “hook” 到像 state 这样的 React 特性中。
  • Hook 可能会让你想起 import:它们需要在非条件语句中调用。调用 Hook 时,包括 useState,仅在组件或另一个 Hook 的顶层被调用才有效。
  • useState Hook 返回一对值:当前 state 和更新它的函数。
  • 你可以拥有多个 state 变量。在内部,React 按顺序匹配它们。
  • State 是组件私有的。如果你在两个地方渲染它,则每个副本都有独属于自己的 state。 Q:为什么React需要state? A:定义在组件中的局部变量无法多次渲染持久保存,当React再次渲染组件的时候,它会从头渲染,不会考虑之前对局部变量的任何更改,更新局部变量不会触发渲染。
  1. 保留 渲染之间的数据。
  2. 触发 React 使用新数据渲染组件(重新渲染)。

useState Hook 提供了这两个功能:

  1. State 变量 用于保存渲染间的数据。
  2. State setter 函数 更新变量并触发 React 再次渲染组件。

添加一个 state 变量

jsx
//添加state变量要先从文件顶部React中导入useState
import {useState}from 'react';
//替换局部变量修改为:这里的 `[` 和 `]` 语法称为[数组解构]它允许你从数组中读取值。 `useState` 返回的数组总是正好有两项。
const [index,setIndex]=useState(0);
//index是一个state变量,setIndex是对应的setter函数
//定义函数设置setIndex函数
function handleClick(){
 setIndex(index+1);
}

在 React 中,useState 以及任何其他以“use”开头的函数都被称为 Hook

陷阱

Hooks ——以 use 开头的函数——只能在组件或自定义 Hook 的最顶层调用。 你不能在条件语句、循环语句或其他嵌套函数内调用 Hook。在组件顶部 “use” React 特性,类似于在文件顶部“导入”模块。

剖析useState

调用useState,是在告诉React你想让组件记住一些东西:

jsx
const [index,setIndex]=useState(0);
//这个例子中,你希望React记住index
//惯例是将这对返回值命名为 `const [thing, setThing]`。

useState的唯一参数是state变量的初始值,每次渲染时,useState会给你一个包含两个值的==数组==:1.state变量(index)会保存上次渲染的值。2.state setter函数(setIndex)可以更新state变量并且触发react重新渲染组件 以下是实际发生的情况:

jsx
const [index, setIndex] = useState(0);
  1. 组件进行第一次渲染。 因为你将 0 作为 index 的初始值传递给 useState,它将返回 [0, setIndex]。 React 记住 0 是最新的 state 值。
  2. 你更新了 state。当用户点击按钮时,它会调用 setIndex(index + 1)。 index 是 0,所以它是 setIndex(1)。这告诉 React 现在记住 index 是 1 并触发下一次渲染。
  3. 组件进行第二次渲染。React 仍然看到 useState(0),但是因为 React 记住 了你将 index 设置为了 1,它将返回 [1, setIndex]
  4. 以此类推!

Q:React 如何知道返回哪个 state? A:遵循顶层调用Hooks,Hooks将始终以相同顺序被调用。

State是隔离且私有的:这个直接看例子 ![[Pasted image 20240628183840.png]] 在这个例子中,之前的 Gallery 组件以同样的逻辑被渲染了两次。试着点击每个画廊内的按钮。你会注意到它们的 state 是相互独立的:如果你渲染同一个组件两次,每个副本都会有完全隔离的 state!改变其中一个不会影响另一个。与 props 不同,state 完全私有于声明它的组件。父组件无法更改它。这使你可以向任何组件添加或删除 state,而不会影响其他组件。 完成挑战:State:组件的记忆 – React 中文文档

后面几节的知识点比较零碎我就写在一起了

渲染和提交

Q:为什么渲染并不一定会导致 DOM 更新? A:如果渲染结果与上次一样,那么 React 将不会修改 DOM

==触发渲染->渲染组件->提交到DOM==

1.触发渲染

1.组件初次渲染 2.组件或者祖先之一的状态发生改变重新渲染

2.React 渲染你的组件

根组件->内部状态变化触发渲染的组件(递归调用的过程)

陷阱

渲染必须始终是一次 纯计算:

  • 输入相同,输出相同。 给定相同的输入,组件应始终返回相同的 JSX。(当有人点了西红柿沙拉时,他们不应该收到洋葱沙拉!)
  • 只做它自己的事情。 它不应更改任何存在于渲染之前的对象或变量。(一个订单不应更改其他任何人的订单。)

3.React 把更改提交到 DOM 上

设置 state 会触发渲染,渲染会及时生成一张快照

“正在渲染” 就意味着 React 正在调用你的组件——一个函数。你从该函数返回的 JSX 就像是在某个时间点上 UI 的快照。它的 props、事件处理函数和内部变量都是 根据当前渲染时的 state 被计算出来的。 state 如同一张快照 – React 中文文档文档有图很清晰 当 React 重新渲染一个组件时:

  1. React 会再次调用你的函数
  2. 函数会返回新的 JSX 快照
  3. React 会更新界面以匹配返回的快照 ![[Pasted image 20240629104651.png]] ![[Pasted image 20240629104709.png]]
  • 设置 state 不会更改现有渲染中的变量,但会请求一次新的渲染。
  • React 会在事件处理函数执行完成之后处理 state 更新。这被称为批处理。
  • 要在一个事件中多次更新某些 state,你可以使用 setNumber(n => n + 1) 更新函数。
  • 将 React 中所有的 state 都视为不可直接修改的。(state可以保存任意类型的js值和对象,但是不能直接修改存放在React State中的对象,当要更新一个对象时要创建一个新对象)
  • 当你在 state 中存放对象时,直接修改对象并不会触发重渲染,并会改变前一次渲染“快照”中 state 的值。
  • 不要直接修改一个对象,而要为它创建一个  版本,并通过把 state 设置成这个新版本来触发重新渲染。
  • 你可以使用这样的 {...obj, something: 'newValue'} 对象展开语法来创建对象的拷贝。
  • 对象的展开语法是浅层的:它的复制深度只有一层。
  • 想要更新嵌套对象,你需要从你更新的位置开始自底向上为每一层都创建新的拷贝。
  • 想要减少重复的拷贝代码,可以使用 Immer。

什么是mutation?

可以在state中存放任意类型的Javascript值,这些值是不可变的(immutable),可以通过替换他们的值触发重新渲染。

jsx
const[x,setX]=useState(0);
setX(5);//替换值。
//tate `x` 从 `0` 变为 `5`,但是数字 `0` 本身并没有发生改变。在 JavaScript 中,无法对内置的原始值,如数字、字符串和布尔值,进行任何更改。
jsx
错误:
onPointerMove={e => {  
position.x = e.clientX;  
position.y = e.clientY;  
//这里直接更改了state函数
}}
onPointerMove={e => {  
setPosition({ //使用新对象替换position值再次渲染组件
x: e.clientX,  
y: e.clientY  
});  
}}

局部 mutation (突变)是可以接受的

像这样的代码是有问题的,因为它改变了 state 中现有的对象:

jsx
position.x = e.clientX;position.y = e.clientY;

但是像这样的代码就 没有任何问题,因为你改变的是你刚刚创建的一个新的对象:

jsx
const nextPosition = {};nextPosition.x = e.clientX;nextPosition.y = e.clientY;setPosition(nextPosition);

事实上,它完全等同于下面这种写法:

jsx
setPosition({  x: e.clientX,  y: e.clientY});

只有当你改变已经处于 state 中的 现有 对象时,mutation 才会成为问题。而修改一个你刚刚创建的对象就不会出现任何问题,因为 还没有其他的代码引用它。改变它并不会意外地影响到依赖它的东西。这叫做“局部 mutation”。你甚至可以 在渲染的过程中 进行“局部 mutation”的操作。

使用展开语法复制对象

在之前的例子中,始终会根据当前指针的位置创建出一个新的 position 对象。但是通常,你会希望把 现有 数据作为你所创建的新对象的一部分。例如,你可能只想要更新表单中的一个字段,其他的字段仍然使用之前的值。

下面的代码中,输入框并不会正常运行,因为 onChange 直接修改了 state

jsx
import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleFirstNameChange(e) {
    person.firstName = e.target.value;//这样是错误的
  }

  function handleLastNameChange(e) {
    person.lastName = e.target.value;//这样是错误的
  }

  function handleEmailChange(e) {
    person.email = e.target.value;//这样是错误的
  }
  setPerson({
   firstName:e.target.value,
   lastName:person.lastName,
   email:person.email
  });
  //可以使用对象展开语法,请注意 `...` 展开语法本质是是“浅拷贝”——它只会复制一层。这使得它的执行速度很快,但是也意味着当你想要更新一个嵌套属性时,你必须得多次使用展开语法。
  setPerson({
  ...person,
  firstName:e.target.value
  });

  return (
    <>
      <label>
        First name:
        <input
          value={person.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:
        <input
          value={person.lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Email:
        <input
          value={person.email}
          onChange={handleEmailChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

更新一个嵌套对象

考虑下面这种结构的嵌套对象:

jsx
const [person, setPerson] = useState({ 
name: 'Niki de Saint Phalle', 
artwork: { 
title: 'Blue Nana',   
city: 'Hamburg', 
image: 'https://i.imgur.com/Sd1AgUOm.jpg',  }});

如果你想要更新 person.artwork.city 的值,用 mutation 来实现的方法非常容易理解:

jsx
person.artwork.city = 'New Delhi';

但是在 React 中,你需要将 state 视为不可变的!为了修改 city 的值,你首先需要创建一个新的 artwork 对象(其中预先填充了上一个 artwork 对象中的数据),然后创建一个新的 person 对象,并使得其中的 artwork 属性指向新创建的 artwork 对象:

jsx
const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };setPerson(nextPerson);

或者,写成一个函数调用:

jsx
setPerson({  ...person, // 复制其它字段的数据  
		   artwork: { // 替换 artwork 字段    
		   
		    ...person.artwork, // 复制之前 person.artwork 中的数据    
		    city: 'New Delhi' // 但是将 city 的值替换为 New Delhi!  
		    }});

对象并非是嵌套的 而是指向

下面这个对象从代码上来看是“嵌套”的:

jsx
let obj = {  name: 'Niki de Saint Phalle',  
		   artwork: {    
		   title: 'Blue Nana', 
		   city: 'Hamburg', 
	       image: 'https://i.imgur.com/Sd1AgUOm.jpg',  }};

然而,当我们思考对象的特性时,“嵌套”并不是一个非常准确的方式。当这段代码运行的时候,不存在“嵌套”的对象。你实际上看到的是两个不同的对象:

jsx
let obj1 = {title: 'Blue Nana', 
			city: 'Hamburg', 
			 image: 'https://i.imgur.com/Sd1AgUOm.jpg',};
			 let obj2 = { 
			  name: 'Niki de Saint Phalle',  
			  artwork: obj1};

对象 obj1 并不处于 obj2 的“内部”。例如,下面的代码中,obj3 中的属性也可以指向 obj1

jsx
let obj1 = { 
title: 'Blue Nana', 
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',};
let obj2 = { 
name: 'Niki de Saint Phalle', 
artwork: obj1};
let obj3 = {  
name: 'Copycat', 
artwork: obj1};

如果你直接修改 obj3.artwork.city,就会同时影响 obj2.artwork.cityobj1.city。这是因为 obj3.artworkobj2.artworkobj1 都指向同一个对象。当你用“嵌套”的方式看待对象时,很难看出这一点。相反,它们是相互独立的对象,只不过是用属性“指向”彼此而已。

使用 Immer 编写简洁的更新逻辑 

更新 state 中的数组

  • 你可以把数组放入 state 中,但你不应该直接修改它。不要直接修改数组,而是创建它的一份 新的 拷贝,然后使用新的数组来更新它的状态。 ![[Pasted image 20240629135858.png]]

陷阱

不幸的是,虽然 slicesplice 的名字相似,但作用却迥然不同:

  • slice 让你可以拷贝数组或是数组的一部分。
  • splice 会直接修改 原始数组(插入或者删除元素)。

在 React 中,更多情况下你会使用 slice(没有 p !),因为你不想改变 state 中的对象或数组。更新对象这一章节解释了什么是 mutation,以及为什么不推荐在 state 里这样做。

  • 你可以使用 [...arr, newItem] 这样的数组展开语法来向数组中添加元素。
  • 你可以使用 filter() 和 map() 来创建一个经过过滤或者变换的数组。
  • 你可以使用 Immer 来保持代码简洁。
jsx
添加元素:
setArr(//替换state
[//是通过传入一个新数组实现的
   ...arrs,//原来数组所有元素
   {id:nextId++,name:name}]//末尾添加新元素
);
示例:
setArtists([  
{ id: nextId++, name: name },  
...artists // 将原数组中的元素放在末尾  

]);
删除元素:
setArtists(
   artists.filter(a=>a.id!==artist.id)
   //这里,`artists.filter(s => s.id !== artist.id)` 表示“创建一个新的数组,该数组由那些 ID 与 `artists.id` 不同的 `artists` 组成”。换句话说,每个 artist 的“删除”按钮会把 _那一个_ artist 从原始数组中过滤掉,并使用过滤后的数组再次进行渲染。注意,`filter` 并不会改变原始数组。
);

转换数组:
用map创建一个新数组:
原始数组:
const[shapes,setShapes]=useState(
  initialShapes
);
map创建新数组:
const nextShapes =shapes.map(shape=>{
    if(shape.type==='square'){
      //不做改变
      return shape;
     }else{
        return {
        ...shape,
        y:shape.y+50,
        };
     }
});
setShapes(nextShapes);//使用新数组重新渲染

替换数组中的元素:要替换一个元素,请使用 `map` 创建一个新数组。在你的 `map` 回调里,第二个参数是元素的索引。使用索引来判断最终是返回原始的元素(即回调的第一个参数)还是替换成其他值:
function handleIncrementClick(index) {
    const nextCounters = counters.map((c, i) => {
      if (i === index) {
        // 递增被点击的计数器数值
        return c + 1;
      } else {
        // 其余部分不发生变化
        return c;
      }
    });
    setCounters(nextCounters);
  }
  插入元素:
  function handleClick() {
    const insertAt = 1; // 可能是任何索引
    const nextArtists = [
      // 插入点之前的元素:
      ...artists.slice(0, insertAt),
      // 新的元素:
      { id: nextId++, name: name },
      // 插入点之后的元素:
      ...artists.slice(insertAt)
    ];
    setArtists(nextArtists);
    setName('');
  }
  反转数组和数组排序需要先拷贝一遍数组在新数组上更新状态,不能直接改变原数组
   function handleClick() {
    const nextList = [...list];
    nextList.reverse();
    setList(nextList);
  }

此处的 ... 是一个对象展开语法,被用来创建一个对象的拷贝.

使用 Immer 编写简洁的更新逻辑 

在没有 mutation 的前提下更新嵌套数组可能会变得有点重复。就像对对象一样:

  • 通常情况下,你应该不需要更新处于非常深层级的 state 。如果你有此类需求,你或许需要调整一下数据的结构,让数据变得扁平一些。
  • 如果你不想改变 state 的数据结构,你也许会更喜欢使用 Immer ,它让你可以继续使用方便的,但会直接修改原值的语法,并负责为你生成拷贝值。 下面是我们用 Immer 来重写的艺术愿望清单的例子:
jsx
import{useState}from'react';
import { useImmer } from 'use-immer';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
  const [myList, updateMyList] = useImmer(
    initialList
  );
  const [yourList, updateYourList] = useImmer(
    initialList
  );
  function handleToggleMyList(id, nextSeen) {
    updateMyList(draft => {
      const artwork = draft.find(a =>
        a.id === id
      );
      artwork.seen = nextSeen;
    });
  }
  function handleToggleYourList(artworkId,nextSeen){
     updateYourList(draft=>{
        const artwork=draft.find(a=>a.id===artworkId);
        artwork.seen=nextSeen;});
        }
 return (
    <>
      <h1>艺术愿望清单</h1>
      <h2>我想看的艺术清单:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
      <h2>你想看的艺术清单:</h2>
      <ItemList
        artworks={yourList}
        onToggle={handleToggleYourList} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

尝试一些挑战

示例(数组增删改查表):

jsx
import { useState } from 'react';
import AddTodo from './AddTodo.js';
import TaskList from './TaskList.js';

let nextId = 3;
const initialTodos = [
  { id: 0, title: 'Buy milk', done: true },
  { id: 1, title: 'Eat tacos', done: false },
  { id: 2, title: 'Brew tea', done: false },
];

export default function TaskApp() {
  const [todos, setTodos] = useState(
    initialTodos
  );

  function handleAddTodo(title) {
    setTodos([
      ...todos,
      {
        id: nextId++,
        title: title,
        done: false
      }
    ]);
  }

  function handleChangeTodo(nextTodo) {
    setTodos(todos.map(t => {
      if (t.id === nextTodo.id) {
        return nextTodo;
      } else {
        return t;
      }
    }));
  }

  function handleDeleteTodo(todoId) {
    setTodos(
      todos.filter(t => t.id !== todoId)
    );
  }

  return (
    <>
      <AddTodo
        onAddTodo={handleAddTodo}
      />
      <TaskList
        todos={todos}
        onChangeTodo={handleChangeTodo}
        onDeleteTodo={handleDeleteTodo}
      />
    </>
  );
}
Immer形式
import { useState } from 'react';
import { useImmer } from 'use-immer';
import AddTodo from './AddTodo.js';
import TaskList from './TaskList.js';

let nextId = 3;
const initialTodos = [
  { id: 0, title: 'Buy milk', done: true },
  { id: 1, title: 'Eat tacos', done: false },
  { id: 2, title: 'Brew tea', done: false },
];

export default function TaskApp() {
  const [todos, setTodos] = useState(
    initialTodos
  );

  function handleAddTodo(title) {
    todos.push({
      id: nextId++,
      title: title,
      done: false
    });
  }

  function handleChangeTodo(nextTodo) {
    const todo = todos.find(t =>
      t.id === nextTodo.id
    );
    todo.title = nextTodo.title;
    todo.done = nextTodo.done;
  }

  function handleDeleteTodo(todoId) {
    const index = todos.findIndex(t =>
      t.id === todoId
    );
    todos.splice(index, 1);
  }

  return (
    <>
      <AddTodo
        onAddTodo={handleAddTodo}
      />
      <TaskList
        todos={todos}
        onChangeTodo={handleChangeTodo}
        onDeleteTodo={handleDeleteTodo}
      />
    </>
  );
}

Copyright © 2024-present LofiSu