本文目录:
- 1.React入口
- 2.JSX语法
- 3.组件
- 4.正确使用setState
- 5.生命周期
- 6.组件复合
- 7.redux
- 8.react-redux
- 9.react-router
- 10.PureComponent
- 11.认识Hook
- 12.自定义Hook与Hook使用规则
- 13.Hook API之useMemo与useCallback
参考网址:https://zh-hans.reactjs.org/docs/getting-started.html
1.React入口
起步
- 创建项目: npx create-react-app my-app
- 打开项目: cd my-app
- 启动项目: npm start
- 暴露配置项: npm run eject
注意:暴露配置项操作是不可逆的
cra文件结构
├── README.md 文档
├── public 静态资源
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
└── src 源码
├── App.css
├── App.js 根组件
├── App.test.js
├── index.css 全局样式
├── index.js 入口文文件
├── logo.svg
└── serviceWorker.js pwa支持
├── package.json npm 依赖
入口文件定义,webpack.config.js
entry: [
// WebpackDevServer客户端,它实现开发时热更新功能
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
// 应用程序入口:src/index
paths.appIndexJs,
].filter(Boolean),
webpack.config.js 是webpack配置⽂文件,开头的常量声明可以看出cra能够支持ts、sass及css模块化。
// Check if TypeScript is setup
const useTypeScript = fs.existsSync(paths.appTsConfig);
// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
React和ReactDom
删除src下面所有代码,新建index.js。
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(<h1>Hello React</h1>, document.querySelector('#root'));
React负责逻辑控制,数据 -> VDOM
ReactDom渲染实际DOM,VDOM -> DOM
React使用JSX来描述UI
babel-loader把JSX 编译成相应的 JS 对象,React.createElement再把这个JS对象构造成React需要的虚拟dom。
2.JSX语法
JSX是一种JavaScript的语法扩展,其格式⽐比较像模版语言,但事实上完全是在JavaScript内部实现的。
JSX可以很好地描述UI,能够有效提高开发效率,
2.1.基本使用
表达式{}的使用,index.js
const name = "react study";
const jsx = <div>hello, {name}</div>;
2.2.函数
函数也是合法表达式,index.js
const obj = {
fistName: "Harry",
lastName: "Potter"
};
function formatName(name) {
return name.fistName + " " + name.lastName;
}
const jsx = <div>{formatName(user)}</div>;
2.3.对象
jsx是js对象,也是合法表达式,index.js
const greet = <div>good</div>;
const jsx = <div>{greet}</div>;
2.4.条件语句
条件语句可以基于上⾯结论实现,index.js
const show = true;//false;
const greet = <div>good</div>;
const jsx = (
<div>
{/* 条件语句 */}
{show ? greet : "登录"}
{show && greet}
</div>
);
2.5.数组
数组会被作为一组子元素对待,数组中存放一组jsx可用于显示列表数据
const a = [0, 1, 2];
const jsx = (
<div>
{/* 数组 */}
<ul>
{/* diff时候,⾸首先⽐比较type,然后是key,所以同级同类型元素,key值必须得 唯一 */}
{a.map(item => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
2.6.属性的使用
import logo from "./logo.svg";
const jsx = (
<div>
{/* 属性:静态值用双引号,动态值用花括号;class、for等要特殊处理。 */}
<img src={logo} style={{ width: 100 }} className="img" />
</div>
);
2.7.模块化
css模块化,创建index.module.css,index.js
import style from "./index.module.css";
<img className={style.logo} />
或者npm install sass -D
import style from "./index.module.scss";
<img className={style.logo} />
更多css modules规则参考
http://www.ruanyifeng.com/blog/2016/06/css_modules.html
3.组件
组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。
组件有两种形式:class组件和function组件。
3.1.class组件
class组件通常拥有状态和生命周期,继承于Component,实现render方法。用class组件创建⼀个Clock
import React, {
Component
} from "react";
export default class ClassComponent extends React.Component {
constructor(props) {
super(props);
// 使用state属性维护状态,在构造函数中初始化状态
this.state = {
date: new Date()
};
}
componentDidMount() {
// 组件挂载之后启动定时器每秒更新状态
this.timerID = setInterval(() => {
// 使用setState⽅方法更新状态
this.setState({
date: new Date()
});
}, 1000);
}
componentWillUnmount() {
// 组件卸载前停止定时器
clearInterval(this.timerID);
}
componentDidUpdate() {
console.log("componentDidUpdate");
}
render() {
return <div > {
this.state.date.toLocaleTimeString()
} < /div>;
}
}
3.2.function组件
函数组件通常无状态,仅关注内容展示,返回渲染结果即可
从React16.8开始引入了hooks,函数组件也能够拥有状态。
用function组件创建一个Clock:
import React, { useState, useEffect } from "react";
export function FunctionComponent(props) {
const [date, setDate] = useState(new Date());
useEffect(() => {//副作用
const timer = setInterval(() => {
setDate(new Date());
}, 1000);
return () => clearInterval(timer);//组件卸载的时候执行
}, []);
return (
<div>
<h3>FunctionComponent</h3>
<p>{date.toLocaleTimeString()}</p>
</div>
);
}
提示: 如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做componentDidMount , componentDidUpdate 和componentWillUnmount 这三个函数的组合。
4.正确使用setState
setState(partialState, callback)
- partialState : object|function
用于产生与当前state合并的子集。 - callback : function
state更新之后被调用。
4.1.关于 setState() 你应该了了解三件事:
不要直接修改 State
例如,此代码不会重新渲染组件:
// 错误示范
this.state.comment = 'Hello';
⽽是应该使用 setState() :
// 正确使用
this.setState({comment: 'Hello'});
4.2.State 的更新可能是异步的
出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。
观察以下例例子中log的值和button显示的counter。
import React, {
Component
} from "react";
export default class SetStatePage extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0
};
}
changeValue = v => {
this.setState({
counter: this.state.counter + v
});
console.log("counter", this.state.counter);
};
setCounter = () => {
this.changeValue(1);
//this.changeValue(2);
//console.log("counter", this.state.counter);
};
render() {
const {
counter
} = this.state;
return (
< div >
<h3 > SetStatePage < /h3>
<button onClick = {this.setCounter} >
{counter}
< /button>
</div>
);
}
}
如果要获取到最新状态值有以下⽅式:
1.在回调中获取状态值
changeValue = v => {
this.setState({
counter: this.state.counter + v
},
() => {
console.log("counter", this.state.counter);
}
);
};
- 使用定时器:
setTimeout(() => {
this.setCounter();
}, 0);
- 原生事件中修改状态
componentDidMount(){
document.body.addEventListener('click', this.changeValue, false)
}
总结: setState只有在合成事件和生命周期函数中是异步的,在原生事件和setTimeout中都是同步的,这里的异步其实是批量更新。
4.3.State 的更新会被合并
changeValue = v => {
this.setState({
counter: this.state.counter + v
});
};
setCounter = () => {
this.changeValue(1);
this.changeValue(2);
};
此时的 this.changeValue(1); 不会生效
如果想要链式更新state:
changeValue = v => {
this.setState(state => ({
counter: state.counter + v
}));
};
setCounter = () => {
this.changeValue(1);
this.changeValue(2);
};
5.生命周期
参考文档:https://zh-hans.reactjs.org/docs/react-component.html#constructor
当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
- constructor()
- static getDerivedStateFromProps()
- render()
- componentDidMount()
当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapsBeforeUpdate()
- componentDidUpdate()
以下的三个生命周期函数将被废弃,用getDerivedStateFromProps代替,目前使用的话加上UNSAFE_:
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
引入两个新的生命周期函数
- static getDerivedStateFromProps
- getSnapshotBeforeUpdate
getDerivedStateFromProps
static getDerivedStateFromProps(props, state)
getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。
请注意,不管原因是什么,都会在每次渲染前触发此方法。这与UNSAFE_componentWillReceiveProps 形成对比,后者仅在父组件重新渲染时触发,而不是在内部调用 setState 时。
getSnapshotBeforeUpdate
getSnapshotBeforeUpdate(prevProps, prevState)
在render之后,在componentDidUpdate之前。
getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate(prevProps, prevState, snapshot) 。
6.组件复合
复合组件给与你⾜够的敏捷去定义⾃定义组件的外观和行为,这种⽅式更明确和安全。如果组件间有公⽤的非UI逻辑,将它们抽取为JS模块导入使用而不是继承它。
不具名
新建一个layout.js文件
import React, {
Component
} from "react";
import TopBar from "../components/TopBar";
import BottomBar from "../components/BottomBar";
export default class Layout extends Component {
componentDidMount() {
const {
title = "商城"
} = this.props;
document.title = title;
}
render() {
const {
children,
showTopBar,
showBottomBar
} = this.props;
console.log("children", children);
return (
< div >
{showTopBar && <TopBar />}
{children}
{showBottomBar && <BottomBar />}
</div>
);
}
}
UserPage.js:
import React, { Component } from "react";
import Layout from "./Layout";
export default class UserPage extends Component {
render() {
return (
<Layout showTopBar={true} showBottomBar={true} title="用户中心">
<div>
<h3>UserPage</h3>
</div>
</Layout>
);
}
}
具名
类似于Vu中的具名插槽,我们传一个对象进去
import React, {
Component
} from "react";
import Layout from "./Layout";
export default class HomePage extends Component {
render() {
return ( < Layout showTopBar = {
false
}
showBottomBar = {
true
}
title = "商城首页" > {
/* <div>
<h3>HomePage</h3>
</div> */
} {
{
content: (
<div >
<h3 > HomePage < /h3>
</div>
),
txt: "这是个⽂文本",
btnClick: () => {
console.log("btnClick");
}
}
} <
/Layout>
);
}
}
7.redux
在下面的场景中,引入 Redux 是比较明智的
- 你有着相当⼤量的、随时间变化的数据;
- 你的 state 需要有一个单一可靠数据来源;
- 你觉得把所有 state 放在最顶层组件中已经⽆法满足需要了了。
- 某个组件的状态需要共享。
redux 是 JavaScript应用的状态容器,提供可预测化的状态管理。它保证程序行为一致性且易于测试。
安装redux
npm install redux --save
redux上手
用一个累加器举例
- 需要一个store来存储数据
- store里的reducer初始化state并定义state修改规则
- 通过dispatch一个action来提交对数据的修改
- action提交到reducer函数里,根据传入的action的type,返回新的state
创建store,src/store/ReduxStore.js
import {
createStore
} from 'redux'
const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'ADD':
return state + 1
case 'MINUS':
return state - 1
default:
return state
}
}
const store = createStore(counterReducer)
export default store
创建ReduxPage
import React, {
Component
} from "react";
import store from "../store/ReduxStore";
export default class ReduxPage extends Component {
componentDidMount() {
store.subscribe(() => {
console.log("subscribe");
this.forceUpdate();
//this.setState({});
});
}
add = () => {
store.dispatch({
type: "ADD"
});
};
minus = () => {
store.dispatch({
type: "MINUS"
});
};
render() {
console.log("store", store);
return (
<div>
<h3>ReduxPage</h3>
<p>{store.getState()}</p>
<button onClick={this.add}>add</button>
<button onClick={this.minus}>minus</button>
</div>
);
}
}
如果点击按钮不能更新,因为没有订阅(subscribe)状态变更
还可以在src/index.js的render里订阅状态变更
import store from './store/ReduxStore'
const render = () => {
ReactDom.render( <
App / > ,
document.querySelector('#root')
)
}
render()
store.subscribe(render)
检查点
- createStore 创建store
- reducer 初始化、修改状态函数
- getState 获取状态值
- dispatch 提交更新
- subscribe 变更订阅
8.react-redux
安装
npm install react-redux --save
react-redux提供了两个api
- Provider 为后代组件提供store
- connect 为组件提供数据和变更方法
全局提供store,index.js
import React from 'react'
import ReactDom from 'react-dom'
import App from './App'
import store from './store/'
import { Provider } from 'react-redux'
ReactDom.render(
<Provider store={store}>
<App/>
</Provider>,
document.querySelector('#root')
)
获取状态数据,ReactReduxPage.js
import React, {
Component
} from "react";
import {
connect
} from "react-redux";
class ReactReduxPage extends Component {
render() {
const {
num,
add,
minus
} = this.props;
return ( <
div >
<
h1 > ReactReduxPage < /h1> <
p > {
num
} < /p> <
button onClick = {
add
} > add < /button> <
button onClick = {
minus
} > minus < /button> < /
div >
);
}
}
const mapStateToProps = state => {
return {
num: state,
};
};
const mapDispatchToProps = {
add: () => {
return {
type: "add"
};
},
minus: () => {
return {
type: "minus"
};
}
};
export default connect(
mapStateToProps, //状态映射 mapStateToProps
mapDispatchToProps, //派发事件映射
)(ReactReduxPage);
connect中的参数:state映射和事件映射
9.react-router
react-router包含3个库,react-router、react-router-dom和react-router-native。react-router提供最基本的路由功能,实际使用的时候我们不会直接安装react-router,而是根据应用运行的环境选择安装react-router-dom(在浏览器中使用)或react-router-native(在rn中使用)。react-router-dom和react-router-native都依赖react-router,所以在安装时,react-router也会⾃自动安装,创建web应用,
使用:
安装
npm install --save react-router-dom
基本使用
react-router中奉行一切皆组件的思想,路由器-Router、链接-Link、路由-Route、独占-Switch、重定向-Redirect都以组件形式存在
创建RouterPage.js
import React, {
Component
} from "react";
import {
BrowserRouter as Router,
Route,
Link
} from "react-router-dom";
export default class RouterPage extends Component {
render() {
return (
<div >
<h3 > RouterPage < /h3>
<Router >
<Link to = "/" > 首页 < /Link>
<Link to = "/user" > 用户中⼼ < /Link>
{/* 根路路由要添加exact,实现精确匹配 */ }
<Route
exact
path = "/"
component = {HomePage}
//children={() => <div>children</div>}
//render={() => <div>render</div>}
/>
<Route
path = "/user"
component = {UserPage}
/>
</Router>
</div>
);
}
}
class HomePage extends Component {
render() {
return ( <
div >
<
h3 > HomePage < /h3> <
/div>
);
}
}
class UserPage extends Component {
render() {
return ( <
div >
<
h3 > UserPage < /h3> <
/div>
);
}
}
Route渲染内容的三种方式
Route渲染优先级:children>component>render。
这三种方式互斥,你只能用一种。
children:func
有时候,不管location是否匹配,你都需要渲染一些内容,这时候你可以用children。
除了不管location是否匹配都会被渲染之外,其它⼯作⽅法与render完全一样
render:func
但是当你用render的时候,你调用的只是个函数。
只在当location匹配的时候渲染。
component: component
只在当location匹配的时候渲染。
404页面
设定一个没有path的路由在路由列表最后面,表示一定匹配
{/* 添加Switch表示仅匹配一个*/ }
<Switch >
{/* 根路由要添加exact,实现精确匹配 */ }
<Route
exact
path = "/"
component = {HomePage}
/>
<Route
path = "/user"
component = {UserPage}
/>
<Route
component = {EmptyPage}
/>
</Switch>
class EmptyPage extends Component {
render() {
return (
<div >
<h3 > EmptyPage - 404 < /h3>
</div>
);
}
}
10.PureComponent
React.PureComponent与 React.Component很相似。两者的区别在于 React.Component并未实现 shouldComponentUpdate(),而 React.PureComponent 中以浅层对比 prop 和 state 的方式来实现了该函数。
如果赋予 React 组件相同的 props 和 state,render() 函数会渲染相同的内容,那么在某些情况下使用 React.PureComponent 可提高性能。
实现性能优化
定制了shouldComponentUpdate后的Component
import React, {Component,PureComponent} from "react";
export default class PureComponentPage extends PureComponent {
constructor(props) {
super(props);
this.state = {
counter: 0,
// obj: {
// num: 2,
// },
};
}
setCounter = () => {
this.setState({
counter: 100,
// obj: {
// num: 200,
// },
});
};
render() {
const {
counter,
obj
} = this.state;
console.log("render");
return (
<div >
<h1 > PuerComponentPage < /h1>
<div onClick = {this.setCounter} > counter: {counter} < /div>
</div>
);
}
}
11.认识Hook
Hook 是什么? Hook 是一个特殊的函数,它可以让你“钩入” React 的特性。例如, useState 是允许你在 React 函数组件中添加 state 的 Hook。
什么时候会用 Hook? 如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hook。
import React, { useState } from "react";
export default function HookPage(props) {
// 声明一个叫 “count” 的 state 变量,初始化为0
const [count, setCount] = useState(0);
return (
<div>
<h3>HookPage</h3>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>add</button>
</div>
);
}
使用 Effect Hook
Effect Hook 可以让你在函数组件中执行副作用操作。
数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。不管你知不知道这些操作,或是“副作用”这个名字,应该都在组件中使用过它们。
import React, { useState, useEffect } from "react";
export default function HookPage(props) {
// 声明一个叫 “count” 的 state 变量,初始化为0
const [count, setCount] = useState(0);
// 与 componentDidMount 和 componentDidUpdate相似
useEffect(() => {
// 更新 title
document.title = `You clicked ${count} times`;
});
return (
<div>
<h3>HookPage</h3>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>add</button>
</div>
);
}
在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录⽇志以及执行其他包含副作⽤的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。
默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候 才执行。
effect 的条件执行
默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。
然而,在某些场景下这么做可能会矫枉过正。比如,在上一部分的订阅示例中,我们不需要在每次组件更新时都创建新的订阅,而是仅需要在 source props 改变时重新创建。
要实现这一点,可以给 useEffect 传递第二个参数,它是 effect 所依赖的值数组。更新后的示例如下:
import React, { useState, useEffect } from "react";
export default function HookPage(props) {
// 声明一个叫 “count” 的 state 变量,初始化为0
const [count, setCount] = useState(0);
const [date, setDate] = useState(new Date());
// 与 componentDidMount 和 componentDidUpdate相似
useEffect(() => {
// 更新 title
document.title = `You clicked ${count} times`;
}, [count]);
useEffect(() => {
const timer = setInterval(() => {
setDate(new Date());
}, 1000);
}, []);
return (
<div>
<h3>HookPage</h3>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>add</button>
<p>{date.toLocaleTimeString()}</p>
</div>
);
}
此时,只有当 useEffect第二个参数组里的数值改变后才会重新创建订阅。
清除 effect
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,useEffect函数需返回一个清除函数,以防止内存泄漏,清除函数会在组件卸载前执行。
useEffect(() => {
const timer = setInterval(() => {
setDate(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
12.自定义Hook与Hook使用规则
有时候我们会想要在组件之间重用一些状态逻辑。目前为止,有两种主流方案来解决这个问题:高阶组件和 render props。自定义 Hook 可以让你在不增加组件的情况下达到同样的目的。
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
import React, { useState, useEffect, useMemo } from "react";
export default function CustomHookPage(props) {
//定义一个叫count的state变量,初始化为0
const [count, setCount] = useState(0);
//和didMount、didUpdate类似
useEffect(() => {
console.log("count effect");
// 只需要在count发生改变的时候执行就可以啦
document.title = `点击了了${count}次`;
}, [count]);
return (
<div>
<h3>自定义Hook</h3>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>add</button>
<p>{useClock().toLocaleTimeString()}</p>
</div>
);
}
//自定义hook,命名必须以use开头
function useClock() {
const [date, setDate] = useState(new Date());
useEffect(() => {
console.log("date effect");
//只需要在didMount时候执行就可以了
const timer = setInterval(() => {
setDate(new Date());
}, 1000);
//清除定时器,类似willUnmount
return () => clearInterval(timer);
}, []);
return date;
}
Hook 使用规则
Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:
只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有⼀个地⽅方可以调用 Hook —— 就是自定义的 Hook 中。)
13.Hook API之useMemo与useCallback
useMemo
把“创建”函数和依赖项数组作为参数传入 useMemo ,它仅会在某个依赖项改变时才重新计算memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
···
import React, { useState, useMemo } from "react";
export default function UseMemoPage(props) {
const [count, setCount] = useState(0);
const expensive = useMemo(() => {
console.log("compute");
let sum = 0;
for (let i = 0; i < count; i++) {
sum += i;
}
return sum;
//只有count变化,这⾥才重新执⾏
}, [count]);
const [value, setValue] = useState("");
return (
<div>
<h3>UseMemoPage</h3>
<p>expensive:{expensive}</p>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>add</button>
<input value={value} onChange={event => setValue(event.target.value)} />
</div>
);
}
···
useCallback
把内联回调函数及依赖项数组作为参数传入 useCallback ,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate )的子组件时,它将⾮常有用。
···
import React, { useState, useCallback, PureComponent } from "react";
export default function UseCallbackPage(props) {
const [count, setCount] = useState(0);
const addClick = useCallback(() => {
let sum = 0;
for (let i = 0; i < count; i++) {
sum += i;
}
return sum;
}, [count]);
const [value, setValue] = useState("");
return (
<div>
<h3>UseCallbackPage</h3>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>add</button>
<input value={value} onChange={event => setValue(event.target.value)} />
<Child addClick={addClick} />
</div>
);
}
class Child extends PureComponent {
render() {
console.log("child render");
const { addClick } = this.props;
return (
<div>
<h3>Child</h3>
<button onClick={() => console.log(addClick())}>add</button>
</div>
);
}
}
···
useCallback(fn, deps) 相当于 useMemo(() => fn, deps) 。