Skip to content

起步

html
<div id="root"></div>
<!-- 引入文件 -->
<script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script>
<script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script>
<script>
  // 创建 react 元素
  // 参数一表示元素名称,参数二表示元素属性,参数三往后的参数都表示元素的子节点
  // React.createElement 这种方式不常用,了解即可
  const title = React.createElement(
    'h1',
    { title: '第一个应用' },
    '第一个应用',
    React.createElement('span', null, 'span')
  )
  // 渲染 react 元素
  // 参数一表示要渲染的 react 元素(可以为组件),参数二表示挂载点(根节点)
  ReactDOM.render(title, document.getElementById('root'))
</script>

脚手架安装

bash
npm i create-react-app -g # 全局安装脚手架,创建 react 应用需要 node 14 或更高
create-react-app 项目名称 # 创建项目
# create-react-app 项目名称 --template typescript # 创建 ts 项目
npm start # 启动项目

文件梳理

public/index.html

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <!-- %PUBLIC_URL% 代表 public 文件夹的路径 -->
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <!-- 开启理想视口,用于移动端网页适配 -->
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <!-- 用于配置浏览器页签和地址栏的颜色,此配置只对安卓手机浏览器生效,存在兼容性问题 -->
    <meta name="theme-color" content="#000000" />
    <!-- 用于描述网站信息 -->
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <!-- 用于苹果手机浏览器用户收藏网站到桌面时显示的图标 -->
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <!-- 用于引入网页应用加壳转变手机应用时的配置项 -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <title>React App</title>
  </head>
  <body>
    <!-- 当当前浏览器版本不支持 js 时的提醒 -->
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

public/manifest.json

js
// 用于网页应用加壳转变手机应用时的配置项

public/robots.txt

js
// 爬虫规则文件,当爬虫在爬取页面时规定哪些东西可以爬取哪些不可以

src/reportWebVitals.js

js
// 用于记录网页性能,利用 web-vitals 实现
const reportWebVitals = onPerfEntry => {
  if (onPerfEntry && onPerfEntry instanceof Function) {
    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
      getCLS(onPerfEntry)
      getFID(onPerfEntry)
      getFCP(onPerfEntry)
      getLCP(onPerfEntry)
      getTTFB(onPerfEntry)
    })
  }
}
export default reportWebVitals

src/setupTests.js

js
// 用于应用的整体测试或某一个模块单元测试

jsx

基本使用

js
import React from 'react'
import ReactDOM from 'react-dom'
// 在 jsx 中设置 class 应写成 className
const title = (
  <h1 className="title">
    <span>xxx</span>
  </h1>
)
ReactDOM.render(title, document.getElementById('root'))

使用 js 表达式

js
import React from 'react'
import ReactDOM from 'react-dom'
// import { createRoot } from 'react-dom/client'; // ReactDOM.render在 react18 已不再支持,需引入 createRoot 函数
// 在 jsx 中使用js表达式
const name = 'jack'
const age = 10
const sayhi = () => 'hei'
// jsx 本身也是 js 表达式
const dv = <div>div</div>
const obj = { a: 1 } // 对象是一个例外,不能当做表达式直接使用,一般只会出现在 style 属性中
// 在 {} 中不允许出现语句如:if、for等
const title = (
  <div className="title">
    JSX,{name},年龄:{age},{age > 10 ? '11' : '10'},{sayhi()},{dv}
    {/* {obj} 对象是一个例外,不能当做表达式直接使用*/}
  </div>
)
// ReactDOM.render在 react18 已不再支持,可使用 createRoot 如下写法
// createRoot(document.getElementById('root')).render(title);
ReactDOM.render(title, document.getElementById('root'))

条件渲染

js
import React from 'react'
import ReactDOM from 'react-dom'
let flag = true
const isshow = () => {
  if (flag) {
    return <div>loading...</div>
  }
  return <div>加载数据</div>
}
const title = (
  <div className="title">
    {isshow()}
    {flag ? <div>loading...</div> : <div>加载数据</div>}
  </div>
)
ReactDOM.render(title, document.getElementById('root'))

列表渲染

js
import React from 'react'
import ReactDOM from 'react-dom'
const list = [
  { id: 1, name: '1' },
  { id: 2, name: '1' },
  { id: 3, name: '1' },
  { id: 4, name: '1' }
]
const title = (
  <div className="title">
    {/* 渲染一组数据时应该使用map方法,和vue一样需要添加key属性 */}
    {list.map(item => (
      <div key={item.id}>{item.name}</div>
    ))}
  </div>
)
ReactDOM.render(title, document.getElementById('root'))

样式处理

js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css' // 引入样式
const title = (
  // 行内样式遵循驼峰命名法
  <div style={{ backgroundColor: 'skyblue' }} className="title">
    行内样式
  </div>
)
ReactDOM.render(title, document.getElementById('root'))
css
.title {
  color: red;
}

react 组件

创建组件方式

js
// 方式一:使用js函数创建组件
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css' // 引入样式
// 函数组件:使用 js 函数创建的组件
// 约定1:函数名称必须以大写字母开头(react 据此区分组件和普通的 react 元素)
// 约定2:函数组件必须有返回值,表示该组件的结构
// 如果返回值为null,表示不渲染任何内容
// 使用函数名作为组件标签名
function Hello() {
  return <div>react组件</div>
}
const Hello2 = () => <div>react组件2</div>
const title = (
  <div style={{ backgroundColor: 'skyblue' }} className="title">
    <Hello />
    <Hello2 />
  </div>
)
ReactDOM.render(title, document.getElementById('root'))
js
// 方式二:使用ES6的class类创建组件
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css' // 引入样式
// 类组件:使用ES6的class类创建组件
// 约定1:类名称必须以大写字母开头(react 据此区分组件和普通的react元素)
// 约定2:类组件应该继承 React.Component 父类,从而可以使用父类中提供的方法和属性
// 约定3:类组件必须提供 render 方法
// 约定4:render 方法必须有返回值,表示该组件的结构
class Hello extends React.Component {
  render() {
    return <div>类组件</div>
  }
}
const title = (
  <div style={{ backgroundColor: 'skyblue' }} className="title">
    <Hello />
  </div>
)
ReactDOM.render(title, document.getElementById('root'))

抽离组件

js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css' // 引入样式
import Hello from './Hello' // 引入组件
const title = (
  <div style={{ backgroundColor: 'skyblue' }} className="title">
    <Hello />
  </div>
)
ReactDOM.render(title, document.getElementById('root'))
js
import React from 'react'
class Hello extends React.Component {
  render() {
    return <div>类组件</div>
  }
}
export default Hello

事件处理

js
import React from 'react'
import ReactDOM from 'react-dom'
// 事件绑定
// React事件名采用驼峰命名法
class Hello extends React.Component {
  // React中的事件对象叫做合成事件(对象)
  // 合成事件:兼容所有浏览器,无需担心跨浏览器兼容性问题
  hand(e) {
    console.log(e)
  }
  render() {
    return <div onClick={this.hand}>类组件</div>
  }
}
function App() {
  function hand(e) {
    console.log(e)
  }
  return <div onClick={hand}>函数组件</div>
}
const title = (
  <div style={{ backgroundColor: 'skyblue' }} className="title">
    <Hello />
    <App />
  </div>
)
ReactDOM.render(title, document.getElementById('root'))

有状态组件和无状态组件

js
// 函数组件又叫做无状态组件,类组件又叫做有状态组件
// 无状态组件没有自己的状态,只负责数据展示(非响应式)
// 类组件有自己的状态,负责更新 ui 让页面动起来

组件中的 state 和 setState

js
import React from 'react'
import ReactDOM from 'react-dom'
// state 和 setState 的基本使用
// 状态(state)是私有的只能在组件内部使用
class Hello extends React.Component {
  // constructor(){
  //   super()
  //   // 初始化 state
  //   this.state = {
  //     count:0
  //   }
  // }
  // 简化初始化 state语法
  state = {
    count: 0
  }
  render() {
    return (
      <div>
        <div>计数器:{this.state.count}</div>
        <button
          onClick={() => {
            // 通过 this.state.count 直接修改state是不可取的,需要使用 setState 方法来修改,这样是响应式的
            this.setState({
              count: this.state.count + 1
            })
          }}
        >
          +1
        </button>
      </div>
    )
  }
}
const title = (
  <div style={{ backgroundColor: 'skyblue' }} className="title">
    <Hello />
  </div>
)
ReactDOM.render(title, document.getElementById('root'))

this 指向问题

js
import React from 'react'
import ReactDOM from 'react-dom'
class Hello extends React.Component {
  state = {
    count: 0
  }
  // 事件
  onIncrement() {
    // 这里的 this 不再是组件本身了,所以会报错
    this.setState({
      count: this.state.count + 1
    })
  }
  render() {
    return (
      <div>
        <div>计数器:{this.state.count}</div>
        <button onClick={this.onIncrement}>+1</button>
        {/* <button onClick={()=>{
          这种方式使用的是箭头函数 this 指向组件本身所以不会报错
          this.setState({
            count:this.state.count+1
          })
        }}>+1</button> */}
      </div>
    )
  }
}
const title = (
  <div style={{ backgroundColor: 'skyblue' }} className="title">
    <Hello />
  </div>
)
ReactDOM.render(title, document.getElementById('root'))

解决

js
// 方式一
import React from 'react'
import ReactDOM from 'react-dom'
class Hello extends React.Component {
  state = {
    count: 0
  }
  onIncrement() {
    this.setState({
      count: this.state.count + 1
    })
  }
  render() {
    return (
      <div>
        <div>计数器:{this.state.count}</div>
        {/* <button onClick={this.onIncrement}>+1</button> */}
        {/* 利用箭头函数 */}
        <button onClick={() => this.onIncrement()}>+1</button>
      </div>
    )
  }
}
const title = (
  <div style={{ backgroundColor: 'skyblue' }} className="title">
    <Hello />
  </div>
)
ReactDOM.render(title, document.getElementById('root'))

// 方式二
class Hello extends React.Component {
  // 利用bind方法解决
  constructor() {
    super()
    this.onIncrement = this.onIncrement.bind(this)
  }
  state = {
    count: 0
  }
  onIncrement() {
    this.setState({
      count: this.state.count + 1
    })
  }
  render() {
    return (
      <div>
        <div>计数器:{this.state.count}</div>
        <button onClick={this.onIncrement}>+1</button>
      </div>
    )
  }
}
// 方法三(推荐)
import React from 'react'
import ReactDOM from 'react-dom'
class Hello extends React.Component {
  state = {
    count: 0
  }
  // 利用箭头函数形式的 class 实例解决
  // 该语法是实验性语法,但是由于 babel 的存在可以直接使用
  onIncrement = () => {
    this.setState({
      count: this.state.count + 1
    })
  }
  render() {
    return (
      <div>
        <div>计数器:{this.state.count}</div>
        <button onClick={this.onIncrement}>+1</button>
      </div>
    )
  }
}
const title = (
  <div style={{ backgroundColor: 'skyblue' }} className="title">
    <Hello />
  </div>
)
ReactDOM.render(title, document.getElementById('root'))

表单处理

js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
// 受控组件:其值受到react控制的元素
// react 将 state 和表单元素的value绑定一起,由state的值来控制表单元素的值
class Hello extends React.Component {
  state = {
    txt: '',
    content: '',
    city: 'sh',
    ische: false
  }
  onIncrement = e => {
    this.setState({
      txt: e.target.value
    })
  }
  onIncrement2 = e => {
    this.setState({
      content: e.target.value
    })
  }
  onIncrement3 = e => {
    this.setState({
      city: e.target.value
    })
  }
  onIncrement4 = e => {
    this.setState({
      ische: e.target.checked
    })
  }
  render() {
    return (
      <div>
        {/* 这些数据都是响应式的 */}
        <input type="text" value={this.state.txt} onChange={this.onIncrement} />
        <div>{this.state.txt}</div>
        <textarea
          value={this.state.content}
          onChange={this.onIncrement2}
        ></textarea>
        <select value={this.state.city} onChange={this.onIncrement3}>
          <option value="sh">上海</option>
          <option value="bj">北京</option>
          <option value="gz">广州</option>
        </select>
        <input
          type="checkbox"
          checked={this.state.ische}
          onChange={this.onIncrement4}
        />
      </div>
    )
  }
}
ReactDOM.render(<Hello />, document.getElementById('root'))

// 优化版本
class Hello extends React.Component {
  state = {
    txt: '',
    content: '',
    city: 'sh',
    ische: false
  }
  onIncrement = e => {
    const target = e.target
    // 根据类型获取值
    const value = target.type === 'checkbox' ? target.checked : target.value
    // 获取 name
    const name = target.name
    this.setState({
      // [键名]:ES6 语法动态键名
      [name]: value
    })
    console.log(this.state[name])
  }
  render() {
    return (
      <div>
        <input
          type="text"
          name="txt"
          value={this.state.txt}
          onChange={this.onIncrement}
        />
        <div>{this.state.txt}</div>
        <textarea
          value={this.state.content}
          name="content"
          onChange={this.onIncrement}
        ></textarea>
        <select value={this.state.city} name="city" onChange={this.onIncrement}>
          <option value="sh">上海</option>
          <option value="bj">北京</option>
          <option value="gz">广州</option>
        </select>
        <input
          type="checkbox"
          name="ische"
          checked={this.state.ische}
          onChange={this.onIncrement}
        />
      </div>
    )
  }
}
ReactDOM.render(<Hello />, document.getElementById('root'))

非受控组件

js
// 通过非受控组件获取表单元素的值(不推荐)
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
// 非受控组件
class Hello extends React.Component {
  constructor() {
    super()
    // 创建 ref 获取 Dom对象
    this.txtRef = React.createRef()
  }
  gettxt = () => {
    console.log(this.txtRef.current.value)
  }
  render() {
    return (
      <div>
        <input type="text" ref={this.txtRef} />
        <button onClick={this.gettxt}>文本框的值</button>
      </div>
    )
  }
}
ReactDOM.render(<Hello />, document.getElementById('root'))

组件通信

父传子

js
// props 的值是只读的所以不可修改
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
// 函数组件传递方式
const Hello1 = props => {
  // 在子组件中通过形参获得数据
  return (
    <div>
      <h1>
        函数组件:{props.name},{props.age}
      </h1>
    </div>
  )
}
// 类组件
class Hello extends React.Component {
  // 在构造函数中必须将 props 传递给 super 函数,这样在构造函数中才能获取到数据
  constructor(props) {
    super(props)
    console.log(props)
  }
  render() {
    return (
      <h1>
        类组件{this.props.name},{this.props.age}
      </h1>
    )
  }
}
// 传递数据,可以传递任意类型的数据
ReactDOM.render(
  <Hello1 name="jack" age="45" age={[1, 35]} tag={<p>p</p>} />,
  document.getElementById('root')
)

props 深入

js
// children 属性记录父组件里的内容
const Test = () => {
  return <button>test组件</button>
}
const App = props => {
  console.log(props)
  return (
    <div>
      <h1>{props.children}</h1>
    </div>
  )
}
// 父组件里的内容可以是html元素也可以是组件
ReactDOM.render(
  <App>
    <p>子节点</p>
    <Test />
  </App>,
  document.getElementById('root')
)
js
const App = props => {
  props.children()
  return <div></div>
}
// 还可以是函数
ReactDOM.render(
  <App>{() => console.log('函数子节点')}</App>,
  document.getElementById('root')
)

props 校验

js
const App = props => {
  const arr = props.str.replace('')
  return <div></div>
}
// 当外侧使用 App 组件时不知道规则,本来需要字符串却传递了一个数字,那么就会导致组件内报错,也不好排查错误
ReactDOM.render(<App str={55} />, document.getElementById('root'))

安装 prop-types

bash
npm i prop-types # 用于传参类型校验
js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import PropTypes from 'prop-types' // 导入插件
const App = props => {
  const arr = props.str.replace('.')
  return <div></div>
}
// 添加 props 校验
App.propTypes = {
  str: PropTypes.string
}
ReactDOM.render(<App str={55} />, document.getElementById('root'))

这样就能知道是参数类型错误了

js
// 常用校验规则
App.propTypes = {
  a: PropTypes.string, // 字符串
  fn: PropTypes.func.isRequired, // 类型为函数且是必填项
  tag: PropTypes.element, // React 元素(element)
  filter: PropTypes.shape({
    // 指定固定参数和对应类型
    area: PropTypes.number,
    price: PropTypes.bool // 布尔类型
  })
}

props 默认值

js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
const App = props => {
  return (
    <div>
      <h1>{props.pagesize}</h1>
    </div>
  )
}
// 添加 props 默认值,只在未传入值时生效
App.defaultProps = {
  pagesize: 10
}
ReactDOM.render(<App />, document.getElementById('root'))

子传父

js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
class Hello extends React.Component {
  state = {
    info: ''
  }
  // 提供回调函数供子组件传递数据
  getinfo = data => {
    this.setState({
      info: data
    })
  }
  render() {
    return (
      <div>
        父组件:{this.state.info}
        <Child getinfo={this.getinfo} />
      </div>
    )
  }
}
function Child(props) {
  const info = '5'
  const cinfo = function () {
    props.getinfo(info)
  }
  return (
    <div>
      子组件
      <button onClick={cinfo}>传递数据</button>
    </div>
  )
}
ReactDOM.render(<Hello />, document.getElementById('root'))

兄弟组件通信

js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
// 兄弟组件通信
class Hello extends React.Component {
  // 提供共享状态
  state = {
    count: 0
  }
  // 提供修改状态的方法
  getinfo = () => {
    this.setState({
      count: this.state.count + 1
    })
  }
  render() {
    return (
      <div>
        <Child count={this.state.count} />
        <Child2 getinfo={this.getinfo} />
      </div>
    )
  }
}
function Child(props) {
  return (
    <div>
      <h1>数值{props.count}</h1>
    </div>
  )
}
class Child2 extends React.Component {
  render() {
    return <button onClick={this.props.getinfo}>+1</button>
  }
}
ReactDOM.render(<Hello />, document.getElementById('root'))

祖孙通信

js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
// 祖孙通信
// 创建 context 得到两个组件
const { Provider, Consumer } = React.createContext()
class Hello extends React.Component {
  state = {
    count: 100
  }

  render() {
    return (
      <Provider value={this.state.count}>
        <div>
          <Child />
        </div>
      </Provider>
    )
  }
}
function Child() {
  return (
    <div>
      Child
      <Child2 />
    </div>
  )
}
function Child2() {
  return (
    <div>
      <Consumer>{data => <span>{data}</span>}</Consumer>
    </div>
  )
}
ReactDOM.render(<Hello />, document.getElementById('root'))

组件生命周期

挂载阶段

js
// 创建时(挂载阶段) 三个阶段
// constructor:创建组件时,最先执行,作用:初始化 state 为事件处理程序绑定this
// render:每次组件渲染都会触发,作用:渲染UI(不能调用setState 方法,会导致递归更新(页面)最终报错)
// componentDidMount:组件挂载(完成DOM渲染)后,作用:发送网络请求,DOM操作
class App extends React.Component {
  constructor(props) {
    super(props)
    console.log('生命周期钩子,constructor')
  }
  componentDidMount() {
    console.log('生命周期钩子,componentDidMount')
  }
  render() {
    console.log('生命周期钩子,render')
    return (
      <div>
        <h1>打豆豆次数:</h1>
        <button>打豆豆</button>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'))

更新阶段

js
// 更新时
// 每次调用 setState 修改数据时会执行 render 生命周期钩子
// 当组件接收到新属性时会执行 render 生命周期钩子
// 当调用 forceUpdate 时会执行 render 生命周期钩子
class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    }
  }
  updata = () => {
    // this.setState({
    //   count:this.state.count+1
    // })
    // 强制更新
    this.forceUpdate()
  }
  render() {
    console.log('生命周期钩子,render')
    return (
      <div>
        <Count count={this.state.count} />
        <button onClick={this.updata}>打豆豆</button>
      </div>
    )
  }
}
class Count extends React.Component {
  render() {
    console.log('子组件生命周期钩子,render')
    return <h1>打豆豆次数:{this.props.count}</h1>
  }
}
ReactDOM.render(<App />, document.getElementById('root'))
js
// 更新时
// componentDidUpdate:组件更新(完成DOM渲染)后,作用:发送网络请求,DOM操作(注意:如果要 setState 必须放在一个 if 条件中)
class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    }
  }
  updata = () => {
    this.setState({
      count: this.state.count + 1
    })
    // 强制更新
    // this.forceUpdate()
  }
  render() {
    console.log('生命周期钩子,render')
    return (
      <div>
        <Count count={this.state.count} />
        <button onClick={this.updata}>打豆豆</button>
      </div>
    )
  }
}
class Count extends React.Component {
  render() {
    console.log('子组件生命周期钩子,render')
    return <h1 id="title">打豆豆次数:{this.props.count}</h1>
  }
  // 如果需要调用 setState 修改状态必须放在一个 if 条件中,否则就会递归更新,最终导致报错
  componentDidUpdate(prevProps) {
    // componentDidUpdate 函数参数(prevProps)表示上一次接收到的 props 数据
    console.log('子组件生命周期钩子,componentDidUpdate')
    // 获取DOM
    const title = document.getElementById('title')
    console.log(title)
    // this.setState({}) // 直接调用会报错
    if (prevProps.count !== this.props.count) {
      this.setState({}) // 这时就可以正常执行
    }
  }
}
ReactDOM.render(<App />, document.getElementById('root'))

卸载阶段

js
// 组件卸载时执行
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    }
  }
  updata = () => {
    this.setState({
      count: this.state.count + 1
    })
  }
  render() {
    return (
      <div>
        {this.state.count > 4 ? '销毁' : <Count count={this.state.count} />}
        <button onClick={this.updata}>打豆豆</button>
      </div>
    )
  }
}
class Count extends React.Component {
  render() {
    return <h1 id="title">打豆豆次数:{this.props.count}</h1>
  }
  componentWillUnmount() {
    console.log('组件销毁生命周期钩子执行')
  }
}
ReactDOM.render(<App />, document.getElementById('root'))

组件复用

render-props 模式

js
// render props模式 类似于 vue 里的插槽
class App extends React.Component {
  render() {
    return (
      <div>
        <h1>render props模式</h1>
        {/* 这里 render 函数的形参就能拿到组件内部暴露出来的数据,进而根据需求返回html结构交给子组件渲染 */}
        <Mouse
          render={mouse => {
            return (
              <p>
                x:{mouse.x},y:{mouse.y}
              </p>
            )
          }}
        />
        <Mouse
          render={mouse => {
            return (
              <p
                style={{
                  position: 'fixed',
                  top: mouse.y - 50,
                  left: mouse.x - 50,
                  backgroundColor: 'red',
                  width: '100px',
                  height: '100px'
                }}
              ></p>
            )
          }}
        />
      </div>
    )
  }
}
class Mouse extends React.Component {
  state = {
    x: 0,
    y: 0
  }
  move = e => {
    this.setState({
      x: e.clientX,
      y: e.clientY
    })
  }
  // 在页面渲染完成后获取DOM元素
  componentDidMount() {
    window.addEventListener('mousemove', this.move)
  }
  render() {
    // 将需要复用的数据暴露出去,这里的函数名称是可自定义的
    return this.props.render(this.state)
  }
}
ReactDOM.render(<App />, document.getElementById('root'))

使用 children 代替 render 写法(推荐)

js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import PropTypes from 'prop-types'
class App extends React.Component {
  render() {
    return (
      <div>
        <h1>render props模式</h1>
        {/* 这里直接将需要渲染的html直接放在组件中即可 */}
        <Mouse>
          {mouse => {
            return (
              <p>
                x:{mouse.x},y:{mouse.y}
              </p>
            )
          }}
        </Mouse>
        <Mouse>
          {mouse => {
            return (
              <p
                style={{
                  position: 'fixed',
                  top: mouse.y - 50,
                  left: mouse.x - 50,
                  backgroundColor: 'red',
                  width: '100px',
                  height: '100px'
                }}
              ></p>
            )
          }}
        </Mouse>
      </div>
    )
  }
}
class Mouse extends React.Component {
  state = {
    x: 0,
    y: 0
  }
  move = e => {
    this.setState({
      x: e.clientX,
      y: e.clientY
    })
  }
  componentDidMount() {
    window.addEventListener('mousemove', this.move)
  }
  render() {
    // 这种写法就不能使用 this.props.render(this.state) 需要改成 children
    return this.props.children(this.state)
  }
}
// 使用这种写法时推荐添加 props 校验
Mouse.prototype = {
  children: PropTypes.func.isRequired // 规定 children 属性是函数类型且必传
}
ReactDOM.render(<App />, document.getElementById('root'))

简写方式

js
function App() {
  return (
    <div className="App">
      <A render1={val => <B val={val} />} />
    </div>
  )
}
function A(props) {
  return (
    <>
      <p>A组件</p>
      {/* 这样可以实现插槽的功能 */}
      {props.render1('这是传递过去的参数')}
    </>
  )
}
function B(props) {
  return <p>B组件,值{props.val}</p>
}

高阶组件

js
// 函数名称约定以 with 开头
// 指定函数参数,参数应该以大写字母开头(作为要渲染的组件)
// 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回
// 在该组件中,渲染参数组件,同时将状态通过 prop 传递给参数组件

// 创建高阶组件
function withMouse(WrappedComponent) {
  // 该组件提供复用的状态逻辑
  class Mouse extends React.Component {
    state = {
      x: 0,
      y: 0
    }
    move = e => {
      this.setState({
        x: e.clientX,
        y: e.clientY
      })
    }
    componentDidMount() {
      window.addEventListener('mousemove', this.move)
    }
    // 将传递进来的html结构渲染并将组件状态传递过去
    render() {
      return <WrappedComponent {...this.state} />
    }
  }
  // 返回组件
  return Mouse
}
// 使用高阶组件
const Position = props => (
  <p>
    鼠标当前位置:x:{props.x},y:{props.y}
  </p>
)
const MousePosition = withMouse(Position)

const div = props => (
  <p
    style={{
      position: 'fixed',
      top: props.y - 50,
      left: props.x - 50,
      backgroundColor: 'red',
      width: '100px',
      height: '100px'
    }}
  ></p>
)
const Mousediv = withMouse(div)
class App extends React.Component {
  render() {
    return (
      <div>
        <h1>高阶组件</h1>
        <MousePosition></MousePosition>
        <Mousediv></Mousediv>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'))

简化版本

js
// 定义高阶组件
function withInfo(Info) {
  function Gao() {
    const info = '外部需要的已经处理好的数据'
    return <Info info={info} />
  }
  return Gao
}
function A(props) {
  return <p>A组件,值{props.info}</p>
}
// 使用高阶组件
const InfoA = withInfo(A)

设置 displayName

js
// 默认情况下,React 使用组件名称作为 displayName
// 为高阶组件设置 displayName 便于调试时区分不同组件
js
// 只需在高阶组件中设置 displayName 即可
function withMouse(WrappedComponent) {
  // 该组件提供复用的状态逻辑
  class Mouse extends React.Component {
    state = {
      x: 0,
      y: 0
    }
    move = e => {
      this.setState({
        x: e.clientX,
        y: e.clientY
      })
    }
    componentDidMount() {
      window.addEventListener('mousemove', this.move)
    }
    // 将传递进来的html结构渲染并将组件状态传递过去
    render() {
      return <WrappedComponent {...this.state} />
    }
  }
  // 设置组件名称///////////////////////////////////////////////////////////////
  Mouse.displayName = `withMouse${getDisplayName(WrappedComponent)}`
  // 返回组件
  return Mouse
}
// 封装高阶组件名称函数//////////////////////////////////////////////////////////////
function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}
// 使用高阶组件
const Position = props => (
  <p>
    鼠标当前位置:x:{props.x},y:{props.y}
  </p>
)
const MousePosition = withMouse(Position)

const div = props => (
  <p
    style={{
      position: 'fixed',
      top: props.y - 50,
      left: props.x - 50,
      backgroundColor: 'red',
      width: '100px',
      height: '100px'
    }}
  ></p>
)
const Mousediv = withMouse(div)
class App extends React.Component {
  render() {
    return (
      <div>
        <h1>高阶组件</h1>
        <MousePosition></MousePosition>
        <Mousediv></Mousediv>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'))

传递 props

js
// 如果不设置就会丢失 props
// 原因是因为高阶组件并没有继续向下传递 props,在高阶组件内是可以拿到的

解决

js
// 只需要在高阶函数中将 props 向下传递即可
js
// 创建高阶组件
function withMouse(WrappedComponent) {
  // 该组件提供复用的状态逻辑
  class Mouse extends React.Component {
    state = {
      x: 0,
      y: 0
    }
    move = e => {
      this.setState({
        x: e.clientX,
        y: e.clientY
      })
    }
    componentDidMount() {
      window.addEventListener('mousemove', this.move)
    }
    // 将传递进来的html结构渲染并将组件状态传递过去
    render() {
      // 这里将 props 向下传递//////////////////////////////////////////////////
      return <WrappedComponent {...this.state} {...this.props} />
    }
  }
  // 设置组件名称
  Mouse.displayName = `withMouse${getDisplayName(WrappedComponent)}`
  // Mouse.displayName = `ddd`
  // 返回组件
  return Mouse
}
// 封装高阶组件名称函数
function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}
// 使用高阶组件
const Position = props => (
  <p>
    鼠标当前位置:x:{props.x},y:{props.y}
  </p>
)
const MousePosition = withMouse(Position)

const div = props => {
  console.log(props) // 组件就可以使用 props ///////////////////////////////////////////
  return (
    <p
      style={{
        position: 'fixed',
        top: props.y - 50,
        left: props.x - 50,
        backgroundColor: 'red',
        width: '100px',
        height: '100px'
      }}
    ></p>
  )
}
const Mousediv = withMouse(div)
class App extends React.Component {
  render() {
    return (
      <div>
        <h1>高阶组件</h1>
        <MousePosition></MousePosition>
        <Mousediv a="传递的props属性"></Mousediv> //
        这里传递数据/////////////////////////
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'))

错误边界

js
// 错误边界可以捕获后代组件可能出现的错误并处理,不至于一个组件出错导致整个整个应用崩溃
function App() {
  return (
    <div className="App">
      <Fu />
    </div>
  )
}
// 错误边界:react 错误边界 getDerivedStateFromError、componentDidCatch 捕获后代组件的生命周期函数中的错误!
class Fu extends Component {
  state = {
    hasError: '' // 固定写法,值为空字符串,不能写别的,不然无法达到效果
  }
  // static getDerivedStateFromError(error) {
  //   console.log(error); // getDerivedStateFromError 钩子能捕获后代组件的错误并收集到形参中
  //   return {
  //     hasError: error, // 和state中hasError字段对应
  //   };
  // }
  // 使用 componentDidCatch 同样可以实现
  // React 16 将提供一个内置函数 componentDidCatch,如果 render() 函数抛出错误,则会触发该函数。
  componentDidCatch(error, info) {
    console.log(error, info)
    this.setState({
      hasError: error // 和state中hasError字段对应
    })
  }
  render() {
    return (
      <>
        <p>父组件</p>
        {this.state.hasError ? <span>网络不稳定</span> : <Zi />}
      </>
    )
  }
}

function Zi() {
  const info = '' // 该组件渲染时出错
  return (
    <>
      {info.map(item => (
        <p key={item.id}>{item.name}</p>
      ))}
    </>
  )
}

react 原理

setState 说明

js
// setState 是异步更新数据的
class App extends React.Component {
  state = {
    count: 1
  }
  handl = () => {
    this.setState({
      count: this.state.count + 1
    })
    console.log(this.state.count) // 此时打印的还是更新之前的数据
    // 后面的 setState 不要依赖于前面的 setState
    this.setState({
      count: this.state.count + 1
    })
    console.log(this.state.count) // 此时打印的还是更新之前的数据
  }
  // 当调用多次 setState 只会触发一次重新渲染(render 钩子)
  render() {
    return (
      <div>
        <h1>数据{this.state.count}</h1>
        <button onClick={this.handl}>更新</button>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'))

推荐语法

js
class App extends React.Component {
  state = {
    count: 1
  }
  handl = () => {
    // state 表示最新的 state 数据,props 表示最新的 props
    this.setState((state, props) => {
      return {
        count: state.count + 1
      }
    })
    this.setState((state, props) => {
      return {
        count: state.count + 1
      }
    })
    // 这种写法也是异步更新数据的
    console.log(this.state.count) // 1
  }
  // 当调用多次 setState 只会触发一次重新渲染(render 钩子)
  render() {
    return (
      <div>
        <h1>数据{this.state.count}</h1>
        <button onClick={this.handl}>更新</button>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'))

第二个参数

js
// 当在状态更新后(页面完成重新渲染)立即执行某个操作,类似于 vue 里的 nextTick
class App extends React.Component {
  state = {
    count: 1
  }
  handl = () => {
    this.setState(
      state => {
        return {
          count: state.count + 1
        }
      },
      () => {
        // 这个回调函数会在页面重新渲染之后执行
        console.log(this.state.count) // 这时就能获取到更新后的 count
      }
    )
  }
  render() {
    return (
      <div>
        <h1>数据{this.state.count}</h1>
        <button onClick={this.handl}>更新</button>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'))

JSX 转化过程

组件更新机制

组件性能优化

减轻 state

避免不必要的重新渲染

js
class App extends React.Component {
  state = {
    count: 0
  }
  handl = () => {
    this.setState(state => {
      return {
        count: state.count + 1
      }
    })
  }
  // 更新前钩子
  shouldComponentUpdate(nextProps, nextState) {
    // return false 返回 false 则该组件不会重新渲染
    // nextProps 表示最新的 props 数据,nextState 表示最新的 state 数据
    // this.state 可以通过这种方式获取更新前的数据
    return true
  }
  render() {
    return (
      <div>
        <h1>数据{this.state.count}</h1>
        <button onClick={this.handl}>更新</button>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'))
js
// 阻止渲染案例
class App extends React.Component {
  state = {
    num: 0
  }
  handl = () => {
    this.setState({
      num: Math.floor(Math.random() * 3)
    })
  }
  shouldComponentUpdate(nextProps, nextState) {
    // 当最新计算出来的值和更新前的值一样时阻止重新渲染,提升性能
    return nextState.num !== this.state.num
  }
  render() {
    return (
      <div>
        <h1>随机数:{this.state.num}</h1>
        <button onClick={this.handl}>重新生成</button>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'))
js
class App extends React.Component {
  state = {
    num: 0
  }
  handl = () => {
    this.setState({
      num: Math.floor(Math.random() * 3)
    })
  }
  render() {
    return (
      <div>
        <Number num={this.state.num} />
        <button onClick={this.handl}>重新生成</button>
      </div>
    )
  }
}
// 用最新的 props 值来阻止更新
class Number extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps.num !== this.props.num
  }
  render() {
    console.log(1)
    return (
      <div>
        <h1>随机数:{this.props.num}</h1>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'))

纯组件

js
class App extends React.PureComponent {
  state = {
    num: 0
  }
  handl = () => {
    this.setState({
      num: Math.floor(Math.random() * 3)
    })
  }
  // 只要继承自 PureComponent ,那么我们就不需要做下面阻止渲染的操作,更加简便
  // shouldComponentUpdate(nextProps,nextState){
  //   // 当最新计算出来的值和更新前的值一样时阻止重新渲染,提升性能
  //   return nextState.num !== this.state.num
  // }
  render() {
    console.log(1)
    return (
      <div>
        <h1>随机数:{this.state.num}</h1>
        <button onClick={this.handl}>重新生成</button>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'))

纯组件内部比较机制

问题:直接修改原对象的值

解决:创建一个新的对象然后重新赋值

虚拟 DOM 和 Diff 算法

渲染 html 字符串

js
// 由于 react 为了防止脚本注入攻击(XSS),做了特殊的防护机制,所以在 react 中无法直接渲染 html 字符串
// 可以通过 dangerouslySetInnerHTML 属性进行渲染 __html 的值就写入 html 字符串
export const Detail: FC<RouteComponentProps<MatchParams>> = props => {
  const product = {
    features: '<div>这是内容</div>'
  }
  return (
    <>
      <div dangerouslySetInnerHTML={{ __html: product.features }}></div>
    </>
  )
}

react 路由

安装

bash
npm i react-router-dom

基本使用(v5 版本)

js
// 导入路由组件 v6 版本语法
import { BrowserRouter, Route, Link, Routes } from 'react-router-dom'
const First = () => <p>页面一内容</p>
// 用 Router 组件包裹整个应用
const App = () => (
  <BrowserRouter>
    <div>
      <h1>React路由基础</h1>
      {/* 指定路由入口 最终 Link 组件会被编译成 a 标签*/}
      <Link to="/first">页面一</Link>
      {/* 由于路由插件版本升级,必须在 Route 组件外包裹 Routes 组件*/}
      <Routes>
        {/* 指定路由出口 element 指定要展示的内容(组件)*/}
        {/* path 指定路由规则 */}
        {/* 由于路由插件版本升级,以前的 component 写法已不再支持应替换为 element*/}
        <Route path="first" element={<First />}></Route>
      </Routes>
    </div>
  </BrowserRouter>
)
ReactDOM.render(<App />, document.getElementById('root'))
// v5 版本写法
import { BrowserRouter, Route, Link } from 'react-router-dom'
const First = () => <p>页面一内容</p>
// 用 Router 组件包裹整个应用
const App = () => (
  <BrowserRouter>
    <div>
      <h1>React路由基础</h1>
      <Link to="/first">页面一</Link>
      <Route path="/first" component={First}></Route>
    </div>
  </BrowserRouter>
)
ReactDOM.render(<App />, document.getElementById('root'))

HashRouter

js
// 导入路由组件
import { Route, Link, Routes, HashRouter } from "react-router-dom";
const First = () => <p>页面一内容</p>;
// 用 Router 组件包裹整个应用
const App = () => (
  {/* 和 BrowserRouter 的区别是地址栏上有无 # 和 vue 路由的 hash 模式、history 模式类似*/}
  {/* 使用 BrowserRouter 上线时需要后端做处理*/}
  <HashRouter>
    <div>
      <h1>React路由基础</h1>
      <Link to="/first">页面一</Link>
      <Routes>
        <Route path="first" element={<First />}></Route>
      </Routes>
    </div>
  </HashRouter>
);
ReactDOM.render(<App />, document.getElementById("root"));
// v5 版本写法
import { HashRouter, Route, Link } from "react-router-dom";
const First = () => <p>页面一内容</p>;
// 用 Router 组件包裹整个应用
const App = () => (
  <HashRouter>
    <div>
      <h1>React路由基础</h1>
      <Link to="/first">页面一</Link>
      <Route path="/first" component={First}></Route>
    </div>
  </HashRouter>
);
ReactDOM.render(<App />, document.getElementById("root"));

路由执行过程

js
// 导入路由组件
import { Route, Link, Routes, BrowserRouter } from 'react-router-dom'
const First = () => <p>页面一内容</p>
const First2 = () => <p>页面二内容</p>
// 用 Router 组件包裹整个应用
class App extends React.Component {
  render() {
    return (
      <BrowserRouter>
        <div>
          <h1>React路由基础</h1>
          {/* 指定路由入口 */}
          <Link to="/first">页面一</Link>
          <Link to="/first2">页面一</Link>
          <Routes>
            {/* 指定路由出口 element 指定要展示的内容(组件)*/}
            <Route path="first" element={<First />}></Route>
            <Route path="first2" element={<First2 />}></Route>
          </Routes>
        </div>
      </BrowserRouter>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'))
// v5 版本写法
import { Route, Link, BrowserRouter } from 'react-router-dom'
const First = () => <p>页面一内容</p>
const First2 = () => <p>页面二内容</p>
// 用 Router 组件包裹整个应用
class App extends React.Component {
  render() {
    return (
      <BrowserRouter>
        <div>
          <h1>React路由基础</h1>
          <Link to="/first">页面一</Link>
          <Link to="/first2">页面一</Link>
          <Route path="/first" component={First}></Route>
          <Route path="/first2" component={First2}></Route>
        </div>
      </BrowserRouter>
    )
  }
}
ReactDOM.render(<App />, document.getElementById('root'))

编程式导航

js
import { Route, Link, BrowserRouter } from 'react-router-dom'
class Login extends React.Component {
  link = () => {
    // 编程式导航
    this.props.history.push('/home')
  }
  render() {
    return (
      <div>
        <h1>登录页</h1>
        <button onClick={this.link}>跳转</button>
      </div>
    )
  }
}
const Home = props => {
  const fanhui = function () {
    // 在函数组件中通过 props 属性来进行页面跳转 go 函数表示回退和前进,参数(负数为回退正数为前进)为回退(前进)步数
    props.history.go(-1)
  }
  return (
    <div>
      <h1>首页</h1>
      <button onClick={fanhui}>回退</button>
    </div>
  )
}
const App = () => (
  <BrowserRouter>
    <div>
      <Link to="/login">去登录</Link>
      {/* path 为 / 时表示默认路由 */}
      <Route path="/login" component={Login}></Route>
      <Route path="/home" component={Home}></Route>
    </div>
  </BrowserRouter>
)
ReactDOM.render(<App />, document.getElementById('root'))

匹配模式

js
// 问题:默认路由在任何规则下都能匹配成功(怎么样默认路由对应的组件都会显示)

精确匹配

js
import { Route, Link, BrowserRouter } from 'react-router-dom'
class Login extends React.Component {
  link = () => {
    // 编程式导航
    this.props.history.push('/home')
  }
  render() {
    return (
      <div>
        <h1>登录页</h1>
        <button onClick={this.link}>跳转</button>
      </div>
    )
  }
}
const Home = props => {
  const fanhui = function () {
    // 在函数组件中通过 props 属性来进行页面跳转 go 函数表示回退和前进
    props.history.go(-1)
  }
  return (
    <div>
      <h1>首页</h1>
      <button onClick={fanhui}>回退</button>
    </div>
  )
}
const App = () => (
  <BrowserRouter>
    <div>
      <Link to="/login">去登录</Link>
      <Route path="/login" component={Login}></Route>
      {/* 添加 exact 属性即可变为精确匹配,推荐给默认路由添加精准匹配 */}
      <Route exact path="/" component={Home}></Route>
    </div>
  </BrowserRouter>
)
ReactDOM.render(<App />, document.getElementById('root'))

配置 404 页面

js
import { Home } from './page'
import { BrowserRouter, Route, Switch } from 'react-router-dom'
function App() {
  return (
    <div className={styles.App}>
      <BrowserRouter>
        {/* Switch 组件可以指定每次进行路由匹配时只会显示一个组件(路由) */}
        <Switch>
          <Route path="/" exact component={Home} />
          <Route path="/signIn" render={() => <h1>登录页面</h1>} />
          {/* 不指定 path 则表示匹配所有上面定义未匹配的(不存在的路径)路径, 404匹配必须放在所有 path 最底下*/}
          <Route render={() => <h1>404页面</h1>} />
        </Switch>
      </BrowserRouter>
    </div>
  )
}
export default App

路由传参

params 参数

js
// 在组件中传递 params 参数
;<Link to="/detail/2"></Link>

// 声明接收参数
function App() {
  return (
    <BrowserRouter>
      <div className="App">
        {/* id 就表示要动态捕获的路由参数 */}
        <Route path="/detail/:id" component={HouseDetail}></Route>
        {/* 路由参数后加上 ? 表示参数可选 */}
        <Route path="/detail2/:id?" component={HouseDetail2}></Route>
        <Route path="/" exact render={() => <Redirect to="/home" />}></Route>
      </div>
    </BrowserRouter>
  )
}

// 在组件中获取
export default class HouseDetail extends Component {
  componentDidMount() {
    console.log(this.props.match.params)
  }
}

search 参数

js
// 在组件中传递 search 参数,search 参数无需声明接收可直接使用
;<Link to="/detail/?id=2"></Link>

// 在组件中获取
export default class HouseDetail extends Component {
  render() {
    console.log(this.props.location.search)
  }
}

state 参数

js
// 在jsx中传递参数
;<Link
  to={{ pathname: '/rent/add', state: { name: communityName, id: community } }}
></Link>

// 在js中传递
onTipsClick = ({ communityName, community }) => {
  // 通过 replace 或者 push 跳转时可以添加第二个参数表示需要传递的参数,这种方式传递的参数不在地址栏显示且刷新还在(仅在 BrowserRouter 下刷新不丢失,HashRouter 刷新会丢失)
  this.props.history.replace('/rent/add', {
    name: communityName,
    id: community
  })
}

// 在跳转的页面中获取数据
export default class RentAdd extends Component {
  constructor(props) {
    super(props)
    console.log(props)
  }
}

路由问题(withRouter 使用)

js
// 这是Map引入的公共组件,该组件中不能直接使用props.history.go(-1)
import React from 'react'
import { NavBar } from 'antd-mobile'
import './index.scss'
export default function NavHeader(props) {
  // 默认情况下只有路由 Route 直接渲染的组件才能获取到路由信息(如:props.history.go())
  return (
    <NavBar
      className="navbar"
      mode="light"
      icon={<i className="iconfont icon-back" />}
      onLeftClick={() => props.history.go(-1)}
    >
      {props.title}
    </NavBar>
  )
}
js
// 解决方案
import React from 'react'
import { NavBar } from 'antd-mobile'
import './index.scss'
//////////////////////////////////////////////////////////////////////////
// 导入 withRouter 高阶组件
import { withRouter } from 'react-router-dom'
//////////////////////////////////////////////////////////////////////////
function NavHeader(props) {
  // 默认情况下只有路由 Route 直接渲染的组件才能获取到路由信息(如:props.history.go())
  return (
    <NavBar
      className="navbar"
      mode="light"
      icon={<i className="iconfont icon-back" />}
      onLeftClick={() => props.history.go(-1)}
    >
      {props.title}
    </NavBar>
  )
}
//////////////////////////////////////////////////////////////////////////
// 使用 withRouter 高阶组件包裹该组件就可以使用了
export default withRouter(NavHeader)
//////////////////////////////////////////////////////////////////////////

路由钩子

js
// 当很多组件需要使用时可以引入路由函数使用
import React from 'react'
import styles from './Header.module.css'
import logo from '../../assets/logo.svg'
import { Layout, Typography, Input, Menu, Button, Dropdown } from 'antd'
import { GlobalOutlined } from '@ant-design/icons'
import {
  useHistory,
  useLocation,
  useParams,
  useRouteMatch
} from 'react-router-dom'
export const Header: React.FC = () => {
  const history = useHistory() // 可以取得 history 路由数据
  const location = useLocation() // 可以取得 location 路由数据
  const params = useParams() // 可以取得路由参数
  const match = useRouteMatch() // 可以取得 url 匹配的数据
  return (
    <div className={styles['app-header']}>
      <div className={styles['top-header']}>
        <div className={styles['inner']}>
          <Button.Group className={styles['button-group']}>
            <Button
              onClick={() => {
                // 直接使用
                history.push('register')
              }}
            >
              注册
            </Button>
            <Button
              onClick={() => {
                // 直接使用
                history.push('signIn')
              }}
            >
              登录
            </Button>
          </Button.Group>
        </div>
      </div>
    </div>
  )
}

基本使用(v6 版本)

js
// v6 版本中无需再配置精准匹配
import { BrowserRouter, Routes, Route, Link, Navigate } from 'react-router-dom'
function App() {
  return (
    <BrowserRouter>
      <div className="App">
        <Link to="/about">about页面</Link>
        <Link to="/about2">about2页面</Link>
        {/* V6 版本移除了 Switch 组件取而代之的是 Routes 组件,且必须使用该组件否则报错*/}
        <Routes>
          {/* caseSensitive 属性表示区分路径大小写 */}
          <Route path="/about" caseSensitive element={<About />}>
            {/* index 属性表示这个路由是默认路由 */}
            <Route index element={<About2 />} />
          </Route>
          <Route path="/about2" element={<About2 />} />
          {/* 移除 Redirect 组件,V6 版本使用 Navigate 组件实现路由重定向,可以设置属性 replace 表示跳转模式,默认是 push */}
          <Route path="/" element={<Navigate to="/about" replace={true} />} />
          <Route path="*" element={<About2 />} />
        </Routes>
      </div>
    </BrowserRouter>
  )
}
function About() {
  return <p>About组件</p>
}
function About2() {
  return <p>About2组件</p>
}

useRoutes 路由表

未使用 useRoutes

js
// 未使用 useRoutes
function App() {
  return (
    <BrowserRouter>
      <div className="App">
        <Link to="/about">about页面</Link>
        <Link to="/about2">about2页面</Link>
        <Routes>
          <Route path="/about" element={<About />} />
          <Route path="/about2" element={<About2 />} />
          <Route path="/" element={<Navigate to="/about" />} />
          <Route path="*" element={<div>404</div>} />
        </Routes>
      </div>
    </BrowserRouter>
  )
}

使用 useRoutes

js
import { Link, Navigate, useRoutes } from 'react-router-dom'
function App() {
  // V6 版本新增 useRoutes 路由表功能
  // 借助 useRoutes 函数能快速生成路由
  const Element = useRoutes([
    {
      path: '/about',
      element: <About />
    },
    {
      path: '/about2',
      element: <About2 />
    },
    {
      path: '/',
      element: <Navigate to="/about" />
    },
    {
      path: '*',
      element: <div>404</div>
    }
  ])

  return (
    <div className="App">
      <Link to="/about">about页面</Link>
      <Link to="/about2">about2页面</Link>
      {Element}
    </div>
  )
}
function About() {
  return <p>About组件</p>
}
function About2() {
  return <p>About2组件</p>
}

注意:使用上面写法时必须改造 index.js 将 BrowserRouter 组件直接包裹 App 跟组件

js
import React from 'react'
import App from './App'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
)

嵌套路由

js
import { Link, Navigate, useRoutes, Outlet } from 'react-router-dom'
function App() {
  const Element = useRoutes([
    {
      path: '/about',
      element: <About />
    },
    {
      path: '/about2',
      element: <About2 />,
      // 使用 children 表示下级路由
      children: [
        {
          // 使用路由表的形式二级路由不需要像传统写法那样必须带上一级路由如此处按传统写法路径应为:/about2/AboutSum
          // index 表示二级默认路由
          index: true,
          element: <AboutSum />
        },
        {
          // 这里不能在路径前加 /
          path: 'aboutsum2',
          element: <AboutSum2 />
        }
      ]
    },
    {
      path: '/',
      element: <Navigate to="/about" />
    }
  ])

  return (
    <div className="App">
      <Link to="/about">about页面</Link>
      <Link to="/about2">about2页面</Link>
      {Element}
    </div>
  )
}
function About() {
  return <p>About组件</p>
}
function About2() {
  return (
    <>
      <p>About2组件</p>
      {/* 使用 Outlet 组件指定下级路由呈现的位置 */}
      <Outlet />
    </>
  )
}
function AboutSum() {
  return <p>AboutSum组件</p>
}
function AboutSum2() {
  return <p>AboutSum2组件</p>
}

路由传参

params 参数

js
// 引入 useParams 钩子
import { Link, Navigate, useRoutes, useParams } from 'react-router-dom'
function App() {
  const Element = useRoutes([
    {
      // 需要先声明接收 params 参数
      path: '/about2/:id',
      element: <About2 />
    },
    {
      path: '/',
      element: <Navigate to="/about" />
    }
  ])

  return (
    <div className="App">
      {/* 传递参数 */}
      <Link to="/about2/2">about2页面</Link>
      {Element}
    </div>
  )
}
function About2() {
  // 获取路由 Params 参数
  const a = useParams()
  console.log(a) // {id: '2'}
  return <p>About2组件</p>
}

search 参数

js
import { Link, Navigate, useRoutes, useSearchParams } from 'react-router-dom'
function App() {
  const Element = useRoutes([
    {
      path: '/about2',
      element: <About2 />
    },
    {
      path: '/',
      element: <Navigate to="/about" />
    }
  ])

  return (
    <div className="App">
      <Link to="/about">about页面</Link>
      {/* 传递 search 参数 */}
      <Link to="/about2?id=2&name=name">about2页面</Link>
      {Element}
    </div>
  )
}
function About2() {
  // 使用 useSearchParams 获取参数,setSearch 用于修改参数
  const [search, setSearch] = useSearchParams()
  console.log(search.get('id')) // 获取 id
  console.log(search.get('name')) // 获取 name
  return (
    <>
      <p>About2组件</p>
      <button onClick={() => setSearch('id=3&name=name2')}>更新参数</button>
    </>
  )
}

state 参数

js
import { Link, Navigate, useRoutes, useLocation } from 'react-router-dom'
function App() {
  const Element = useRoutes([
    {
      path: '/about2',
      element: <About2 />
    },
    {
      path: '/',
      element: <Navigate to="/about" />
    }
  ])

  return (
    <div className="App">
      <Link to="/about">about页面</Link>
      {/* 传递 state 参数 */}
      <Link to="/about2" state={{ id: 1, name: 'name' }}>
        about2页面
      </Link>
      {Element}
    </div>
  )
}
function About2() {
  // 获取 state 参数
  const a = useLocation()
  console.log(a)
  return <p>About2组件</p>
}

编程式导航

js
import { Navigate, useRoutes, useNavigate, useLocation } from 'react-router-dom'
function App() {
  // 使用 useNavigate 钩子
  const navigate = useNavigate()
  const Element = useRoutes([
    {
      path: '/about2',
      element: <About2 />
    },
    {
      path: '/',
      element: <Navigate to="/about" />
    }
  ])

  return (
    <div className="App">
      <button onClick={() => navigate(1)}>前进</button>
      <button onClick={() => navigate(-1)}>后退</button>
      {/* 使用 useNavigate 跳转*/}
      <button onClick={() => navigate('/about')}>about页面</button>
      {/* 传递参数 */}
      <button
        onClick={() =>
          navigate('/about2', {
            replace: false,
            state: {
              id: 2,
              name: 'name'
            }
          })
        }
      >
        about2页面
      </button>
      {Element}
    </div>
  )
}
function About2() {
  // 获取路由参数
  const a = useLocation()
  console.log(a)
  return <p>About2组件</p>
}

useNavigationType

js
// useNavigationType 钩子可以返回当前导航类型(用户是如何来到当前页面的)
import {
  Navigate,
  useRoutes,
  useNavigate,
  useNavigationType
} from 'react-router-dom'
function App() {
  const navigate = useNavigate()
  const Element = useRoutes([
    {
      path: '/about',
      element: <About />
    },
    {
      path: '/about2',
      element: <About2 />
    },
    {
      path: '/',
      element: <Navigate to="/about" />
    }
  ])

  return (
    <div className="App">
      <button onClick={() => navigate('/about')}>about页面</button>
      <button onClick={() => navigate('/about2')}>about2页面</button>
      {Element}
    </div>
  )
}
function About2() {
  // 返回值:POP(在浏览器直接打开了这个路由)、PUSH、REPLACE
  const a = useNavigationType()
  console.log(a)
  return <p>About2组件</p>
}

useResolvedPath

js
import { useResolvedPath } from 'react-router-dom'
function About2() {
  // useResolvedPath 可以用来解析地址信息
  console.log(useResolvedPath('/user?id=001&name=tom#qwe'))
  return <p>About2组件</p>
}

路由懒加载

js
import { lazy, Suspense } from 'react'
// 使用 lazy 函数懒加载组件
const Text = lazy(() => import('./pages/text'))
function App() {
  const Element = useRoutes([
    {
      path: '/text',
      element: <Text />
    }
  ])

  return (
    <div className="App">
      {/* 使用 Suspense 实现 loading 效果 */}
      <Suspense fallback={<>加载中...</>}>{Element}</Suspense>
    </div>
  )
}

react-Hooks

js
// 利用 hooks 可以让函数组件也拥有类组件的能力

useState

js
// 引入 useState 给函数组件提供状态
import { useState } from 'react'
function App() {
  // 第一个表示 state 数据,第二个表示修改状态的函数(名称可自定义约定以 set 开头),useState(0) 括号中的值表示该条 state 的初始值
  // 使用函数修改状态时和类组件的 setstate 类似都是异步的
  const [count, setCount] = useState < number > 0
  return (
    <div className="App">
      {/* 使用 state */}
      <p>数值:{count}</p>
      {/* 修改 state */}
      <button onClick={() => setCount(count + 1)}>加一</button>
    </div>
  )
}
export default App

useEffect

组件更新和初始化

js
// 引入 useEffect 给函数组件提供生命周期
import { useState, useEffect } from 'react'

function App() {
  const [count, setCount] = useState < number > 0
  //////////////////////////////////////////////////////////////////////
  // 不传递第二个参数时每次页面渲染或者是状态改变(组件卸载不会执行)时 useEffect 钩子都会执行
  // 注意:在这种写法下不要直接调用函数修改状态,否则则会无限循环执行 useEffect 钩子
  // 这种写法类似于类组件中的 componentDidUpdate 钩子
  useEffect(() => {})
  //////////////////////////////////////////////////////////////////////
  return (
    <div className="App">
      {/* 使用 state */}
      <p>数值:{count}</p>
      {/* 修改 state */}
      <button onClick={() => setCount(count + 1)}>加一</button>
    </div>
  )
}

export default App

多次执行问题(组件更新)

js
function App() {
  const [count, setCount] = useState<number>(0);
  const [count2, setCount2] = useState<number>(0);
  // 当传递一个空数组时表示仅在组件渲染完成的时候执行一次(注意:当开启严格模式(React.StrictMode)时页面刷新会执行两次,这是由于在 react18 中会对每个组件进行两次渲染以便观察一些意想不到的结果)类似于 componentDidMount
  useEffect(() => {
    console.log(100);
  }, []);
  return (
    <div className="App">
      {/* 使用 state */}
      <p>数值1:{count}</p>
      <p>数值2:{count2}</p>
      {/* 修改 state */}
      <button
        onClick={() => {
          setCount(count + 1);
          setCount2(count2 + 1);
        }}
      >
        加一
      </button>
    </div>
  );
}

// 当传递一个空数组时且开启严格模式(React.StrictMode)时页面刷新会执行两次
import React from "react";
import ReactDOM, { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

 const root = ReactDOM.createRoot(
   document.getElementById('root') as HTMLElement
 );
 root.render(
   <React.StrictMode>
    <App />
   </React.StrictMode>
 );
 reportWebVitals();

// 改为如下写法可解决(不使用严格模式)
import React from "react";
import ReactDOM, { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

createRoot(document.getElementById("root") as HTMLElement).render(<App />);
js
function App() {
  const [count, setCount] = useState < number > 0
  const [count2, setCount2] = useState < number > 0
  // 当第二个参数传递的是某一个状态时,只有这个状态变化时钩子才会执行
  // 还可以传递多个状态,只要传递进来的状态有一个发生变化就会执行
  useEffect(() => {
    console.log(100)
  }, [count])
  return (
    <div className="App">
      {/* 使用 state */}
      <p>数值1:{count}</p>
      <p>数值2:{count2}</p>
      {/* 修改 state */}
      <button
        onClick={() => {
          setCount(count + 1)
          setCount2(count2 + 1)
        }}
      >
        加一
      </button>
    </div>
  )
}
export default App

组件卸载

js
function App() {
  const [count, setCount] = useState < number > 0
  return <div className="App">{count > 2 ? '' : <Zi />}</div>
}
// 这种写法表示只在组件卸载时执行
function Zi() {
  useEffect(() => {
    console.log('组件挂载') // 这里的代码会在页面挂载时执行一次
    return () => {
      console.log('组件卸载') // 这里的代码会在页面卸载时执行一次
    }
  }, [])
  return <p>子组件</p>
}
export default App

useContext

js
// useContext 可以实现父组件与后代组件的通信
import { useState, useEffect, useContext, createContext } from 'react'
// createContext 需要传递一个默认值表示传递给后代组件的默认值
const Context = createContext(0)
function App() {
  const [count, setCount] = useState < number > 0
  return (
    <div className="App">
      {/* 通过 Context.Provider 组件传递数据 */}
      {/* 传递多个数据 */}
      {/* 
      	  <Context.Provider value={{count,setCount}}>
        	<Zi />
      	  </Context.Provider>
       */}
      <Context.Provider value={count}>
        <Zi />
      </Context.Provider>
      <button
        onClick={() => {
          setCount(count + 1)
        }}
      >
        加一
      </button>
    </div>
  )
}
function Zi() {
  return (
    <>
      <p>子组件</p>
      <Sun />
    </>
  )
}
function Sun() {
  // 子组件通过 useContext 钩子获取数据
  const value = useContext(Context)
  // 接收参数和修改数据的方法
  // const {count,setCount} = useContext(Context);
  return <p>孙组件:{value}</p>
}
export default App

useRef

useRef 用于获取 DOM 元素

js
import { useRef } from 'react'
export const TodoAdd = () => {
  const input = useRef(null)
  function enter() {
    // 获取文本框内容
    if (input.current.value) {
      // 需要注意的是,修改 Ref 对象中 current 的值并不会引发组件的重新渲染
      input.current.value = ''
    }
  }
  return (
    <>
      <input type="text" ref={input} />
      <button onClick={enter}>添加</button>
    </>
  )
}

useRef 用于数据持久化

js
import { useRef, useEffect, useState } from 'react'
export function App() {
  // 这里如果使用 let 保存定时器 id,由于 setCount 会使组件重新渲染,这时每次更新都重新 let 一个变量,所以在进行解绑操作的时候,let 变量为 初始值 null
  // let timerId = null
  // useRef 不会因组件的更新而丢失数据,所以可以使用它来保存定时器 id
  // 就是利用了 useRef 创建的对象在组件更新期间引用地址保持不变
  const timerId = useRef()
  const [count, setCount] = useState(0)
  useEffect(() => {
    timerId.current = setInterval(() => {
      console.log('执行')
      setCount(count + 1)
    }, 1000)
  }, [])
  const stop = () => {
    clearInterval(timerId.current)
  }
  return (
    <div>
      <div>{count}</div>
      <button onClick={stop}>停止</button>
    </div>
  )
}

useReducer

useReducer 和 useState 非常类似,当组件的状态逻辑复杂或者下一个状态取决于上一个状态时,推荐使用 useReducer。

useReducer 可以将组件内部的逻辑处理抽离到组件外部,减轻组件自身负担

js
import { useState, useReducer } from 'react'
// 处理 useReducer 对应的状态逻辑函数,state 表示当前 useReducer 的状态值,action 表示执行何种操作(是一个带有 type 的对象)
const reducer = (state, action) => {
  switch (action.type) {
    case 'add':
      return state + action.payload
    case 'sub':
      return state - action.payload
    default:
      return state
  }
}
function App() {
  // useReducer 第一个参数是一个表示处理状态逻辑的函数,根据当前状态计算出新的状态并返回,第二个参数表示状态初始值
  const [state, dispatch] = useReducer(reducer, 0)
  return (
    <div className="App">
      <p>{state}</p>
      {/* 修改状态 */}
      <button onClick={() => dispatch({ type: 'add', payload: 1 })}>+1</button>
      <button onClick={() => dispatch({ type: 'sub', payload: 1 })}>-1</button>
    </div>
  )
}
export default App

useMemo

只要父组件状态更新,无论有没有对后代组件进行操作,后代组件都会重新渲染,这时 useMemo 可以避免这些不必要的渲染提升性能

js
// 引入 useMemo
import React, { memo, useMemo, useState } from 'react'
// useMemo 需要配合 memo 使用
const Heading = memo(({ style, title }) => {
  console.log('Rendered:', title)
  return <h1 style={style}>{title}</h1>
})

export default function App() {
  const [count, setCount] = useState(0)
  // 使用 useMemo 包裹需要监听的数据,只有在第二个参数数组内的数据发生变化 useMemo 才会重新计算,如果为空数组表示只在页面初始化时计算一次
  // 当 props 是引用数据类型时会记忆其引用地址,当依赖项无变化时返回的还是上一次的引用
  // useMemo 计算结果是 return 回来的值, 主要用于 缓存计算结果的值
  // useMemo 可以缓存任意数据(数值、对象、函数)
  const memoizedStyle = useMemo(() => {
    return {
      backgroundColor: 'red',
      color: 'white'
    }
  }, [])

  return (
    <>
      <button
        onClick={() => {
          setCount(count + 1)
        }}
      >
        加一
      </button>
      <Heading style={memoizedStyle} title="Memoized" />
    </>
  )
}

用 useMemo 缓存函数

用 useMemo 缓存组件

useCallback

useCallback 与 useMemo 极其类似,可以说是一模一样,唯一不同的是 useMemo 返回的是函数运行的结果,而 useCallback 返回的是函数

注意:这个函数是父组件传递子组件的一个函数,防止做无关的刷新,其次,这个组件必须配合 memo,否则不但不会提升性能,还有可能降低性能

js
// 引入 useCallback 和 memo
import { useState, memo, useCallback } from 'react'

// 用 memo 包裹子组件配合复用避免重新渲染
const Children = memo(({ click }) => {
  console.log('子组件更新了')
  return (
    <div>
      子组件:<button onClick={click}>点击</button>
    </div>
  )
})

function App() {
  console.log('组件更新')
  const [count, setCount] = useState(0)
  // useCallback 接收两个参数,第一个是一个函数,第二个表示依赖项
  // 只有在依赖项发生变化时,useCallback 所记录引用地址才会发生变化,空数组表示只在页面初始化变化一次
  // useCallback 计算结果是函数, 主要用于缓存函数
  const hand = useCallback(() => {
    console.log('点击子组件按钮')
  }, [])

  return (
    <div className="App">
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>加一</button>
      <Children click={hand} />
    </div>
  )
}

memo

js
// memo 函数用于检测组件 props 的变化,当 props 值发生变化时才会重新渲染组件,类似与类组件的纯组件
// memo 出现在 hooks 之前,所以 memo 只能检测 props 的变化对于 useState 等 hook 还是会重新渲染
// memo 比较 props 时是浅对比,就是说如果 props 是引用数据类型时只会简单的对比变化前后的引用地址
import { useEffect, useState, memo } from 'react'
// 使用 memo 函数包裹组件
const A = memo(() => {
  console.log('a渲染了')
  return <p>xxx</p>
})
export const TodoFooter = ({ setTodos, list }) => {
  console.log('子组件渲染')
  const [sum, setsum] = useState(0)
  useEffect(() => {
    let arr = 0
    list.forEach(item => {
      if (item.done) {
        arr++
      }
    })
    setsum(arr)
  }, [list])
  return (
    <div>
      已完成{sum}项<button onClick={() => setTodos([])}>清空</button>
      <A />
    </div>
  )
}

第二个参数

js
const A = memo(
  () => {
    console.log('a渲染了')
    return <p>xxx</p>
  },
  (prevProps, nextProps) => {
    // memo 可以传递第二个参数是一个函数,prevProps 表示上一次的 props,nextProps 表示最新的 props
    // 此函数必须返回一个布尔值,为 true 时组件不会重新渲染,反之则重新渲染
    return prevProps === nextProps
  }
)

hooks 进阶

父组件更新导致子组件更新,如果这时子组件状态未改变其实无 DOM 操作,只会进行 diff 算法

Redux

js
// Redux 不区分框架,可以提供全局状态管理
// npm i redux 安装 redux
// 可以只安装 redux 就可以完成全局状态管理
// npm i react-redux 安装 react-redux  react-redux 是 react 团队出品
// 如果一个组件依赖 redux 里的数据,每当 redux 里的数据发生变化,其后代组件即使无任何依赖外部(redux、父组件)的数据也会重新渲染

创建全局状态

在 redux 文件夹下创建 store.ts 提供全局状态

typescript
// 在 redux 文件夹下创建 store.ts 提供全局状态
import { createStore } from 'redux'
import languageReducer from './languageReducer'

const store = createStore(languageReducer)
export default store

提供数据源

在 redux 文件夹下创建 languageReducer.ts 作为数据源

typescript
// 在 redux 文件夹下创建 languageReducer.ts 作为数据源
interface LanguageState {
  language: 'en' | 'zh'
  // 表示 languageList 是一个由对象组成的列表
  languageList: { name: string; code: string }[]
}
const defaultState: LanguageState = {
  language: 'zh',
  languageList: [
    { name: '中文', code: 'zh' },
    { name: 'English', code: 'en' }
  ]
}
export default (state = defaultState, action) => {
  return state
}

使用状态

在类组件中使用数据

js
// 引入全局状态
import store from '../../redux/store'

// 新建 State 接口约束 class 组件的 state
interface State {
  language: 'en' | 'zh';
  languageList: { name: string, code: string }[];
}
// 将自定义接口传递给 React.Component 泛型的第二个参数就可以正常约束状态
class HeaderComponent extends React.Component<RouteComponentProps, State> {
  constructor(props) {
    super(props)
    const storeState = store.getState()
    /////////////////////////////////////////////////////////////
    this.state = {
      language: storeState.language,
      languageList: storeState.languageList
    }
    /////////////////////////////////////////////////////////////
  }
  render() {
    return (
      <div className={styles['app-header']}>
        <div className={styles['top-header']}>
          <div className={styles['inner']}>
            <Typography.Text>让旅游更幸福</Typography.Text>
            <Dropdown.Button
              style={{ marginLeft: 15 }}
              overlay={
                <Menu>
                  {this.state.languageList.map(l => (
                    <Menu.Item key={l.code}>{l.name}</Menu.Item>
                  ))}
                </Menu>
              }
              icon={<GlobalOutlined />}
            >
              {this.state.language === 'zh' ? '中文' : 'English'}
            </Dropdown.Button>
          </div>
        </div>
      </div>
    )
  }
}

在函数组件中使用数据

js
// 引入 useSelector 函数使用全局状态
import { useSelector } from "react-redux";
// 引入定义全局 state 接口
import { RootState } from "../../redux/store";

export const Header: React.FC = () => {
  ///////////////////////// 应用自定义全局状态约束接口
// redux 本来就是解决组件强耦合的问题,现在使用 useSelector 函数必须指定 state 的类型,这无疑让组件之间耦合性变强
  const language = useSelector((state: RootState) => state.language);
  //////////////////////////////////////////////////////
  return ();
};

注意

js
// 注意:函数组件使用全局状态时必须在最外层用 Provider 组件包裹(哪儿用哪儿包裹)
// 建议直接在 app 根组件中直接包裹
import { Provider } from 'react-redux'
import store from './redux/store'
function App() {
  return (
    <Provider store={store}>
      <div className={styles.App}>
        <BrowserRouter>
          <Route path="/" exact component={Home} />
        </BrowserRouter>
      </div>
    </Provider>
  )
}
export default App

也可以直接写在 index.tsx 中

js
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import { Provider } from "react-redux";
import store from "./redux/store";

createRoot(document.getElementById("root") as HTMLElement).render(
  <Provider store={store}>
    <App />
  </Provider>
);

全局状态类型强耦合问题

js
// redux 本来就是解决组件强耦合的问题,现在每次在组件中使用 useSelector 函数必须指定 state 的类型,这无疑让组件之间耦合性变强,最终导致组件无法复用
typescript
// 解决方案
// 在 redux 文件夹下新建 hooks.ts
import {
  useSelector as useReduxSelector,
  TypedUseSelectorHook
} from 'react-redux'
// 引入 state 类型接口
import { RootState } from './store'
export const useSelector: TypedUseSelectorHook<RootState> = useReduxSelector
typescript
// store 文件导出的接口
export interface RootState {
  language: 'en' | 'zh'
  // 表示 languageList 是一个由对象组成的列表
  languageList: { name: string; code: string }[]
}

在组件中引入 hooks.ts

js
import {useSelector} from '../../redux/hooks'

export const Header: React.FC = () => {
// 这样再使用 useSelector 函数就不用指定 state 的类型了
  const language = useSelector((state) => state.language);
  //////////////////////////////////////////////////////
  return ();
};

修改状态

js
// hook.ts 和 store.ts 无变化和上面的代码一样

languageReducer.ts

typescript
// 简单改造 languageReducer.ts 文件
interface LanguageState {
  language: 'en' | 'zh'
  // 表示 languageList 是一个由对象组成的列表
  languageList: { name: string; code: string }[]
}
const defaultState: LanguageState = {
  language: 'zh',
  languageList: [
    { name: '中文', code: 'zh' },
    { name: 'English', code: 'en' }
  ]
}
//////////////////////////////////////////////////////////////////////////////
export default (state = defaultState, action) => {
  switch (action.type) {
    case 'change_language':
      return { ...state, language: action.payload }
    default:
      return state
  }
}

在组件中使用

js
import { useSelector } from '../../redux/hooks'
// 引入 store 可以使用 dispatch 修改状态
import store from '../../redux/store'
// 还可以引入 useDispatch 函数修改全局状态
import { useDispatch } from 'react-redux'
export const Header: React.FC = () => {
  const language = useSelector(state => state.language)

  // const language = store.getState().language; 可以通过 store.getState() 获取状态但想获取实时更新的数据必须每次都执行依次这个函数

  const languageList = useSelector(state => state.languageList)
  // 可以使用 useDispatch() 方法修改全局状态
  const dispatch = useDispatch()

  // 切换语言
  const menuClickHandler = e => {
    // 调用 react-redux 提供的钩子修改状态
    dispatch({ type: 'change_language', payload: e.key })
    // 调用 redux 提供的 dispatch 方法修改状态
    // store.dispatch({ type: "change_language", payload: e.key });
  }

  return (
    <div className={styles['app-header']}>
      {language === 'zh' ? '中文' : 'English'}
    </div>
  )
}

监听状态

js
import store from "../../redux/store";
export const Header: React.FC = () => {
  const menuClickHandler = (e) => {
    store.dispatch({ type: "change_language", payload: e.key });
  };
  // 只要调用 store.dispatch 或者 useDispatch(react-redux 提供)方法时 subscribe 函数就会被触发
  store.subscribe(() => {
    console.log("数据更新了");
  });
  return (
    <div className={styles["app-header"]}>
      <div className={styles["top-header"]}>
        <div className={styles["inner"]}>
          <Dropdown.Button
            style={{ marginLeft: 15 }}
            overlay={
              <Menu onClick={menuClickHandler}>
                {languageList.map((l) => (
                  <Menu.Item key={l.code}>{l.name}</Menu.Item>
                ))}
              </Menu>}>
          </Dropdown.Button>
    </div>
  );
};

问题

js
// 这样直接使用会导致执行一次 dispatch , subscribe 函数会触发多次的问题
import React, { useEffect } from "react";
import store from "../../redux/store";
export const Header: React.FC = () => {
    ////////////////////////////////////////////////////////////////////
    // 这样使用完美解决问题
    useEffect(() => {
        // store.subscribe 方法会有一个返回值是一个函数,用于取消监听
        // 在页面挂载时监听状态,保证 store.subscribe 只执行一次
        let unsubscribe = store.subscribe(() => {
            console.log("数据更新了");
        });
        return () => {
            // 在页面卸载时取消监听
            unsubscribe();
        };
    }, []);
    ////////////////////////////////////////////////////////////////////
  return ();
};

模块化

创建 language 文件夹和 recommendProductsState 表示这是两个模块

创建 languageActions.ts 文件封装 action 函数

typescript
// languageActions.ts

// 创建 action type 从源头避免写错导致排错麻烦
export const CHANGE_LANGUAGE = 'change_language'
// 创建 action 工厂函数保证在调用 dispatch 传递的参数正确
export const changeLanguage = (language: 'zh' | 'en') => {
  return { type: CHANGE_LANGUAGE, payload: language }
}

将上述数据源 languageReducer.ts 文件放入其中并替换 type

typescript
import { CHANGE_LANGUAGE } from './languageActions'
interface LanguageState {
  language: 'en' | 'zh'
  // 表示 languageList 是一个由对象组成的列表
  languageList: { name: string; code: string }[]
}
const defaultState: LanguageState = {
  language: 'zh',
  languageList: [
    { name: '中文', code: 'zh' },
    { name: 'English', code: 'en' }
  ]
}
export default (state = defaultState, action) => {
  switch (action.type) {
    // 替换为变量避免出错
    case CHANGE_LANGUAGE:
      return { ...state, language: action.payload }
    default:
      return state
  }
}

创建 recommendProductsState.ts 文件

typescript
// 创建 recommendProductsState.ts 表示第二个 redux 模块,避免所有数据全写在一个文件中
interface RecommendProductsState {
  productList: any[]
  loading: boolean
  error: string | null
}
const defaultState: RecommendProductsState = {
  productList: [],
  loading: true,
  error: null
}
export default (state = defaultState, action) => {
  return state
}

修改 store.ts 文件使之支持模块化

typescript
// 引入 combineReducers 函数用于模块化
import { createStore, combineReducers } from 'redux'

import languageReducer from './language/languageReducer'
import recommendProductsState from './recommendProductsState/recommendProductsState'

// 用 combineReducers 函数将多个模块合并
const rootReducer = combineReducers({
  // 这里的键名就是模块名
  language: languageReducer,
  recommendProducts: recommendProductsState
})
// 定义 RootState 类型
export type RootState = ReturnType<typeof store.getState>

// 最后将这个结合体传入 createStore
const store = createStore(rootReducer)
export default store

在组件中使用封装好的 action 函数

js
// 引入模块化 action 文件
import { changeLanguage } from '../../redux/language/languageActions'
// 引入 store 可以使用 dispatch 修改状态
import store from '../../redux/store'
export const Header: React.FC = () => {
  /////////////////////////////////////////////////////////////////////////////
  const menuClickHandler = e => {
    store.dispatch(changeLanguage(e.key))
  }

  return (
    <div className={styles['app-header']}>
      {language === 'zh' ? '中文' : 'English'}
    </div>
  )
}

在组件中使用模块化后的数据

js
import { useSelector } from "../../redux/hooks";
export const Header: React.FC = () => {
  // 这里使用的时候需要区分模块,所以在 state 后加上模块名(格式:state.模块名.该模块下的数据名)
  const language = useSelector((state) => state.language.language);
  const languageList = useSelector((state) => state.language.languageList);
  return ()
};

中间件

js
// 中间件会在 dispatch 之前执行
// redux-thunk 能让全局状态支持函数类型,从而支持在全局状态中调用 api 的能力
// npm i redux-thunk 安装中间件

改造 store.ts 文件

typescript
// 引入 applyMiddleware
import { createStore, combineReducers, applyMiddleware } from 'redux'

import languageReducer from './language/languageReducer'
import recommendProductsState from './recommendProductsState/recommendProductsState'

// 引入 redux-thunk
import thunk from 'redux-thunk'

const rootReducer = combineReducers({
  language: languageReducer,
  recommendProducts: recommendProductsState
})
export interface RootState {
  language: 'en' | 'zh'
  languageList: { name: string; code: string }[]
}

// 将 applyMiddleware 传入 createStore 第二个参数中
const store = createStore(rootReducer, applyMiddleware(thunk))
export default store

添加异步函数中间件 giveData

typescript
export const changeerror = (err: boolean) => {
  return { type: CHANGE_ERROR, payload: err }
}
// 这里参数 dispatch 表示派发函数,getState 函数可以取得全局状态
export const giveData = () => (dispatch, getState) => {
  setTimeout(() => {
    dispatch(changeerror(!getState().recommendProducts.error))
  }, 1000)
}

在组件中使用

js
// 引入中间件
import { giveData } from "../../redux/recommendProductsState/recommendProductsAction";
import store from "../../redux/store";
export const Header: React.FC = () => {
  const menuClickHandler = (e) => {
    // 调用中间件
    store.dispatch(giveData());
  };
  return ();
};

自定义中间件

js
// 自定义中间件可以统一拦截所有的 action
// 自定义中间件公式
const middleware = store => next => action => {}

新建 middleware 文件夹作为中间件目录

新建 actionLog.ts 写入中间件

typescript
// 引入自定义中间类型
import { Middleware } from 'redux'
export const actionLog: Middleware = store => next => action => {
  console.log('state 当前', store.getState())
  console.log('被拦截的 action 对象', action)
  // next 函数用于分发 action 只有被分发的 action 才会执行
  next(action)
  console.log('更新后的 state', store.getState())
}

在 store.ts 中引入并使用中间件

typescript
import { createStore, combineReducers, applyMiddleware } from 'redux'
import languageReducer from './language/languageReducer'
import recommendProductsState from './recommendProductsState/recommendProductsState'

// 引入自定义中间件
import { actionLog } from './middleware/actionLog'

import thunk from 'redux-thunk'
const rootReducer = combineReducers({
  language: languageReducer,
  recommendProducts: recommendProductsState
})
export type RootState = ReturnType<typeof store.getState>

// 将自定义中间件传入 applyMiddleware 的第二个参数
const store = createStore(rootReducer, applyMiddleware(thunk, actionLog))
export default store
js
// 这样就能拦截页面上所有的 action

添加多个中间件

在 middleware 文件夹下创建 actionLog.ts 和 actionLog2.ts

actionLog.ts

typescript
import { Middleware } from 'redux'
export const actionLog: Middleware = store => next => action => {
  console.log('中间件1执行', store.getState())
  next(action)
}

actionLog2.ts

typescript
import { Middleware } from 'redux'
export const actionLog2: Middleware = store => next => action => {
  console.log('中间件2执行', store.getState())
  next(action)
}

改造 store.ts

typescript
// 引入自定义中间件
import { actionLog } from './middleware/actionLog'
import { actionLog2 } from './middleware/actionLog2'

// 将这些中间件依次传入 applyMiddleware 函数中,这里传入的顺序对应中间件执行的顺序
const store = createStore(
  rootReducer,
  applyMiddleware(thunk, actionLog2, actionLog)
)
export default store

react-redux

js
// npm i react-redux 安装 react-redux

redux-toolkit(RTK)

js
// 由于 redux 的各种规范与插件异常繁琐,所以官方推出了 redux-toolkit 这个库来简化 redux 的使用
// https://redux-toolkit.js.org/ 官方文档
// npm install @reduxjs/toolkit 安装依赖
// npm install react-redux RTK 需要配合 react-redux使用
// redux-toolkit内置了thunk插件,不再需要单独安装,可以直接处理异步的action。
// 可参考 https://zhuanlan.zhihu.com/p/382487951

创建 action

新建 productDetail 文件夹

slice.ts

typescript
// 引入 createSlice 函数
import { createSlice } from '@reduxjs/toolkit'
interface ProductDetailState {
  loading: boolean
  error: string | null
  product: any
}
// 定义默认值
const initialState: ProductDetailState = {
  loading: true,
  error: null,
  product: null
}
export const productDetailState = createSlice({
  name: 'productDetailState', // name 表示命名空间
  initialState, // initialState 初始数据
  // toolkit 是把 action 和 reducers 捆绑在一起了,所以不需要单独创建 action
  reducers: {
    // 这里定义的就是 一个个的 action
    fetchStart: state => {
      state.loading = true // toolkit 可以直接修改状态里的某一个值而不用像 redux 中必须返回一个修改后的 state
    },
    fetchSuccess: (state, action) => {
      state.product = action.payload
      state.loading = false
      state.error = null
    }
  }
})

改造 store

typescript
import { createStore, applyMiddleware } from 'redux'
import languageReducer from './language/languageReducer'
import recommendProductsState from './recommendProductsState/recommendProductsState'

// 从 RTK 中引入 combineReducers 函数
import { combineReducers } from '@reduxjs/toolkit'
// 引入 slice.ts 文件
import { productDetailState } from './productDetail/slice'

import thunk from 'redux-thunk'

// 用 combineReducers 函数将多个模块合并
const rootReducer = combineReducers({
  language: languageReducer,
  recommendProducts: recommendProductsState,
  // 传入模块
  productDetail: productDetailState.reducer
})

export type RootState = ReturnType<typeof store.getState>
const store = createStore(rootReducer, applyMiddleware(thunk))
export default store

使用

js
import { FC, useEffect, useState } from "react";
// 引入 productDetailState 函数
import { productDetailState } from "../../redux/productDetail/slice";
// 引入 useSelector 获取状态
import { useSelector } from "../../redux/hooks";
// 引入 useDispatch 调用 action
import { useDispatch } from "react-redux";

export const Detail: FC<RouteComponentProps<MatchParams>> = (props) => {
  // 获取状态
  const loading = useSelector((state) => state.productDetail.loading);
  const product = useSelector((state) => state.productDetail.product);
  const error = useSelector((state) => state.productDetail.error);
  // 获取 dispatch 函数
  const dispatch = useDispatch();

  useEffect(() => {
    // 调用 action
    dispatch(productDetailState.actions.fetchStart());
    setTimeout(() => {
      // 调用 action 并传递参数
      dispatch(
        productDetailState.actions.fetchSuccess({
          shortDescription:
            "这时描述这时描述这时描述这时描述这时描述这时描述这时描述这时描述这时描述这时描述这时描述这时描述这时描述这时描述",
          price: "5000",
          coupons: "巴拉巴拉巴拉",
          points: "巴拉巴拉巴拉",
          discount: "巴拉巴拉巴拉",
          rating: "3.5",
        })
      );
    }, 1000);
  }, []);
  return ();
};

处理异步请求

改造 store

typescript
import languageReducer from './language/languageReducer'
import recommendProductsState from './recommendProductsState/recommendProductsState'
// 从 RTK 中引入 configureStore 函数替代 redux 原生的 createStore 方法
import { combineReducers, configureStore } from '@reduxjs/toolkit'
// 引入 slice.ts 文件
import { productDetailState } from './productDetail/slice'

import { actionLog } from './middleware/actionLog'

const rootReducer = combineReducers({
  language: languageReducer,
  recommendProducts: recommendProductsState,
  productDetail: productDetailState.reducer
})

export type RootState = ReturnType<typeof store.getState>
// configureStore 函数参数接收一个对象,对象中由 reducer 、middleware(可选)、devTools(可选,布尔值)表示是否启用 RTK 浏览器插件
const store = configureStore({
  reducer: rootReducer,
  // 注意 RTK 会默认开启一个中间件,所以我们不能直接替换必须通过形参 getDefaultMiddleware 函数获取到所有的中间件再添加自定义中间件
  middleware: getDefaultMiddleware => [...getDefaultMiddleware(), actionLog]
})
export default store

在 slice.ts 中写入异步函数

typescript
// 引入 createAsyncThunk 函数
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
interface ProductDetailState {
  loading: boolean
  error: string | null
  product: any
}
const initialState: ProductDetailState = {
  loading: true,
  error: null,
  product: null
}
// 创建异步函数
export const getProductDetail = createAsyncThunk(
  'getProductDetail/productDetailState', // 表示命名空间
  // 第二个参数可以用来获取数据、分发事件等等,如 thunkAPI.dispatch、thunkAPI.getState
  async (touristRouteId: string, thunkAPI) => {
    setTimeout(() => {
      thunkAPI.dispatch(productDetailState.actions.fetchStart())
      thunkAPI.dispatch(
        productDetailState.actions.fetchSuccess({
          title: `这是标题${touristRouteId}`,
          shortDescription: '这时描述这时描述这',
          price: '5000',
          coupons: '巴拉巴拉巴拉',
          points: '巴拉巴拉巴拉',
          discount: '巴拉巴拉巴拉',
          rating: '3.5',
          pictures: ['巴拉巴拉巴拉', '巴拉巴拉巴拉', '巴拉巴拉巴拉']
        })
      )
    }, 1000)
  }
)

export const productDetailState = createSlice({
  name: 'productDetailState',
  initialState,
  reducers: {
    fetchStart: state => {
      state.loading = true
    },
    fetchSuccess: (state, action) => {
      state.product = action.payload
      state.loading = false
      state.error = null
    }
  }
})

在组件中使用

js
import { FC, useEffect, useState } from "react";
// 引入刚刚写好的异步函数 getProductDetail
import {
  productDetailState,
  getProductDetail,
} from "../../redux/productDetail/slice";
import { useSelector } from "../../redux/hooks";
import { useDispatch } from "react-redux";
import { RouteComponentProps, useParams } from "react-router-dom";
interface MatchParams {
  touristRouteId: string;
}
export const Detail: FC<RouteComponentProps<MatchParams>> = (props) => {
  const { touristRouteId } = useParams<MatchParams>();
  const dispatch = useDispatch();
  useEffect(() => {
    // 调用这个函数并传递参数
    dispatch(getProductDetail(touristRouteId));
  }, []);
  return ();
};

使用 createAsyncThunk 自动生成的 action

typescript
// 改造 slice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
interface ProductDetailState {
  loading: boolean
  error: string | null
  product: any
}
const initialState: ProductDetailState = {
  loading: true,
  error: null,
  product: null
}
export const getProductDetail = createAsyncThunk(
  'getProductDetail/productDetailState', // 表示命名空间
  // 实际上 createAsyncThunk 函数会返回 pendung、fulfilled、rejected 三种类型
  // createAsyncThunk 会自动生成 pendung、fulfilled、rejected 三个 action
  async (touristRouteId: string, thunkAPI) => {
    // 这里写入异步 api 调用并 return 数据
    return {
      title: `这是标题${touristRouteId}`,
      shortDescription: '这时描述这时描述这时描述这',
      price: '5000',
      coupons: '巴拉巴拉巴拉',
      points: '巴拉巴拉巴拉',
      discount: '巴拉巴拉巴拉',
      rating: '3.5',
      pictures: ['巴拉巴拉巴拉', '巴拉巴拉巴拉', '巴拉巴拉巴拉']
    }
  }
)

export const productDetailState = createSlice({
  name: 'productDetailState', // name 表示命名空间
  initialState, // initialState 初始数据
  reducers: {},
  // createAsyncThunk 自动生成的 pendung、fulfilled、rejected 三个 action 在 createSlice 函数中不能直接在 reducers 中处理,必须在 extraReducers 中
  extraReducers: {
    // 这是 createAsyncThunk 自动生成的 pending action
    [getProductDetail.pending.type]: state => {
      state.loading = true // toolkit 可以直接修改状态里的某一个值而不用像 redux 中必须返回一个修改后的 state
    },
    // 这是 createAsyncThunk 自动生成的 fulfilled action
    [getProductDetail.fulfilled.type]: (state, action) => {
      state.product = action.payload
      state.loading = false
      state.error = null
    },
    // 这是 createAsyncThunk 自动生成的 rejected action
    [getProductDetail.rejected.type]: (state, action) => {
      state.loading = false
      state.error = action.payload
    }
  }
})

最简单的用法

js
// npm i react-redux @reduxjs/toolkit 安装依赖

store.js

js
import { configureStore } from '@reduxjs/toolkit'
import { productDetailState } from './login/slice'
import { actionLog } from './middleware/actionlog'

export default configureStore({
  reducer: {
    // 有多个 slice 可以继续往后添加
    productDetail: productDetailState.reducer,
    // 使用中间件
    // 注意 RTK 会默认开启一个中间件,所以我们不能直接替换必须通过形参 getDefaultMiddleware 函数获取到所有的中间件再添加自定义中间件
    middleware: getDefaultMiddleware => [...getDefaultMiddleware(), actionLog]
  }
})

/middleware/actionlog

js
export const actionLog = store => next => action => {
  console.log('执行', action)
  // console.log('中间件1执行', store.getState());获取当前 redux 里的数据
  // console.log('被拦截的 action 对象', action);
  // next 函数用于分发 action 只有被分发的 action 才会执行
  next(action)
  // console.log('更新后的 state', store.getState());
}

login/slice.js

js
import { createSlice } from '@reduxjs/toolkit'
const initialState = {
  loading: true,
  product: 'test'
}
export const productDetailState = createSlice({
  name: 'productDetailState',
  initialState,
  reducers: {
    fetchSuccess: (state, action) => {
      state.product = action.payload.product
    }
  }
})

App.js

js
// 使用 Provider 包裹整个应用
import store from './redux/store'
import { Provider } from 'react-redux'
function App() {
  return (
    <Provider store={store}>
      <div className="App"></div>
    </Provider>
  )
}

在组件中使用

js
import { useSelector, useDispatch } from 'react-redux'
// 导入 login/slice
import { productDetailState } from '../../redux/login/slice'
export default function Login() {
  const product = useSelector(state => state.productDetail.product)
  const navigate = useNavigate()
  const dispatch = useDispatch()
  function setinfo() {
    // 调用 dispatch 修改数据
    dispatch(
      // 此处的 productDetailState 要和 /login/slice 到处的 action 的 name(命名空间)相同
      productDetailState.actions.fetchSuccess({
        product: 'test2'
      })
    )
  }
  return (
    <div className="login">
      {/* 使用数据 */}
      <h1 onClick={setinfo}>React项目: 后台管理系统{product}</h1>
    </div>
  )
}

反向代理

在脚手架中可以在 src 下新建 setupProxy.js

js
const proxy = require('http-proxy-middleware')

module.exports = function (app) {
  app.use(
    proxy('/api', {
      target: 'http://localhost:5000',
      // changeOrigin 用于控制服务器收到的请求头中 host 的值,如果不加这条配置,服务器获取到的请求地址就为当前前端应用启动机器的地址,如果加上那么就是这里 target 的值
      changeOrigin: true,
      pathRewrite: { '/^api': '' }
    }),
    proxy('/api1', {
      target: 'http://localhost:3000',
      changeOrigin: true,
      pathRewrite: { '/^api1': '' }
    })
  )
}

修改 webpack 配置

js
// https://ant.design/docs/react/use-with-create-react-app-cn 说明文档

antd 样式按需引入

js
// antd 公共样式如果不引入一些组件样式可能会有问题,如 message 组件
import 'antd/es/style/index.css'
// 按需引入用到的组件对应的css
import 'antd/es/message/style/index.css'
import 'antd/es/button/style/index.css'
import 'antd/es/input/style/index.css'
import 'antd/es/form/style/index.css'

function App() {
  return <div className="App"></div>
}

react18 新特性

js
// juejin.cn/post/7094037148088664078 参考
// react18 中只有使用了并发特性才会开启并发更新(将大量更新任务变得可中断)
const [list, setList] = useState([])
useEffect(() => {
  // startTransition API 就是并发特性,此处的更新会开启并发更新(标记为不紧急更新可能会被其他紧急渲染任务抢占)
  startTransition(() => {
    setList(new Array(10000).fill(null))
  })
})