# react-markdown笔记
**Repository Path**: cai-lunduo/react-markdown-notes
## Basic Information
- **Project Name**: react-markdown笔记
- **Description**: react学习的markdown笔记合集
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 3
- **Forks**: 0
- **Created**: 2021-05-30
- **Last Updated**: 2023-11-04
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# React学习记录
参考文档:https://react.docschina.org/docs/getting-started.html
这里不讲述概念,直接上语法,至于jsx是什么,到上面那个网站上面看
## 生命周期图
https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
## 1. state与setState
[TOC]
这是react最主要的两个内置属性与内置方法,state表示当前组件的状态/属性,而要改变state的值则通过setState方法,切不可直接修改state,因为setState内部是异步修改的
例子:
```js
export default class CartSample extends Component {
this.state = {
text: ''
}
setGoodsText = e => {
this.setState({
text: e.target.value
})
}
render() {
return (
)
}
}
```
上面CartSample就是一个组件,里面有state和setState,监听input框的change事件,一旦触发则让setState改变state中的值,这是最简单也是最常用的用法。
## 2. 条件判断
在jsx中,我们在大括号{}内只能写表达式,不能编写逻辑代码,如if判断、for循环,此时我们可以通过短路逻辑判断,如:
下面例子写了当state的goods长度不为0时则展示对应数据
```js
export default class CartSample extends Component {
this.state = {
goods: [
{ id: 1, text: "web全栈架构师", count: 2 },
]
}
render() {
return (
定义,所以this为undefined。
为了让我们方法中的this可以指向类本身,我们可以选择四种方式:
```react
// 方式一 构造器中给对应函数指定上下文, 如
this.addGood = this.addGood.bind(this) ------> 推荐
// 方式二 在JSX给种绑定this
// 方式三 类里面方法用箭头函数定义,箭头函数是没有上下文的,上下文会指向类
addGood = () => {} ------> 推荐
// 方式四 JSX里用箭头函数
addGood = () => { () => {this.addGood()} }
```
## 7. setState注意事项
使用setState修改值有两个方法:
一个是传入对象修改,另一个方式是传入函数修改
```react
// 方式一
state = {
text: ''
}
this.setState({
text: '新值'
})
// 方式二
state = {
text: ''
}
this.setState(prevState => {
let text = '123'
prevState.text = text
})
```
### 7.1 方式一注意事项
在使用方式一时要注意,若多次使用方式一进行setState,react最终会将多个setState操作给合并,这时候,若是多次setState了通过值,则只取最后一次setState的时候的值,如:
```
state = {
text: ''
}
this.setState({
text: '新值'
})
this.setState({
text: '新新值'
})
```
最后的值为 : 新新值
### 7.2 方式二注意事项
使用方式二时,我们最好按照react给我们定的规范,每次都给state需要修改的对象重新设置一个新的的值。
如:
```js
this.state = {
goods: [
{ id: 1, text: "web全栈架构师" },
{ id: 2, text: "python全栈架构师" }
],
text: ''
}
textChange = e => {
this.setState({ text: e.target.value });
};
```
现在当点击某个按钮时会触发addGood事件,addGood用来添加新商品数据到goods中,此时addGood的写法是这样的
```js
addGood = () => {
this.setState(prevState => {
return {
goods: [
...prevState.goods,
{
id: prevState.goods.length + 1,
text: prevState.text
}
]
};
});
}
```
会将以前state.goods里的数据展开,拷贝进一个新的数组,再将新数据加进去,这就是react的规范。
## 8. antd框架引入
安装:
```
npm install antd --save
```
试用按钮组件:
```js
import Button from 'antd/lib/button';
import 'antd/dist/antd.css'
export default class App extends Component {
render() {
return (
)
}
}
```
上面那种方式引入很麻烦,而且是样式是引用全部的样式表,如何按需引入呢?
**安装react-app-rewired取代react-scripts,可以扩展webpack的配置,类似vue.config.js**
```js
npm install react-app-rewired@2.0.2-next.0 babel-plugin-import --save
```
修改package.json的scripts字段:
```js
// 所有react-scripts 都替换成 react-app-rewired 如
"scripts": {
"start": "react-app-rewired start",
...
},
```
在根目录下新建config-overrides.js,内容如下
```js
const { injectBabelPlugin } = require("react-app-rewired");
module.exports = function override(config, env) {
config = injectBabelPlugin(
// 在默认配置基础上注入
// 插件名,插件配置
["import", { libraryName: "antd", libraryDirectory: "es", style: "css" }],
config
);
return config;
};
```
这样子配置就完成了,接下来我们可以实现按需导入了,在需要引入组件的地方:
```js
import {Button} from 'antd'
```
## 9. 容器组件与展示组件
基本原则:容器组件负责数据获取,展示组件负责根据props显示信息,例子如下
```react
import React, { Component } from 'react';
// 容器组件
export default class CommentList extends Component {
constructor(props) {
super(props);
this.state = {
comments: [],
}
}
componentDidMount() {
setTimeout(() => {
this.setState({
comments: [
{ name: 'cai', age: 20, education: 'university' },
{ name: 'chen', age: 22, education: 'university' },
]
})
}, 3000)
}
render() {
return (
{this.state.comments.map((c, i) => {
return
})}
)
}
}
// 展示组件
function Comment({ data }) {
return (
{data.name}
--- {data.age}
--- {data.education}
)
}
```
## 10. 性能优化:浅比较
参考文档:https://react.docschina.org/docs/optimizing-performance.html
UI 更新需要昂贵的 DOM 操作,而 React 内部使用几种巧妙的技术以便最小化 DOM 操作次数。对于大部分应用而言,使用 React 时无需专门优化就已拥有高性能的用户界面。尽管如此,你仍然有办法来加速你的 React 应用。
还是引用9的例子,只不过我们会给CommentList加一个定时器,每隔1.5s就对comments列表进行setState操作,但是我们不修改它的值:
```react
componentDidMount() {
this.timmer = setInterval(() => {
this.setState({
comments: [
{ name: 'cai', age: 20, education: 'university' },
{ name: 'chen', age: 22, education: 'university' },
]
})
}, 1500)
}
```
然后再在Comment组件打印日志。我们会发现打印日志会每隔1.5s打印,这时候问题就来了,明明我们的数据没发生实质上的变化,但页面却一直在渲染,这是一个非常值得优化的点。
解决方案:
### 10.1 使用shouldComponentUpdate
因为使用到生命周期函数,所以我们的Comment组件应该转换为类形式,shouldComponentUpdate用来检查旧值与新值是否相等,返回一个bool值,如为true则重新渲染,否则不渲染
```react
class Comment extends React.Component{
constructor(props) {
super(props)
}
shouldComponentUpdate(nextProps){
if (nextProps.data.name === this.props.data.name &&
nextProps.data.age === this.props.data.age &&
nextProps.data.education === this.props.data.education
) {
return false;
}
return true;
}
render() {
console.log("render comment");
return (
{this.props.data.name}
--- {this.props.data.age}
--- {this.props.data.education}
);
}
}
```
### 10.2 使用PureComponent
首先我们先研究一下PureComponent的实现原理:
PureComponent首先会判断新值与旧值**是否在同一片内存地址**,如果是,则返回true,也就是相同。
否则会再对旧值的内部做一层循环比较(注意:浅比较只会进行一层for循环,有**深层次的结构时浅比较是没有效果的**)
由此可以看出PureComponent的两个缺点:
一、如果是一个对象,或者数组,我们对其内部进行修改,而**没有改变内存地址**时,页面不会重新渲染
二、如果包含**二层及以上的层次结**构时,浅比较无法生效
如果我们有二层的层次结构时还想进行钱比较该怎么办呢?
方法: 传给展示组件的值用第二层的值传递,而非将整个对象或数组传递给展示组件。
继续引用9的例子
此时我们传给Comment组件的值不传入每个comments列表项,而是用展开运算符将每个对象展开,传入最里边的值,意思就是,我有下面这个结构的数组:
```js
// 需要传给展示组件的数据
comments: [
{ name: 'cai', age: 20, education: 'university' },
{ name: 'chen', age: 22, education: 'university' },
]
// 传入的值:{...c}
{this.state.comments.map((c, i) => {
return
})}
// 此时...c == 每个对象的name、age、education值
// 展示组件继承PureComponent
class Comment extends React.PureComponent{
constructor(props) {
super(props)
}
render() {
console.log("render comment");
return (
{this.props.name}
--- {this.props.age}
--- {this.props.education}
);
}
```
### 10.3 使用memo
类形式的组件可以进行浅比较,现在在React v16.6.0之后,函数式也添加了浅比较,也就是memo, 使用方式如下:
```react
constJoke=React.memo(() => (
{this.props.value||'loading...'}
));
```
只需将组件用React.memo包裹即可。
## 11. 高阶组件
### 11.1 核心概念
**高阶组件是参数为组件,返回值为新组件的函数**
高阶组件其实就是装饰器的原型,装饰器内部就是对高阶组件封装了而已,也就是语法糖。
高阶组件的一个最明显的好处就是你可以自行扩展组件行为。想到行为扩展想必你已经联想到mixins了,那为什么不用mixins而非要弄个高阶组件出来呢? 参考文档:https://react.docschina.org/blog/2016/07/13/mixins-considered-harmful.html
接下来我们就讲讲高阶组件的使用:
假设有一个 `CommentList` 组件,它订阅外部数据源,用以渲染评论列表:
```react
class CommentList extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
// 假设 "DataSource" 是个全局范围内的数据源变量
comments: DataSource.getComments()
};
}
componentDidMount() {
// 订阅更改
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
// 清除订阅
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
// 当数据源更新时,更新组件状态
this.setState({
comments: DataSource.getComments()
});
}
render() {
return (
{this.state.comments.map((comment) => (
))}
);
}
}
```
稍后,编写了一个用于订阅单个博客帖子的组件,该帖子遵循类似的模式:
```react
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id)
};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
blogPost: DataSource.getBlogPost(this.props.id)
});
}
render() {
return ;
}
}
```
`CommentList` 和 `BlogPost` 不同 - 它们在 `DataSource` 上调用不同的方法,且渲染不同的结果。但它们的大部分实现都是一样的:
- 在挂载时,向 `DataSource` 添加一个更改侦听器。
- 在侦听器内部,当数据源发生变化时,调用 `setState`。
- 在卸载时,删除侦听器。
你可以想象,在一个大型应用程序中,这种订阅 `DataSource` 和调用 `setState` 的模式将一次又一次地发生。我们需要一个抽象,允许我们在一个地方定义这个逻辑,并在许多组件之间共享它。这正是高阶组件擅长的地方。
对于订阅了 `DataSource` 的组件,比如 `CommentList` 和 `BlogPost`,我们可以编写一个创建组件函数。该函数将接受一个子组件作为它的其中一个参数,该子组件将订阅数据作为 prop。让我们调用函数 `withSubscription`:
```react
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
```
第一个参数是被包装组件。第二个参数通过 `DataSource` 和当前的 props 返回我们需要的数据。
当渲染 `CommentListWithSubscription` 和 `BlogPostWithSubscription` 时, `CommentList` 和 `BlogPost` 将传递一个 `data` prop,其中包含从 `DataSource` 检索到的最新数据:
```react
// 此函数接收一个组件...
function withSubscription(WrappedComponent, selectData) {
// ...并返回另一个组件...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ...负责订阅相关的操作...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... 并使用新数据渲染被包装的组件!
// 请注意,我们可能还会传递其他属性
return ;
}
};
}
```
请注意,HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件*包装*在容器组件中来*组成*新组件。HOC 是纯函数,没有副作用。
被包装组件接收来自容器组件的所有 prop,同时也接收一个新的用于 render 的 `data` prop。HOC 不需要关心数据的使用方式或原因,而被包装组件也不需要关心数据是怎么来的。
因为 `withSubscription` 是一个普通函数,你可以根据需要对参数进行增添或者删除。例如,您可能希望使 `data` prop 的名称可配置,以进一步将 HOC 与包装组件隔离开来。或者你可以接受一个配置 `shouldComponentUpdate` 的参数,或者一个配置数据源的参数。因为 HOC 可以控制组件的定义方式,这一切都变得有可能。
与组件一样,`withSubscription` 和包装组件之间的契约完全基于之间传递的 props。这种依赖方式使得替换 HOC 变得容易,只要它们为包装的组件提供相同的 prop 即可。例如你需要改用其他库来获取数据的时候,这一点就很有用。
### 11.2 **高阶组件的链式调用**
比如我们有两个高阶组件,分别定义不同的行为:
```react
// 高阶组件1
const withLog = Comp => {
console.log("日志打印...");
return props => ;
};
// 高阶组件2
const withSubscribtion = Comp => {
// 获取name,name可能来自于接口或其他手段
const name = "其他属性";
console.log("do something");
return class extends React.Component {
render() {
return ;
}
};
};
// 被扩展组件
class NeedToExtend extends Component {
render() {
return (
);
}
}
// 链式调用过程
const newComp = withLog(withSubscribtion(NeedToExtend))
```
### 11.3 装饰器语法糖使用
ES7装饰器可用于简化高阶组件写法
```
npm i -D babel-plugin-transform-decorators-legacy
```
在config-overrides.js添加配置
```js
config = injectBabelPlugin(
["@babel/plugin-proposal-decorators", { legacy: true }],
config
);
```
使用装饰器,以11.2为例子
```react
// 链式调用过程
const newComp = withLog(withSubscribtion(NeedToExtend))
// 装饰器使用
@withSubscribtion
@withLog
class NeedToExtend extends Component {
render() {
return (
);
}
}
```
## 12. 复合组件
### 12.1 复合内容为jsx
复合组件其实就是展示组件预留个插口位置待容器组件插入相应的值,等同于Vue中的插槽。
对应Vue中的匿名插槽就是`props.children`,如:
```react
import React, { Component } from 'react';
// 展示组件
function Dialog(props) {
return (
{props.children}
);
}
// WelcomeDialog通过复合提供内容
function WelcomeDialog(props) {
return (
);
}
export default function() {
return (
)
}
```
此时`{props.children}`就是插进来的内容
```html
欢迎光临
感谢使用react
```
那么对应Vue的有名插槽是怎么实现的呢?
其实很简单,就是通过props传值,如我在展示组件里在定义一个页脚,页脚内容通过容器组件传入:
```react
function Dialog(props) {
return (
{props.children}
{props.footer}
);
}
const footer = ;
```
以上例子复合内容形式均为jsx。
### 12.2 复合内容为函数
复合内容为函数时就相当于Vue中的作用于插槽,例子如下
假设我现在定义一个Fetcher组件,组件内的复合内容是一个带参函数,我想通过这个函数的参数渲染对应的内容:
```react
{user => (
{user.name}-{user.age}
)}
```
定义组件:
```js
// api接口
const Api = {
getUser() {
return { name: "jerry", age: 20 };
}
};
// 组件
function Fetcher(props) {
const user = Api[props.name](); //获取name并执行函数获取数据
return props.children(user); //通过props.children返回数据
}
```
### 12.3 复合内容为数组
复合内容为数组时其实实质上也是jsx
假如我们想要对复合内容(`类型为数组的jsx`)进行过滤,值过滤出类型为type的元素进行展示:
我们可以用到`React.Children.map`方法,遍历复合内容,并过滤出我们想要的
```react
function Filter({ children, type }) {
return (
{React.Children.map(children, child => {
if (child.type !== type) {
return;
}
return child;
})}
);
}
react
react很不错
vue
vue很不错
```
若我们想对传进来的复合内容进行修改,则必须谨记:
**复合内容为jsx,是虚拟节点,内容不能修改**,因此如果我们要修改它的内容,那我们只能克隆对应的元素在做自己想要的修改即可,如:
现在我有一个按钮组,结构如下
```html
vue
react
angular
```
我不想给每个按钮都加一个`name`,而是给`Radio`的父组件`RadioGroup`加一个`name`,然后让`RadioGroup`循环遍历每个`Radio`,对每个`Radio`加上同一个`name`该怎么操作呢?
如下
```react
function RadioGroup(props) {
return (
{React.Children.map(props.children, child => {
// vdom不可更改,克隆一个新的去改才行
return React.cloneElement(child, { name: props.name });
})}
);
}
function Radio({children, ...rest}) {
return (
);
}
```
这里需要注意的一点就是,再给`Radio`赋值的时候要注意`props`包含`children`和`父级组件传进来的值`。
因此Radio接收参数时需要做分割,分为`children复合内容部分`和`剩余部分`,`剩余部分`传给input作为属性值。
## 13 Refs and the DOM
Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
在典型的 React 数据流中,[props](https://zh-hans.reactjs.org/docs/components-and-props.html) 是父组件与子组件交互的唯一方式。要修改一个子组件,你需要使用新的 props 来重新渲染它。但是,在某些情况下,你需要在典型数据流之外强制修改子组件。被修改的子组件可能是一个 React 组件的实例,也可能是一个 DOM 元素。对于这两种情况,React 都提供了解决办法。
###
## 13. Hook
*Hook* 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性
你之前可能把它们叫做“无状态组件”。但现在我们为它们引入了使用 React state 的能力,所以我们更喜欢叫它”函数组件”。
### 自定义Hook(补充)
自定义hook可以让我们将组件逻辑提取到一个可重复利用的组件内。
这里会有两个案例讲解自定义hook的使用
**第一个简单的例子**:
```react
import {useState, useEffect} from 'react'
function Comp() {
let [num, setNum] = useState(0)
useEffect(() => {
setTimeout(() => {
setNum(++num)
}, 2000)
}, [])
return (
Comp -- {num}
)
}
function App() {
return (
);
}
export default App;
```
如上所示,我们可以将`Comp`组件的
```react
let [num, setNum] = useState(0)
useEffect(() => {
setTimeout(() => {
setNum(++num)
}, 2000)
}, [])
```
给提取到一个单独的组件中,这时候便是一个自定义hook,使得我们调用的代码更简洁,且逻辑抽离到了一起。
我们将上述代码抽离到`./hooks/numHook.js`中。
```react
import {useState, useEffect} from 'react'
export default function useNum() {
let [num, setNum] = useState(0)
useEffect(() => {
setTimeout(() => {
setNum(++num)
}, 2000)
}, [])
return [num, setNum]
}
```
这时候我们在需要引用该hook的地方进行导入引用即可,例子将可以改写为:
```react
import useNum from './hooks/numHook'
function Comp() {
let [num] = useNum()
return (
Comp -- {num}
)
}
function App() {
return (
);
}
export default App;
```
这就是最简单的自定义hook。
### 自定义请求数据hook
这里我们要做的自定义Hook是发起数据请求的hook
实现效果如下:

安装bootstrap:
```shell
npm i -S bootstrap
```
**server端**:
```js
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors())
app.get('/list', (req, res) => {
// 当前页和每页多少条
let {currentPage, perSize} = req.query
perSize = parseInt(perSize, 10)
// 数据 页数和条数对应得数据
let list = []
// 总数据条数
let total = 66
// 总页数
let pageCount = Math.ceil(total / perSize)
// 起始索引
let offset = (currentPage - 1) * perSize
if(currentPage >= pageCount) {
perSize = total % perSize
}
for(let i = offset; i < offset + perSize; i++) {
list.push({id: i+1, name: 'cai-'+(i+1)})
}
res.json({
currentPage,
perSize,
total,
pageCount,
list
})
})
app.listen(8080, () => {
console.log('listening on 8080...')
})
```
记得先启动服务端:
```node
node server
```
在`./hooks/useRequest.js`中:
```react
import {useState, useEffect} from 'react'
export default function useRequest() {
/* 用户可更改的请求数据 */
let [options, setOptions] = useState({currentPage: 1, pageSize: 10})
let [data, setData] = useState({
total: 0,
pageCount: 0,
list: []
})
// 请求数据
function reqeust() {
let {currentPage, pageSize} = options
fetch(`http://localhost:8080/list?currentPage=${currentPage}&perSize=${pageSize}`)
.then(res => res.json())
.then(res => setData({...res}))
}
useEffect(reqeust, [options])
return [data, options, setOptions]
}
```
这里的options state是单独抽离出来的,因为我们要返回给组件setOptions让他自定义请求参数(如:用户点击下一页时我们要重新改变请求参数的`currentPage`为当前页+1)。
```js
onClick={() => {
setOptions({ ...options, currentPage: index + 1 })
}}
```
组件一旦挂载我们就发起数据请求,然后将通过`setData`设置数据,并将`data`返回。可以看到最后返回的格式是[data, options, setOptions],意味着我们在组件内可以通过类似`useState`的方式获取自定义hook的值。
下面组件的取法:
```js
let [data, options, setOptions] = useRequest()
```
在`./pages/Tabel.js`中:
```react
import {useState} from 'react'
import useRequest from '../hooks/useRequest'
export default function Table() {
let [data, options, setOptions] = useRequest()
let { list, pageCount } = data
let [size, setSize] = useState(10)
return (
<>
| id |
name |
{list.map((item) => {
return (
| {item.id} |
{item.name} |
)
})}
{/* 下拉框 */}
>
)
}
```
### 13.1 useState
useState的用法很简单也很方便,比起class的方式简直要简洁得不少
```react
import React, { useState } from 'react';
export default function Example() {
const [count, setCount] = useState(0)
const [fruit, setFruit] = useState('banana')
return (
你点击了{count}次
{fruit}
)
}
```
如上例子所示,useState(0),0表示初始值,而将useState解构成两个成员,一个是状态属性,一个是更改状态的函数,其中useState有多个,这样子就比class的形式要简洁多了。
### 13.2 useEffect
你之前可能已经在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。
`useEffect` 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 `componentDidMount`、`componentDidUpdate` 和 `componentWillUnmount` 具有相同的用途,只不过被合并成了一个 API。
例如,下面这个组件在 React 更新 DOM 后会设置一个页面标题:
```react
import React, { useState, useEffect } from 'react';
export default function Example() {
const [count, setCount] = useState(0)
const [fruit, setFruit] = useState('banana')
//相当于componentDidMount 和 componentDidUpdate
useEffect(() => {
// 使用浏览器的API更新页面标题
document.title = `您点击了 ${count} 次`
})
return (
你点击了{count}次
)
}
```
当你调用 `useEffect` 时,就是在告诉 React 在完成对 DOM 的更改后运行你的“副作用”函数。由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。默认情况下,React 会在每次渲染后调用副作用函数 —— **包括**第一次渲染的时候
副作用函数还可以通过返回一个函数来指定如何“清除”副作用。例如,在下面的组件中使用副作用函数来订阅好友的在线状态,并通过取消订阅来进行清除操作
```react
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; });
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
```
#### useEffect模拟重新发送请求
因为useEffect会在组件更新时调用,所以我们可以维护一个状态,当这个状态改变,我们调用函数重新发送请求。
```react
import React, { useState, useEffect } from 'react';
export default function Example() {
const [count, setCount] = useState(0)
const [flag, setFlag] = useState(false)
function request() {
setCount(++count)
}
useEffect(() => {
request()
},[flag])
return (
你点击了{count}次
)
}
```
#### 13.2.1 useEffect的优化点
当然,`useEffect`和`useState`一样可以同时存在多个,还可以指定每次副作用函数只对那些起作用,和指定那些副作用函数只执行一次,因为比如我们要在`useEffect`中异步获取数据,我们总不能每次渲染操作都获取一次,因此有下面的解决方案:
```react
import React, { useState, useEffect } from 'react';
export default function Example() {
const [count, setCount] = useState(0)
const [fruit, setFruit] = useState('banana')
//相当于componentDidMount 和 componentDidUpdate
useEffect(() => {
// 使用浏览器的API更新页面标题
// 该副作用函数只对count和fruit起作用
document.title = `您点击了 ${count} 次`
console.log(count)
}, [fruit, count])
// 只会调用一次
useEffect(() => {
console.log('异步获取数据操作')
},[])
return (
你点击了{count}次
)
}
```
如上所示,我们可以在useEffect传入一个数组参数,里面为空则表示只执行一次,如果里面有状态则表示只对里面的状态起作用。

**为什么要在 effect 中返回一个函数?** 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。
**React 何时清除 effect?** React 会在组件卸载的时候执行清除操作,相当于类组件的`componentWillUnmount`,也会在每次组件更新之前调用该回调,相当于`componentWillUpdate`。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React *会*在执行当前 effect 之前对上一个 effect 进行清除。
#### 13.2.2 小结
了解了 `useEffect` 可以在组件渲染后实现各种不同的副作用。有些副作用可能需要清除,所以需要返回一个函数:
```react
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
```
其他的 effect 可能不必清除,所以不需要返回。
```react
useEffect(() => {
document.title = `You clicked ${count} times`;
});
```
### useContext(补充)
我们通过`createContext`创建一个Context之后,在子组件可以通过`useContext`拿到我们在祖先组件创建的`Context`,但是前提是我们需要对子组件用`Context.Provider`标签进行包裹,然后通过`value`属性传值。
这里需要注意` const states = useContext(Context)`只能在子组件内部调用,不可在顶级元素使用。
例子如下:
```react
import {createContext, useContext} from 'react'
const Context = createContext()
function Child() {
const states = useContext(Context)
return (
{states.map(state => {
return (
{state}
)
})}
)
}
function App() {
return (
);
}
export default App;
```
在这里我们通过`Context.Provider`传输了一个数字数组到子代组件,子代组件通过`const states = useContext(Context)`获取到父组件传过来的数据并进行展示。
### 优化
假如我们要给多个子组件提供同一份数据,那么我可以将提供数据的父组件给抽离成一个单独的负责**提供数据**的组件:
```react
import {createContext, useState} from 'react'
export const Context = createContext()
export function CountContextProvider({children}) {
let [count, setCount] = useState(10)
const countObj = {
count,
add() {
setCount(++count)
},
minus() {
setCount(--count)
}
}
return (
{children}
)
}
```
子组件:
```react
import {useContext} from 'react'
import {Context, CountContextProvider} from './CountContextProvider'
function CountChild() {
const {count, add, minus} = useContext(Context)
return (
{count}
)
}
let r = () => {
return (
)
}
export default r
```
## 14. Context
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
因为典型的React应用数据是通过Props属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。
总体步骤
```shell
1.creareContext
2.MyContext.Provider包裹通过value传值
3.在需要该值的组件static contextType = ThemeContext;接收context
```
### 14.1 使用Context之前
**何时使用 Context**
Context的目的是共享那些应该在全局共享的数据,例如当前用户的Token数据,或者首选语言。举个例子,在下面的代码中,我们通过一个 “theme” 属性手动调整一个按钮组件的样式:
```react
class App extends React.Component {
render() {
return ;
}
}
function Toolbar(props) {
// Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
// 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
// 因为必须将这个值层层传递所有组件。
return (
);
}
class ThemedButton extends React.Component {
render() {
return ;
}
}
```
使用 context, 我们可以避免通过中间元素传递 props:
```react
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
// 无论多深,任何组件都能读取这个值。
// 在这个例子中,我们将 “dark” 作为当前的值传递下去。
return (
);
}
}
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
return (
);
}
class ThemedButton extends React.Component {
// 指定 contextType 读取当前的 theme context。
// React 会往上找到最近的 theme Provider,然后使用它的值。
// 在这个例子中,当前的 theme 值为 “dark”。
static contextType = ThemeContext;
render() {
return ;
}
}
```
**使用Context之前应该有什么考虑?**
使用Context之前我们必须清楚,Context会使得组件的复用性变差,它主要应用场景在于*很多*不同层级的组件需要访问同样一些的数据,请谨慎使用。
如果只是为了避免层层传递一些属性,`组件组合`有时候是比Context更好的解决方案:
如:
```react
// ... 渲染出 ...
// ... 渲染出 ...
// ... 渲染出 ...
```
当我们发现user只在Avatar使用到但其他上层组件都要层层传递的时候,代码会变得非常冗余。解决方案除了用Context之外,我们还可以将该Avatar组件自身传递下去,这样中间组件无需知道 `user` 或者 `avatarSize` 等 props:
```react
function Page(props) {
const user = props.user;
const userLink = (
);
return ;
}
// 现在,我们有这样的组件:
// ... 渲染出 ...
// ... 渲染出 ...
// ... 渲染出 ...
{props.userLink}
```
这种对组件的*控制反转*减少了在你的应用中要传递的 props 数量,这在很多场景下会使得你的代码更加干净,使你对根组件有更多的把控。但是,这并不适用于每一个场景:这种将逻辑提升到组件树的更高层次来处理,会使得这些高层组件变得更复杂,并且会强行将低层组件适应这样的形式,这可能不会是你想要的。
而且你的组件并不限制于接收单个子组件。你可能会传递多个子组件,甚至会为这些子组件(children)封装多个单独的“接口(slots)
### 14.2 Context用法
Context可以有三种用法
#### 14.2.1 Consumer(函数组件)
就是通过Consumer包裹需要拿到值的元素,这样子那个元素就能拿到上下文的值了
```react
import React from 'react'
function Child(props) {
return (
Child: {props.foo}
)
}
const MyContext = React.createContext()
const { Provider, Consumer } = MyContext;
export default function ContextTest() {
return (
)
}
```
Provider设置值value,然后Consumer就可以拿到值,Consumer内部组件可以通过Props.children格式为带参函数的形式拿到value,然后让对应组件渲染结果。
#### 14.2.2 Hook取值
Hook是一种很方便的存在,它让我们不用通过Consumer就可以很简便的取到上级传给我们的值,但是有版本限制,必须
```react
import React, {useContext} from 'react'
const MyContext = React.createContext()
const { Provider, Consumer } = MyContext;
function Child2() {
const value = useContext(MyContext)
return (
Child2: {value.foo}
)
}
export default function ContextTest() {
return (
)
}
```
#### 14.2.3 Class取值(类组件)
```react
import React from 'react'
const MyContext = React.createContext()
const { Provider, Consumer } = MyContext;
class Child3 extends React.Component {
static contextType = MyContext
render() {
return (
Child3: {this.context.foo}
)
}
}
export default function ContextTest() {
return (
)
}
```
### 14.3 总结
总共有三种方法取到Context的值,如下所示:
```react
import React, {useContext} from 'react'
const MyContext = React.createContext()
const { Provider, Consumer } = MyContext;
// Consumer取值
function Child(props) {
return (
Child: {props.foo}
)
}
// Hook取值
function Child2() {
const value = useContext(MyContext)
return (
Child2: {value.foo}
)
}
// 类的方式指定静态属性contextType,这样子上下文就会自动有个context
class Child3 extends React.Component {
static contextType = MyContext
render() {
return (
Child3: {this.context.foo}
)
}
}
export default function ContextTest() {
return (
{/* Consumer包裹元素 */}
{ value => }
)
}
```
## 15. Form表单组件设计
高阶组件设计功能目的:
1、给input框进行包装,从而对input事件进行管理
2、把触发的事件收集的数据放在一起,按照参数传进来的规则进行校验
3、可以提供给当前表单一个校验方法,用来判断当前的校验是否全都通过,从而决定是否提交表单信息,或者不提交
1的思路:
对input框进行包装,也就是用高阶组件进行包装,在此我们需要谨记不能对vdom进行扩展,而是通过React.cloneElement克隆组件,再进行扩展,扩展内容包括:
- 设置name属性
- 设置value值(默认为空串)
- 绑定onChange事件并让包装器进行管理,也就是状态提升(当子组件发生某件事的时候,让父组件处理),这样子我们就可以再父组件中管理所有input的值
- 在父组件的handleChange中,我们通过解构方式获取name和value,然后进行setState改变值,这样子render函数就会重新执行
当然,在setState之后我们需要进行验证,这里我们需要注意setState可能是异步的,我们的校验函数不能直接放在setState之后,而是放置在setState执行后的回调函数内。
2的思路

数据收集我们可以放在input包装器内,因为包装器传入了两个参数,第一个是校验字段,第二个是参数选项,在包装器进行克隆元素的时候我们可以缓存这些数据。
缓存步骤:
- 将第二个参数值缓存到包装器上下文中,校验时可用到
- 设置name属性为传进来的第一个参数field,将value初始化为空并放到state中,等待状态变化而改变
3的思路
在最外面的包装组件内,定义一个校验字段函数,函数首先获取2所缓存的校验规则,然后用数组的some方法遍历每一项rule,如果有一项不符合就设置错误信息并返回false,否则将错误信息置空。
接着再定义一个校验函数,作用就是对每个input项进行校验,可以通过Object.keys拿出缓存的options再进行map对每个项都进行校验字段。
最后拿到的是一个bool数组,通过数组的every方法判断是否校验成功,如果成功则调用回调函数,并传入成功标识符和相应数据。
最后要展示错误信息,通过判断当前字段的错误信息是否存在,如果存在则显示错误信息,否则不展示。
```react
import React from 'react'
import {Button, Input} from 'antd'
// 创建一个高阶组件:扩展现有a表单,事件处理、数据收集、校验功能
function kFormCreate(Comp) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {}
this.options = {}
}
// 当值变化,改变state值
handleChange = e => {
const {name, value} = e.target
console.log(name, value)
this.setState({
[name]: value
},// 设置完值在做验证
() => this.validateField(name))
}
validate = cb => {
// 对所有Input框都进行验证
const rets = Object.keys(this.options).map(field => this.validateField(field))
const ret = rets.every(v => v === true)
cb(ret, this.state)
}
// 对单项input进行规则验证
validateField = field => {
const rules = this.options[field].rules
// some找到一个验证不通过的规则就不再继续遍历
const ret = rules && !rules.some(rule => {
if(rule.required && !this.state[field]) {
// 验证失败,我们要设置错误信息
this.setState({
[field+'Message']:'该字段不能为空'
})
return true
}
return false
})
// 如果验证成功,错误信息为空串,
if(ret){
this.setState({
[field+'Message']:''
})
}
return ret
}
getFieldDec = (field, options) => {
// 这里做了数据收集与状态提升到包装器内部处理
// 保存当前输入配置项
this.options[field] = options
return InputComp => (
{React.cloneElement(InputComp, {
name: field,
value: this.state[field] || '',
// 状态提升
onChange: this.handleChange
})}
{this.state[field+'Message'] &&
{this.state[field+'Message']}
}
)
}
render() {
return
}
}
}
@kFormCreate
class KForm extends React.Component {
onSubmit = e => {
this.props.validate((isValid, data) => {
if(isValid) {
console.log("验证通过!")
// 做登录相关的逻辑操作
}
})
}
render() {
const { getFieldDec } = this.props
return (
{getFieldDec("userName", {
rules: [{ required: true, message: "Please input your username!" }]
})(
)}
)
}
}
export default KForm
```
## 16. Redux的使用

```node
cnpm install redux --save
```
### 16.1 简单使用
在文件store.js内:
```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
```
我们通过`createStore`创建`Store`对象,参数为`Reducer`对象,`state`是当前`store`的属性值,可以是一个对象,在通过对`action`参数的`type`属性进行判断,修改`state`的值。
接下来我们在需要引用`store`里面的全局状态的组件引入`store`,在此之前我们先通过在控制台打印一下store到底是什么:

如上所示,我们创建的`Store`对象包括了这么些个属性,我们可以通过`getState`方法获取state的值,也可以通过`dispatch`方法改变`state`的值,还有最后我们还要在页面渲染那里用`subscribe`进行订阅。
```react
import React from 'react'
import store from '../store'
// 原始Store组件
function StoreTest() {
return (
<>
{store.getState()}
>
)
}
export default StoreTest
```
如上所示,我们可以通过`getState`获取到前面`store.js`的`state`值,再给