- 可以通过将函数作为 prop 传递给元素如
<button>
来处理事件。 - 必须传递事件处理函数,而非函数调用!
onClick={handleClick}
,不是onClick={handleClick()}
。 - 可以单独或者内联定义事件处理函数。
- 事件处理函数在组件内部定义,所以它们可以访问 props。
- 可以在父组件中定义一个事件处理函数,并将其作为 prop 传递给子组件。
- 可以根据特定于应用程序的名称定义事件处理函数的 prop。
- 事件会向上传播。通过事件的第一个参数调用
e.stopPropagation()
来防止这种情况。 - 事件可能具有不需要的浏览器默认行为。调用
e.preventDefault()
来阻止这种情况。 - 从子组件显式调用事件处理函数 prop 是事件传播的另一种优秀替代方案。
1.添加事件处理函数:
事件处理函数(自定义函数):在响应交互(如点击、悬停、表单输入框获得焦点等)时触发。
通常将事件处理程序命名为
handle
,后接事件名。你会经常看到onClick={handleClick}
,onMouseEnter={handleMouseEnter}
等。
先定义一个函数,然后将其作为prop传入合适的JSX标签。
按照如下三个步骤,即可让它在用户点击时显示消息:
- 在
Button
组件 内部 声明一个名为handleClick
的函数。 - 实现函数内部的逻辑(使用
alert
来显示消息)。 - 添加
onClick={handleClick}
到<button>
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 会立即执行。 如果按如下方式传递内联代码,并不会在点击时触发,而是会在每次组件渲染时触发:
<button onClick={alert('你点击了我!')}>
// 这个 alert 在组件渲染时触发,而不是点击时触发!
如果你想要定义内联事件处理函数,请将其包装在匿名函数中,如下所示:
<button onClick={(这是匿名函数) => alert('你点击了我!')}>
在这两种情况下,你都应该传递一个函数:
<button onClick={handleClick}>
传递了handleClick
函数。<button onClick={() => alert('...')}>
传递了() => alert('...')
函数。
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
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 标签。
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()
:
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>
);
}
当你点击按钮时:
- React 调用了传递给
<button>
的onClick
处理函数。 - 定义在
Button
中的处理函数执行了如下操作:- 调用
e.stopPropagation()
,阻止事件进一步冒泡。 - 调用
onClick
函数,它是从Toolbar
组件传递过来的 prop。
- 调用
- 在
Toolbar
组件中定义的函数,显示按钮对应的 alert。 - 由于传播被阻止,父级
<div>
的onClick
处理函数不会执行。
由于调用了 e.stopPropagation()
,点击按钮现在将只显示一个 alert(来自 <button>
),而并非两个(分别来自 <button>
和父级 toolbar <div>
)。点击按钮与点击周围的 toolbar 不同,因此阻止传播对这个 UI 是有意义的。
阻止默认行为
某些浏览器事件具有与事件相关联的默认行为。例如,点击 <form>
表单内部的按钮会触发表单提交事件,默认情况下将重新加载整个页面:
export default function Signup() {
return (
<form onSubmit={() => alert('提交表单!')}>
<input />
<button>发送</button>
</form>
);
}
你可以调用事件对象中的 e.preventDefault()
来阻止这种情况发生:
export default function Signup() {
return (
<form onSubmit={e => {
e.preventDefault();
alert('提交表单!');
}}>
<input />
<button>发送</button>
</form>
);
}
不要混淆 e.stopPropagation()
和 e.preventDefault()
。它们都很有用,但二者并不相关:
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再次渲染组件的时候,它会从头渲染,不会考虑之前对局部变量的任何更改,更新局部变量不会触发渲染。
- 保留 渲染之间的数据。
- 触发 React 使用新数据渲染组件(重新渲染)。
useState
Hook 提供了这两个功能:
- State 变量 用于保存渲染间的数据。
- State setter 函数 更新变量并触发 React 再次渲染组件。
添加一个 state 变量
//添加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你想让组件记住一些东西:
const [index,setIndex]=useState(0);
//这个例子中,你希望React记住index
//惯例是将这对返回值命名为 `const [thing, setThing]`。
useState的唯一参数是state变量的初始值,每次渲染时,useState会给你一个包含两个值的==数组==:1.state变量(index)会保存上次渲染的值。2.state setter函数(setIndex)可以更新state变量并且触发react重新渲染组件 以下是实际发生的情况:
const [index, setIndex] = useState(0);
- 组件进行第一次渲染。 因为你将
0
作为index
的初始值传递给useState
,它将返回[0, setIndex]
。 React 记住0
是最新的 state 值。 - 你更新了 state。当用户点击按钮时,它会调用
setIndex(index + 1)
。index
是0
,所以它是setIndex(1)
。这告诉 React 现在记住index
是1
并触发下一次渲染。 - 组件进行第二次渲染。React 仍然看到
useState(0)
,但是因为 React 记住 了你将index
设置为了1
,它将返回[1, setIndex]
。 - 以此类推!
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 重新渲染一个组件时:
- React 会再次调用你的函数
- 函数会返回新的 JSX 快照
- 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),可以通过替换他们的值触发重新渲染。
const[x,setX]=useState(0);
setX(5);//替换值。
//tate `x` 从 `0` 变为 `5`,但是数字 `0` 本身并没有发生改变。在 JavaScript 中,无法对内置的原始值,如数字、字符串和布尔值,进行任何更改。
错误:
onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
//这里直接更改了state函数
}}
onPointerMove={e => {
setPosition({ //使用新对象替换position值再次渲染组件
x: e.clientX,
y: e.clientY
});
}}
局部 mutation (突变)是可以接受的
像这样的代码是有问题的,因为它改变了 state 中现有的对象:
position.x = e.clientX;position.y = e.clientY;
但是像这样的代码就 没有任何问题,因为你改变的是你刚刚创建的一个新的对象:
const nextPosition = {};nextPosition.x = e.clientX;nextPosition.y = e.clientY;setPosition(nextPosition);
事实上,它完全等同于下面这种写法:
setPosition({ x: e.clientX, y: e.clientY});
只有当你改变已经处于 state 中的 现有 对象时,mutation 才会成为问题。而修改一个你刚刚创建的对象就不会出现任何问题,因为 还没有其他的代码引用它。改变它并不会意外地影响到依赖它的东西。这叫做“局部 mutation”。你甚至可以 在渲染的过程中 进行“局部 mutation”的操作。
使用展开语法复制对象
在之前的例子中,始终会根据当前指针的位置创建出一个新的 position
对象。但是通常,你会希望把 现有 数据作为你所创建的新对象的一部分。例如,你可能只想要更新表单中的一个字段,其他的字段仍然使用之前的值。
下面的代码中,输入框并不会正常运行,因为 onChange
直接修改了 state
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>
</>
);
}
更新一个嵌套对象
考虑下面这种结构的嵌套对象:
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 来实现的方法非常容易理解:
person.artwork.city = 'New Delhi';
但是在 React 中,你需要将 state 视为不可变的!为了修改 city
的值,你首先需要创建一个新的 artwork
对象(其中预先填充了上一个 artwork
对象中的数据),然后创建一个新的 person
对象,并使得其中的 artwork
属性指向新创建的 artwork
对象:
const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };setPerson(nextPerson);
或者,写成一个函数调用:
setPerson({ ...person, // 复制其它字段的数据
artwork: { // 替换 artwork 字段
...person.artwork, // 复制之前 person.artwork 中的数据
city: 'New Delhi' // 但是将 city 的值替换为 New Delhi!
}});
对象并非是嵌套的 而是指向
下面这个对象从代码上来看是“嵌套”的:
let obj = { name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg', }};
然而,当我们思考对象的特性时,“嵌套”并不是一个非常准确的方式。当这段代码运行的时候,不存在“嵌套”的对象。你实际上看到的是两个不同的对象:
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
:
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.city
和 obj1.city
。这是因为 obj3.artwork
、obj2.artwork
和 obj1
都指向同一个对象。当你用“嵌套”的方式看待对象时,很难看出这一点。相反,它们是相互独立的对象,只不过是用属性“指向”彼此而已。
使用 Immer 编写简洁的更新逻辑
更新 state 中的数组
- 你可以把数组放入 state 中,但你不应该直接修改它。不要直接修改数组,而是创建它的一份 新的 拷贝,然后使用新的数组来更新它的状态。 ![[Pasted image 20240629135858.png]]
陷阱
不幸的是,虽然 slice
和 splice
的名字相似,但作用却迥然不同:
slice
让你可以拷贝数组或是数组的一部分。splice
会直接修改 原始数组(插入或者删除元素)。
在 React 中,更多情况下你会使用 slice
(没有 p
!),因为你不想改变 state 中的对象或数组。更新对象这一章节解释了什么是 mutation,以及为什么不推荐在 state 里这样做。
- 你可以使用
[...arr, newItem]
这样的数组展开语法来向数组中添加元素。 - 你可以使用
filter()
和map()
来创建一个经过过滤或者变换的数组。 - 你可以使用 Immer 来保持代码简洁。
添加元素:
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 来重写的艺术愿望清单的例子:
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>
);
}
尝试一些挑战
示例(数组增删改查表):
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}
/>
</>
);
}