公告
淡泊明志,宁静致远
网站资讯
本站文章字数合计
243.7k
本站Hexo版本
6.1.0
本站Node版本
16.14.2
本站已运行时间
最后更新时间
本文目录
  1. react入门
    1. 入门案例
    2. 创建虚拟DOM的两种方式
    3. 虚拟DOM和真实DOM
    4. JSX
      1. JSX的语法规则
      2. js语句和表达式的区别
  2. react面向组件编程
    1. 函数组件
    2. 类组件
    3. 组件实例的三大属性
      1. state
      2. props
        1. props的基本使用
        2. 批量传递props
        3. props的类型限制
        4. 函数组件使用props
        5. 类组件的构造器和props
      3. refs
        1. 字符串形式的ref
        2. 回调形式的ref
        3. createRef形式的ref
    4. 事件处理
    5. 受控组件和非受控组件
      1. 非受控组件
      2. 受控组件
        1. 受控组件基础
        2. 受控组件柯里化
        3. 受控组件不用柯里化
    6. 生命周期
      1. 生命周期-旧版
      2. 生命周期-新版
      3. 最重要的三个钩子
    7. 虚拟DOM和diff算法
  3. react脚手架
    1. 创建项目
    2. 项目结构说明
    3. TodoList 案例总结
    4. 配置代理
  4. 消息订阅发布机制
  5. 解构赋值再认识
  6. nanoid生成唯一标识
  7. react-router5
    1. 路由的理解
    2. 路由的基本使用
      1. Link组件
      2. Route组件
      3. 路由入门案例
    3. 路由组件和一般组件
    4. NavLink组件
    5. Switch 的使用
    6. 多级路径刷新样式丢失
    7. 路由的严格匹配与模糊匹配
    8. Redirect (重定向)
    9. 嵌套路由
    10. 路由传参
    11. 编程式导航
    12. withRouter 的使用
    13. BrowserRouter 和 HashRouter
  8. redux
    1. 概述
    2. 核心概念
      1. action
      2. reducer
      3. store
    3. 求和案例
    4. 异步编程
  9. react-redux
    1. 基本使用
    2. 优化写法
    3. 多组件数据共享
    4. 纯函数
    5. redux开发工具
  10. react扩展
    1. setState 更新状态的两种写法
    2. 路由组件懒加载 lazyLoad
    3. React Hook
      1. State Hook
      2. Ref Hook
      3. Effect Hook
    4. Fragment
    5. Context (了解)
    6. 组件渲染优化
    7. render props (插槽)
    8. 错误边界
    9. 组件通信方式总结
  11. react-router6
    1. 基本使用
    2. BrowserRouter
    3. HashRouter
    4. Routes
    5. Navigate-重定向
    6. useRoutes() 路由表
    7. Outlet 嵌套路由
    8. NavLink 路由高亮
    9. 路由传参
      1. 传递 params参数
      2. 传递 search参数
      3. 传递 state 参数
    10. useNavigate() 编程式路由导航
    11. Other Hooks(了解)
      1. useMatch()
      2. useInRouterContext
      3. useNavigationType
      4. useOutlet
      5. useResolvedPath
已阅读:%

分类: web前端 | 标签: react

react笔记

发表于: 2022-12-30 17:15:28 | 字数统计: 17k | 阅读时长预计: 84分钟

尚硅谷React教程(2022加更,B站超火react教程)

react入门

React:用于构建用户界面的 JavaScript 库。由 Facebook 开发且开源。

原生 JavaScript 的痛点:

  • 操作 DOM 繁琐、效率低
  • 使用 JavaScript 直接操作 DOM,浏览器进行大量重绘重排
  • 原生 JavaScript 没有组件化编码方案,代码复用率低

React 的特点:

  • 采用组件化模式、声明式编码,提高开发效率和组件复用率
  • React Native 中可用 React 语法进行移动端开发
  • 使用虚拟 DOM 和 Diffing 算法,减少与真实 DOM 的交互

入门案例

  • react.development.js :React 核心库
  • react-dom.development.js :提供 DOM 操作的 React 扩展库
  • babel.min.js :解析 JSX 语法,转换为 JS 代码
<!-- 引入react核心库 -->
<script type="text/javascript" src="../js/react.development.js"></script>
<!-- 引入react-dom,用于支持react操作DOM -->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!-- 引入babel,用于将jsx转为js -->
<script type="text/javascript" src="../js/babel.min.js"></script>

<!-- 准备容器 -->
<div id="app"></div>

<!-- 此处一定要写babel,表示写的不是 JS,而是 JSX,并且靠 babel 翻译 -->
<script type="text/babel">
        // 创建虚拟dom
        let VDOM = <div>Hello React</div>
        // 渲染
        ReactDOM.render(VDOM,document.getElementById('app'))
</script>

创建虚拟DOM的两种方式

两种方式是:js和jsx

  • 使用 JS 创建虚拟 DOM 比 JSX 繁琐
  • JSX 可以让程序员更加简单地创建虚拟 DOM,相当于语法糖
  • 最终 babel 会把 JSX 语法转换为 JS

开发中一般使用jsx的方式,也就是入门案例中使用的方式,会jsx就行

第一种:使用js创建

//1.使用 React 提供的 API 创建虚拟DOM
const VDOM = React.createElement('h1', { id: 'title' }, React.createElement('span', {}, 'Hello,React'))
//2.渲染虚拟DOM到页面
ReactDOM.render(VDOM, document.getElementById('app'))

第二种:使用jsx创建(掌握) 例子见入门案例

虚拟DOM和真实DOM

关于虚拟 DOM:

  1. 本质是 Object 类型的对象(一般对象)
  2. 虚拟 DOM 比较“轻”,真实 DOM 比较“重”,因为虚拟 DOM 是 React 内部在用,无需真实 DOM 上那么多的属性。
  3. 虚拟 DOM 最终会被 React 转化为真实 DOM,呈现在页面上。
//如果VDOM如果有多行,可以用()包起来
const VDOM = (
    <h1 id="title">
    <span>Hello,React</span>
    </h1>
)
ReactDOM.render(VDOM, document.getElementById('app'))

const TDOM = document.getElementById('demo')
console.log('虚拟DOM', VDOM)
console.log('真实DOM', TDOM)

JSX

  • 全称:JavaScript XML
  • React 定义的类似于 XML 的 JS 扩展语法;本质是 React.createElement() 方法的语法糖
  • 作用:简化创建虚拟 DOM

JSX的语法规则

  • 定义虚拟 DOM 时,不要写引号
  • 标签中混入 JS 表达式需要使用 {}
  • 指定类名不用 class,使用 className
  • 内联样式,使用 style={ { key: value } } 的形式
  • 只能有一个根标签
  • 标签必须闭合,单标签结尾必须添加 /<input type="text" />
  • 标签首字母小写,则把标签转换为 HTML 对应的标签,若没有,则报错
  • 标签首字母大写,则渲染对应组件,若没有定义组件,则报错
let title = 'HeLLOjSx'
let VDOM = (
    <div title={title.toUpperCase()}>
    <div className="info">info</div>
<div style={{fontSize: '26px',color: 'red'}}>style内联样式</div>
</div>
)
ReactDOM.render(VDOM,document.getElementById("app"))

js语句和表达式的区别

  • 表达式:一个表达式会产生一个,可以放在任何一个需要值的地方
a
a + b
demo(1)
arr.map()
function test() {}
  • 语句(代码):
if(){}
for(){}
switch(){case:xxxx}
let arr = ['vue','react','angular']
let VDOM = (
    <ul>
    {
        arr.map( v => <li>{v}</li> )
    }  
    </ul>
)
ReactDOM.render(VDOM,document.getElementById("app"))

react面向组件编程

函数组件

要点:

  • 组件名称首字母必须大写,否则会解析成普通标签导致报错,详见 JSX 语法规则
  • 函数需返回一个虚拟 DOM
  • 渲染组件时需要使用标签形式,同时标签必须闭合

渲染组件的过程:

  • React 解析标签,寻找对应组件
  • 发现组件是函数式组件,则调用函数,将返回的虚拟 DOM 转换为真实 DOM ,并渲染到页面中
//1.创建函数式组件
  function MyComponent() {
    console.log(this)//undefined
    return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
  }
  //2.渲染组件到页面
  ReactDOM.render(<MyComponent />, document.getElementById('app'))

注意: 上面函数组件内部this 是 undefined,因为 babel 编译后开启了严格模式

类组件

要点:

  • 使用class关键字来定义组件
  • 使用extends来继承React.Component类
  • 类中使用render方法来返回虚拟DOM

组件渲染过程:

  • React 解析组件标签,寻找组件
  • 发现是类式组件,则 new 该类的实例对象,通过实例调用原型上的 render 方法
  • render 返回的虚拟 DOM 转为真实 DOM ,渲染到页面上·
// 创建类式组件
  class MyComponent extends React.Component {
    render() {
      console.log('render中的this:', this)
      return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
    }
  }
  ReactDOM.render(<MyComponent />, document.getElementById('app'))

组件实例的三大属性

state

state 是组件实例对象最重要的属性,值为对象。又称为状态机,通过更新组件的 state 来更新对应的页面显示。

要点:

  • 初始化 state
  • React 中事件绑定
  • this 指向问题
  • setState 修改 state 状态
  • constructorrender 、自定义方法的调用次数
class Sum extends React.Component{
    constructor(props){// 调用一次
        super(props)
        //初始化state
        this.state = {count: 0}
        //解决this为undefined问题,将原型上面的add方法改变this之后,挂在实例上
        this.add = this.add.bind(this) 
        console.log(this)
    }
    add(){//这样写是挂在原型上
        console.log("add",this)
        //更新state的值
        this.setState({
            count: this.state.count + 1
        })
    }
    render(){//调用 1+N次
        return (
            // 注意绑定事件的方式:a)驼峰 b)带上{}  onClick={this.add}
                <div>
                    sum的值:{ this.state.count }<br/>
                    <button onClick={this.add}>+1</button>                    
                </div>
           )
    }
}
ReactDOM.render(<Sum />,document.getElementById("app"))

简化版(推荐)

class Sum extends React.Component{
    // = 表示直接挂在组件实例身上
    state = {count: 0}
    //采用箭头函数 + 赋值语句形式 this指向外面组件的this
    add = () => { 
        console.log("add",this) //this为组件实例
        //更新state的值
        this.setState({count: this.state.count + 1})
    }
    render(){//调用 1+N次
        return (
            <div>
                sum的值:{ this.state.count }<br/>
                <button onClick={this.add}>+1</button>                
            </div>
            )
    }
}
ReactDOM.render(<Sum />,document.getElementById("app"))

props

每个组件对象都有 props 属性,组件标签的属性都保存在 props 中。props只读的,不能修改。

props的基本使用

通过标签属性传值,会包装在组件的props属性上面。

class Person extends React.Component {
    render(){
        console.log(this.props)//{name: '青阳', age: 18, gender: '男'}
        const {name , age, gender} = this.props
        return (
            <div>
                <div>name: {name}</div>
                <div>age:  {age}</div>
                <div>gender: {gender}</div>
            </div>
        )
    }
}
// 类似于标签属性传值 注意:数值要用{}
ReactDOM.render(<Person name="青阳" age={18} gender="男"/>,document.getElementById('app'))
批量传递props
class Person extends React.Component {
    render(){
        console.log(this.props)
        const {name , age, gender} = this.props
        return (
            <div>
                <div>name: {name}</div>
                <div>age:  {age}</div>
                <div>gender: {gender}</div>
            </div>
        )
    }
}
const p = {name: '青阳',age: 19,gender: '男'}
//通过...批量传递props
ReactDOM.render(<Person {...p} />,document.getElementById('app'))
props的类型限制

React 15.5 以前,React 身上有一个 PropTypes 属性可直接使用,即 name: React.PropTypes.string.isRequired ,没有把 PropTypes 单独封装为一个模块。

React 15.5 开始,把 PropTypes 单独封装为一个模块,需要额外导入使用。

了解即可,用的时候再看,老项目可能使用这种,最新一般使用ts来做类型校验.

<!-- 引入prop-types,用于对组件标签属性进行限制 -->
<script type="text/javascript" src="../js/prop-types.js"></script>

<script type="text/babel">
  class Person extends React.Component {
    render() {
      const { name, age, sex } = this.props
      return (
        <ul>
          <li>姓名:{name}</li>
          <li>性别:{sex}</li>
          <li>年龄:{age}</li>
        </ul>
      )
    }
  }

  // 类型和必要性限制
  Person.propTypes = {
    name: PropTypes.string.isRequired,
    sex: PropTypes.string,
    age: PropTypes.number,
    // 限制 speak 为函数
    speak: PropTypes.func,
  }

  // 指定默认值
  Person.defaultProps = {
    sex: 'male',
    age: 19,
  }

  ReactDOM.render(<Person name="Vue" sex="male" age={11} speak={speak} />, document.getElementById('test'))

  function speak() {
    console.log('speaking...')
  }
</script>

Person.propTypesPerson.defaultProps 可以看作在类身上添加属性,利用 static 关键词就能在类内部进行声明。因此所谓简写只是从类外部移到类内部。

<!-- 引入prop-types,用于对组件标签属性进行限制 -->
<script type="text/javascript" src="../js/prop-types.js"></script>

<script type="text/babel">
  class Person extends React.Component {
    static propTypes = {
      name: PropTypes.string.isRequired,
      sex: PropTypes.string,
      age: PropTypes.number,
      // 限制 speak 为函数
      speak: PropTypes.func,
    }
    static defaultProps = {
      sex: 'male',
      age: 19,
    }

    render() {
      const { name, age, sex } = this.props
      return (
        <ul>
          <li>姓名:{name}</li>
          <li>性别:{sex}</li>
          <li>年龄:{age}</li>
        </ul>
      )
    }
  }

  ReactDOM.render(<Person name="Vue" sex="male" age={11} speak={speak} />, document.getElementById('test'))

  function speak() {
    console.log('speaking...')
  }
</script>
函数组件使用props

由于函数可以传递参数,因此函数式组件可以使用 props

function Person(props){
    const {name,age,gender} = props
    return <div>个人信息 name:{name} , age:{age} , gender:{gender}</div>
}
Person.propTypes = {
    age: PropTypes.number.required
}
Person.defaultProps = {
    age: 10
}
const p = {name: 'tom',gender: '男'}
ReactDOM.render(<Person {...p}/>,document.getElementById("app"))
类组件的构造器和props

官网文档说明 构造函数一般用在两种情况:

  • 通过给 this.state 赋值对象来初始化内部 state
  • 为事件处理函数绑定实例
constructor(props) {
  super(props)
  // 初始化 state
  this.state = { isHot: true, wind: '微风' }
  // 解决 this 指向问题
  this.changeWeather = this.changeWeather.bind(this)
}

因此构造器一般都不需要写。如果要在构造器内使用 this.props 才声明构造器,并且需要在最开始调用 super(props)

refs

Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。

字符串形式的ref

这种形式已过时,效率不高,官方 不建议使用。

class Demo extends React.Component{
    showInput = () => {
        //2.使用ref
        const { myRef } = this.refs
        console.log(myRef) //input标签dom
        console.log(myRef.value) //input标签的值
    }
    render(){
        return (
           <div>
            {/*1.定义ref*/}
            <input type="text" ref="myRef" />
            <button type="button" onClick={this.showInput}>点击显示输入框内容</button>
        </div>
        )
    }
}  
ReactDOM.render(<Demo/>,document.getElementById("app"))
回调形式的ref
class Demo extends React.Component{
    showInput = () => {
        console.log(this.myInput.value);
    }
    render(){
        return (
            <div>
              {/* 
                 通过回调将input标签的dom 挂在实例的 myInput属性上 
                 由于是箭头函数,因此 `this` 是 `render` 函数里的 `this` ,即组件实例
              */}
              <input type="text" ref={input => this.myInput = input} />
              <button type="button" onClick={this.showInput}>点击显示输入框内容</button>
            </div>
       )
    }
}  
ReactDOM.render(<Demo/>,document.getElementById("app"))
createRef形式的ref

该方式通过调用 React.createRef 返回一个容器用于存储节点,且一个容器只能存储一个节点。

class Demo extends React.Component{
    //1.创建ref对象
    myRef = React.createRef()
    showInput = () => {
        //3.使用ref对象
        console.log(this.myRef.current) //input dom对象
        console.log(this.myRef.current.value);
    }
    render(){
        return (
            <div>
                {/* 2.绑定ref对象 */}
                <input type="text" ref={this.myRef} />
                <button type="button" onClick={this.showInput}>点击显示输入框内容</button>
            </div>
            )
        }
}  
ReactDOM.render(<Demo/>,document.getElementById("app"))

事件处理

  • React 使用自定义事件,而非原生 DOM 事件,即 onClick、onBlur :为了更好的兼容性
  • React 的事件通过事件委托方式进行处理:为了高效
  • 通过 event.target 可获取触发事件的 DOM 元素:勿过度使用 ref

当触发事件的元素和需要操作的元素为同一个时,可以不使用 ref

class Demo extends React.Component {
  showData2 = (event) => {
    alert(event.target.value)
  }

  render() {
    return (
      <div>
        <input onBlur={this.showData2} type="text" placeholder="失去焦点提示数据" />
        &nbsp;
      </div>
    )
  }
}

受控组件和非受控组件

包含表单的组件分类:

  • 非受控组件:现用现取。即需要使用时,再获取节点得到数据
  • 受控组件:类似于Vue 双向绑定的从视图层绑定到数据层

尽量使用受控组件,因为非受控组件需要使用大量的 ref

非受控组件

主要是通过ref来实现

class Login extends React.Component{
    handleSubmit = (e) => {
        e.preventDefault();
        const {username,password} = this
        console.log(username.value,",",password.value);
    }
    render(){
        return (
            <div>
                <form onSubmit={this.handleSubmit}>
                账号:<input type="text" ref={c => this.username = c}/><br/>
                    密码:<input type="password" ref={c => this.password = c}/><br/>
                        <button type="submit">登录</button>
                </form>
            </div>
            )
    }
}  
ReactDOM.render(<Login/>,document.getElementById("app"))

受控组件

受控组件基础

通过表单的onChange事件,当内容变化时,将数据更新到state中

class Login extends React.Component{
    handleSubmit = e => {
        e.preventDefault();//阻止默认提交
        const {username,password} = this.state
        console.log(username,",",password);
    }
    setUsername = e => {
        this.setState({username: e.target.value})
    }
    setPassword = e => {
        this.setState({password: e.target.value})
    }
    render(){
            return (
                <div>
                    <form onSubmit={this.handleSubmit}>
                    账号:<input type="text" onChange={this.setUsername}/><br/>
                        密码:<input type="password" onChange={this.setPassword}/><br/>
                            <button type="submit">登录</button>
                    </form>
                </div>
            )
    }
}  
ReactDOM.render(<Login/>,document.getElementById("app"))

对上述受控组件的代码进行优化,希望把 saveUsernamesavePassword 合并为一个函数。有如下两种方案。

  • 高阶函数:参数为函数或者返回一个函数的函数,如 Promise、setTimeout、Array.map()
  • 函数柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式
受控组件柯里化
class Login extends React.Component{
    handleSubmit = (e) => {
        e.preventDefault();
        const {username,password} = this.state
        console.log(username,",",password);
    }
    setFormData = name => {
        return e => {//函数调用返回一个函数,多次接受参数统一处理
            this.setState({[name]: e.target.value})
        }
    }
    render(){
        return (
            <div>
            <form onSubmit={this.handleSubmit}>
            账号:<input type="text" onChange={this.setFormData('username')}/><br/>
            密码:<input type="password" onChange={this.setFormData('password')}/><br/>
            <button type="submit">登录</button>
            </form>
            </div>
            )
    }
}  
ReactDOM.render(<Login/>,document.getElementById("app"))
受控组件不用柯里化

和柯里化差不多,只不过是在onChange里面定义一个箭头函数来接受参数

class Login extends React.Component{
    handleSubmit = (e) => {
        e.preventDefault();
        const {username,password} = this.state
        console.log(username,",",password);
    }
    setFormData = (e,name) => {
        this.setState({[name]: e.target.value})
    }
    render(){
        return (
         <div>
            <form onSubmit={this.handleSubmit}>
         账号:<input type="text" onChange={e => this.setFormData(e,'username') }/>    
         密码:<input type="password" onChange={e => this.setFormData(e,'password') }/>
              <button type="submit">登录</button>
            </form>
        </div>
        )
    }
}  
ReactDOM.render(<Login/>,document.getElementById("app"))

生命周期

生命周期-旧版

初始化阶段ReactDOM.render() 触发的初次渲染

  • constructor
  • componentWillMount
  • render
  • componentDidMount

更新阶段

  1. 父组件重新 render 触发的更新
  • componentWillReceiveProps
  • shouldComponentUpdate :控制组件是否更新的阀门,返回值为布尔值,默认为 true 。若返回 false ,则后续流程不会进行。
  • componentWillUpdate
  • render
  • componentDidUpdate
  1. 组件内部调用 this.setState() 修改状态
  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate
  1. 组件内部调用 this.forceUpdate() 强制更新
  • componentWillUpdate
  • render
  • componentDidUpdate

卸载阶段ReactDOM.unmountComponentAtNode() 触发

  • componentWillUnmount

React Lifecycle

生命周期-新版

更改内容

  • 废弃三个钩子:componentWillMountcomponentWillReceivePropscomponentWillUpdate 。在新版本中这三个钩子需要加 UNSAFE_ 前缀才能使用,后续可能会废弃。
  • 新增两个钩子(实际场景用得很少):getDerivedStateFromPropsgetSnapshotBeforeUpdate

React LIfecycle New

static getDerivedStateFromProps(props, state)

  • 需使用 static 修饰
  • 需返回一个对象更新 state 或返回 null
  • 适用于如下情况:state 的值任何时候都取决于 props

getSnapshotBeforeUpdate(prevProps, prevState)

  • 在组件更新之前获取快照
  • 得组件能在发生更改之前从 DOM 中捕获一些信息(如滚动位置)
  • 返回值将作为参数传递给 componentDidUpdate()
static getDerivedStateFromProps(props,state){
  console.log('getDerivedStateFromProps',props,state);
  return null
}

getSnapshotBeforeUpdate(){
  console.log('getSnapshotBeforeUpdate');
  return 'atguigu'
}

componentDidUpdate(preProps,preState,snapshotValue){
  console.log('componentDidUpdate',preProps,preState,snapshotValue);
}
// getSnapshotBeforeUpdate 案例
class NewsList extends React.Component {
  state = { newsArr: [] }

  componentDidMount() {
    setInterval(() => {
      //获取原状态
      const { newsArr } = this.state
      //模拟一条新闻
      const news = '新闻' + (newsArr.length + 1)
      //更新状态
      this.setState({ newsArr: [news, ...newsArr] })
    }, 1000)
  }

  getSnapshotBeforeUpdate() {
    return this.refs.list.scrollHeight
  }

  componentDidUpdate(preProps, preState, height) {
    this.refs.list.scrollTop += this.refs.list.scrollHeight - height
  }

  render() {
    return (
      <div className="list" ref="list">
        {this.state.newsArr.map((n, index) => {
          return (
            <div key={index} className="news">
              {n}
            </div>
          )
        })}
      </div>
    )
  }
}
ReactDOM.render(<NewsList />, document.getElementById('test'))

最重要的三个钩子

  • render :初始化渲染和更新渲染
  • componentDidMount :进行初始化,如开启定时器、发送网络请求、订阅消息
  • componentWillUnmount :进行收尾,如关闭定时器、取消订阅消息

虚拟DOM和diff算法

Diff

key 的作用:

key 是虚拟 DOM 对象的标识,可提高页面更新渲染的效率。

当状态中的数据发生变化时,React 会根据新数据生成新的虚拟 DOM ,接着对新旧虚拟 DOM 进行 Diff 比较,规则如下:

  • 旧虚拟 DOM 找到和新虚拟 DOM 相同的 key:
    • 若内容没变,直接复用真实 DOM
    • 若内容改变,则生成新的真实 DOM ,替换页面中之前的真实 DOM
  • 旧虚拟 DOM 未找到和新虚拟 DOM 相同的 key:根据数据创建新的真实 DOM ,渲染到页面

使用 index 作为 key 可能引发的问题:

  • 若对数据进行逆序添加、逆序删除等破坏顺序的操作,会进行没有必要的真实 DOM 更新。界面效果没问题,但效率低下。
  • 如果结构中包含输入类的 DOM(如 input 输入框) ,则会产生错误的 DOM 更新。
  • 若不存在对数据逆序添加、逆序删除等破坏顺序的操作,则没有问题。
// 使用 index 作为 key 引发的问题
class Person extends React.Component {
  state = {
    persons: [
      { id: 1, name: '小张', age: 18 },
      { id: 2, name: '小李', age: 19 },
    ],
  }

  add = () => {
    const { persons } = this.state
    const p = { id: persons.length + 1, name: '小王', age: 20 }
    this.setState({ persons: [p, ...persons] })
  }

  render() {
    return (
      <div>
        <h2>展示人员信息</h2>
        <button onClick={this.add}>添加小王</button>
        <h3>使用index作为key</h3>
        <ul>
          {this.state.persons.map((personObj, index) => {
            return (
              <li key={index}>
                {personObj.name}---{personObj.age}
                <input type="text" />
              </li>
            )
          })}
      </div>
    )
  }
}

react脚手架

创建项目

  • 全局安装 React 脚手架:npm i -g create-react-app
  • 创建项目:create-react-app 项目名称
  • 进入文件夹:cd 项目名称
  • 启动项目:npm start

上述方式已经过时,改用下方命令。详见官方说明

npx create-react-app my-app
cd my-app
npm start

项目结构说明

public :静态资源文件

  • manifest.json :应用加壳(把网页变成安卓/IOS 软件)的配置文件
  • robots.txt :爬虫协议文件

src :源码文件

  • App.test.js :用于给 App 组件做测试,一般不用
  • index.js :入口文件
  • reportWebVitals.js :页面性能分析文件,需要 web-vitals 库支持
  • setupTests.js :组件单元测试文件,需要 jest-dom 库支持

React-cli structure

index.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="red" />
    <!-- 网站描述 -->
    <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>

删除项目中不必要的文件,并且使用jsx文件来定义组件

image-20220911172638523

TodoList 案例总结

  1. 拆分组件、实现静态组件,注意:classNamestyle 的写法
  2. 动态初始化列表,如何确定将数据放在哪个组件的 state 中?
  • 某个组件使用:放在其自身的 state
  • 某些组件使用:放在他们共同的父组件 state 中,即状态提升
  1. 关于父子之间通信:
  • 父传子:直接通过 props 传递
  • 子传父:父组件通过 props 给子组件传递一个函数,子组件调用该函数
// 父组件
class Father extends Component {
  state: {
    todos: [{ id: '001', name: '吃饭', done: true }],
    flag: true,
  }

  addTodo = (todo) => {
    const { todos } = this.state
    const newTodos = [todo, ...todos]
    this.setState({ todos: newTodos })
  }

  render() {
    return <List todos={this.state.todos} addTodo={this.addTodo} />
  }
}

// 子组件
class Son extends Component {
  // 由于 addTodo 是箭头函数,this 指向父组件实例对象,因此子组件调用它相当于父组件实例在调用
  handleClick = () => {
    this.props.addTodo({ id: '002', name: '敲代码', done: false })
  }

  render() {
    return <button onClick={this.handleClick}>添加</button>
  }
}
  1. 注意 defaultCheckedchecked 的区别,类似的还有:defaultValuevalue
  2. 状态在哪里,操作状态的方法就在哪里

配置代理

方法一:

package.json 文件中进行配置:

"proxy": "http://localhost:5000"
  • 优点:配置简单,前端请求资源可不加前缀
  • 缺点:不能配置多个代理
  • 工作方式:当请求了 3000 端口号(本机)不存在的资源时,就会把请求转发给 5000 端口号服务器

方法二:

src 目录下创建代理配置文件 setupProxy.js ,进行配置:

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

module.exports = function (app) {
  app.use(
    //api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
    proxy('/api1', {
      //配置转发目标地址(能返回数据的服务器地址)
      target: 'http://localhost:5000',
      //控制服务器接收到的请求头中host字段的值
      /*
      changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
      changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000
      changeOrigin默认值为false,但一般将changeOrigin改为true
      */
      changeOrigin: true,

      //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
      pathRewrite: { '^/api1': '' },
    }),
    proxy('/api2', {
      target: 'http://localhost:5001',
      changeOrigin: true,
      pathRewrite: { '^/api2': '' },
    })
  )
}

消息订阅发布机制

即 React 中兄弟组件或任意组件之间的通信方式。(类似vue的bus)

  • 先订阅,再发布(隔空对话)
  • 适用于任意组件间通信
  • 要在 componentWillUnmount 钩子中取消订阅

使用的工具库:PubSubJS

npm i pubsub-js --save

基础用法:

import PubSub from 'pubsub-js'

// 发布消息
PubSub.publish('topic', 'hello react')

// 订阅消息 token是一个标识,类似setTimeout会有个返回值作为标识
let token = PubSub.subscribe('topic', (msg, data) => {
  console.log(msg, data)
})

// 取消订阅
PubSub.unsubscribe(token)

解构赋值再认识

熟悉一下连续解构赋值、连续解构赋值+重命名

let obj = { a: { b: 1 } }

//传统解构赋值
const { a } = obj

//连续解构赋值
const {
  a: { b },
} = obj

//连续解构赋值 + 重命名
const {
  a: { b: value },
} = obj

nanoid生成唯一标识

https://gitee.com/mirrors/nanoid

生成唯一标识的一个库

npm install --save nanoid

使用

import {nanoid} from 'nanoid'
console.log(nanoid())

react-router5

路由的理解

何为路由?

  • 一个路由是一个映射关系
  • key 为路径,value 可能是 function组件

后端路由:

  • valuefunction ,用于处理客户端的请求
  • 注册路由:router.get(path, function(req, res))
  • 工作过程:Node 接收到请求,根据路径匹配路由,调用对应函数处理请求,返回响应数据

前端路由:

  • value 是组件
  • 注册路由:<Route path="/test" component={Test}>
  • 工作过程:浏览器路径变为 /test ,展示 Test 组件

路由的基本使用

安装 react-router-dom

// 安装 5.X 版本路由
npm install react-router-dom@5.2.0 -S

最新默认是安装的6.x版本,这里以 5.x 版本为例展示基本使用,后面再写6.x版本的使用

导航区使用 <Link>,展示区使用 <Route>

Link组件

定义链接,用来替换a标签

  • to属性:定义点击之后切换的路径

  • className属性: 定义类名

  • 标签体:定义显示的名称

【例如】

<Link className="list-group-item" to="/about">about</Link>

Route组件

路由组件匹配成功之后的展示区

  • path属性:匹配的路径
  • component属性:匹配成功后展示的组件

【例如】

<Route path="/about" component={About} />

路由入门案例

  • 复制boostrap.css到public/css下,并且在index.html中引入

image-20220911224233636

  • <App> 的最外侧包裹 <BrowserRouter><HashRouter>
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import App from './App'

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
)
  • App.jsx
// App.jsx
import React, { Component } from 'react'
import { Link, Route } from 'react-router-dom'
import Home from './components/Home'
import About from './components/About'

export default class App extends Component {
  render() {
    return (
      <div>
        <div className="list-group">
          <Link className="list-group-item" to="/about">
            About
          </Link>
          <Link className="list-group-item" to="/home">
            Home
          </Link>
        </div>
        <div className="panel-body">
          <Route path="/about" component={About} />
          <Route path="/home" component={Home} />
        </div>
      </div>
    )
  }
}

路由组件和一般组件

存放位置不同

  • 一般组件:components
  • 路由组件:pages

写法不同

  • 一般组件:<Demo/>
  • 路由组件:<Route path="/demo" component={Demo}/>

接收到的 props不同

  • 一般组件:标签属性传递
  • 路由组件:接收到三个固定的属性(history、location、match)
history:
  go: ƒ go(n)
  goBack: ƒ goBack()
  goForward: ƒ goForward()
  push: ƒ push(path, state)
  replace: ƒ replace(path, state)

location:
  pathname: "/home/message/detail/2/hello"
  search: ""
  state: undefined

match:
  params: {}
  path: "/home/message/detail/:id/:title"
  url: "/home/message/detail/2/hello"

NavLink 可以实现路由链接的高亮,通过 activeClassName 指定样式名,默认追加类名为 active

<NavLink activeClassName="navLinkActive" to="/about">About</NavLink>

<NavLink activeClassName="navLinkActive" to="/home">Home</NavLink>

封装 NavLink 组件:由于 NavLink 组件中重复的代码太多,因此进行二次封装。

※ 细节点:组件标签的内容会传递到 this.props.children 属性中,反过来通过指定标签的 children 属性可以修改组件标签内容

// MyNavLink 组件
import React, { Component } from 'react'
import { NavLink } from 'react-router-dom'

export default class MyNavLink extends Component {
  render() {
    // this.props.children 可以取到标签内容,如 About, Home
    // 反过来通过指定标签的 children 属性可以修改标签内容
   return <NavLink activeClassName="active" className="list-group-item" {...this.props}/>
  }
}
<MyNavLink to="/about">About</MyNavLink>

<MyNavLink to="/home">Home</MyNavLink>

Switch 的使用

Switch 可以提高路由匹配效率,如果匹配成功,则不再继续匹配后面的路由,即单一匹配。

<!-- 只会展示 Home 组件 -->
<Switch>
  <Route path="/about" component="{About}" />
  <Route path="/home" component="{Home}" />
  <Route path="/home" component="{Test}" />
</Switch>

多级路径刷新样式丢失

  • public/index.html 中 引入样式时不写 .// (常用)
  • public/index.html 中 引入样式时不写 ./%PUBLIC_URL% (常用)
  • 使用 HashRouter
<link rel="stylesheet" href="/css/bootstrap.css" />

<link rel="stylesheet" href="%PUBLIC_URL%/css/bootstrap.css" />

路由的严格匹配与模糊匹配

  • 默认使用模糊匹配(Route组件的path必须包含Link组件要匹配的路径,且顺序一致)
  • 开启严格匹配:<Route exact path="/about" component={About}/>
  • 严格匹配需要再开,开启可能会导致无法继续匹配二级路由

Redirect (重定向)

一般写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到 Redirect 指定的路由

<Switch>
  <Route path="/about" component="{About}" />
  <Route path="/home" component="{Home}" />
  <Redirect to="/about" />
</Switch>

嵌套路由

  • 注册子路由需写上父路由的 path
  • 路由的匹配是按照注册路由的顺序进行

父组件

<!-- 父组件 -->
<MyNavLink to="/about">About</MyNavLink>
<MyNavLink to="/home">Home</MyNavLink>

<Switch>
  <Route path="/about" component={About} />
  <Route path="/home" component={Home} />
  <Redirect to="/about" />
</Switch>

子组件

<ul className="nav nav-tabs">
  <li>
    <MyNavLink to="/home/news">News</MyNavLink>
  </li>
  <li>
    <MyNavLink to="/home/message">Message</MyNavLink>
  </li>
</ul>

<Switch>
  <Route path="/home/news" component={News} />
  <Route path="/home/message" component={Message} />
  <Redirect to="/home/news" />
</Switch>

路由传参

三种方式:params, search, state 参数

三种方式对比:

  • state 方式当前页面刷新可保留参数,但在新页面打开不能保留。前两种方式由于参数保存在 URL 地址上,因此都能保留参数。
  • paramssearch 参数都会变成字符串
<!--1.params方式传参 -->
<Link to='/home/message/detail/tom/21'>params</Link>
<Link to={`/home/message/detail/${item.name}/${item.age}`}>{item.name}</Link>
<!--2.search方式传参 -->
<Link to='/home/message/detail/?name=tom&age=21'>search</Link>
<Link to={`/home/message/detail/?id=${item.name}&title=${item.age}`}>{item.name}</Link>
<!--3.state方式传参 -->
<Link to={{pathname:'/home/message/detail',state: {name: 'tom', age: 21}}}>state</Link>

<!-- params 注册路由 -->
<Route path='/home/message/detail/:name/:age' component={Detail} />
<!-- search 和 state 按正常注册即可 -->
<Route path='/home/message/detail' component={Detail} />

接收参数

//1.params方式接受参数
const { name, age } = this.props.match.params
//2.search方式接受参数
import qs from 'querystring'
const { search } = this.props.location
const { name, age } = qs.parse(search.slice(1))
//3.state方式接受参数
const { name, age } = this.props.location.state

编程式导航

编程式导航是使用路由组件 this.props.history 提供的 API 进行路由跳转:

this.props.history.push(path, state)
this.props.history.replace(path, state)
this.props.history.goForward()
this.props.history.goBack()
this.props.history.go(n)

三种传参方式的编程式导航

// 编程式导航传参
this.props.history.push(`/home/message/detail/${id}/${title}`)
this.props.history.push(`/home/message/detail?id=${id}&title=${title}`)
this.props.history.push(`/home/message/detail`, { id: id, title: title })

withRouter 的使用

withRouter 的作用:加工一般组件,让其拥有路由组件的 API ,如 this.props.history.push 等。

import React, {Component} from 'react'
import {withRouter} from 'react-router-dom'

class Header extends Component {
  ...
}

export default withRouter(Header) //导出前用withRouter加工一下

BrowserRouter 和 HashRouter

底层原理不一样:

  • BrowserRouter 使用的是 H5 的 history API,不兼容 IE9 及以下版本。
  • HashRouter 使用的是 URL 的哈希值。

路径表现形式不一样

  • BrowserRouter 的路径中没有 # ,如:localhost:3000/demo/test
  • HashRouter 的路径包含#,如:localhost:3000/#/demo/test

刷新后对路由 state 参数的影响

  • BrowserRouter 没有影响,因为 state 保存在 history 对象中。
  • HashRouter 刷新后会导致路由 state 参数的丢失!

备注:HashRouter 可以用于解决一些路径错误相关的问题

redux

官网

中文文档

概述

Redux 为何物,类似vue中的vuex

  • Redux 是用于做 状态管理 的 JS 库
  • 可用于 React、Angular、Vue 等项目中,常用于 React
  • 集中式管理 React 应用多个组件共享的状态

何时用 Redux

  • 某个组件的状态,需要让其他组件拿到(状态共享)
  • 一个组件需要改变另一个组件的状态(通信)
  • 使用原则:不到万不得已不要轻易动用

Redux 工作流程

redux 工作流程图

  • 组件想操作 Redux 中的状态:把动作类型和数据告诉 Action Creators
  • Action Creators 创建 action :同步 action 是一个普通对象,异步 action 是一个函数
  • Store 调用 dispatch() 分发 actionReducers 执行
  • Reducers 接收 previousStateaction 两个参数,对状态进行加工后返回新状态
  • Store 调用 getState() 把状态传给组件

核心概念

action

  • 表示动作的对象,包含 2 个属性

  • type :标识属性,值为字符串,唯一,必须属性

  • data :数据属性,类型任意,可选属性。

例如:{type: 'increment', data: 2}

reducer

  • 用于初始化状态、加工状态
  • 根据旧状态和 action 产生新状态
  • 纯函数

纯函数:输入同样的实参,必定得到同样的输出

  • 不能改写参数数据
  • 不产生副作用,如网络请求、输入输出设备(网络请求不稳定)
  • 不能调用 Date.now()Math.random() 等不纯方法

store

  • Redux 核心对象,内部维护着 statereducer
  • 核心 API
    • store.getState() :获取状态
    • store.dispatch(action) :分发任务,触发 reducer 调用,产生新状态
    • store.subscribe(func) :注册监听函数,当状态改变自动调用

求和案例

  • 安装redux
npm i redux@4.0.5
  • App.jsx
import React, { Component } from 'react'
import Count from './components/Count'

export default class App extends Component {
  render() {
    return (
      <div>
        <Count />
      </div>
    )
  }
}
  • index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import store from './redux/store'

ReactDOM.render(<App />, document.getElementById('root'))

// 状态改变重新渲染 App 组件
store.subscribe(() => {
  ReactDOM.render(<App />, document.getElementById('root'))
})


//react18的写法如下
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)
store.subscribe(()=>{
    root.render(<App />)
})
  • redux/constant.js 定义常量
// 保存常量值
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
  • redux/count_reducer.js
import { INCREMENT, DECREMENT } from './constant'

//初始化状态
const initState = 0
export default function countReducer(preState = initState, action) {
  const { type, data } = action
  switch (type) {
    case INCREMENT:
      return preState + data
    case DECREMENT:
      return preState - data
    default:
      return preState
  }
}
  • redux/store.js
import { createStore } from 'redux'
//引入为 Count 组件服务的 reducer
import countReducer from './count_reducer'

export default createStore(countReducer)
  • count_action.js
import {INCREMENT,DECREMENT} from './constant'

export const createIncrementAction = data => ({type: INCREMENT,data})
export const createdecrementAction = data => ({type: DECREMENT,data})
  • components/Count/index.jsx
import React, { Component } from 'react'
import store from '../../redux/store'
import { createIncrementAction, createDecrementAction } from '../../redux/count_action'

export default class Count extends Component {
  // 可在组件单独监听 Redux 状态变化
  // componentDidMount() {
  //     store.subscribe(() => {
  //         this.setState({})
  //     })
  // }

  increment = () => {
    const { value } = this.selectNumber
    // 将 value 转为数值
    // 手动写 Action 对象
    store.dispatch({ type: 'increment', data: value * 1 })
    // 专门创建 Action 对象
    store.dispatch(createIncrementAction(value * 1))
  }

  decrement = () => {
    const { value } = this.selectNumber
    store.dispatch(createDecrementAction(value * 1))
  }

  incrementAsync = () => {
    const { value } = this.selectNumber
    setTimeout(() => {
      store.dispatch(createIncrementAction(value * 1))
    }, 500)
  }

  render() {
    return (
      <div>
        <h1>当前求和为:{store.getState()}</h1>
        <select ref={(c) => (this.selectNumber = c)}>
          <option value="1">1</option>
          <option value="2">2</option>
          <option value="3">3</option>
        </select>
        <button onClick={this.increment}>+</button>
        <button onClick={this.decrement}>-</button>
        <button onClick={this.incrementAsync}>异步加</button>
      </div>
    )
  }
}

编写步骤总结:

1.创建constant.js定义常量

2.创建count_reducer.js

3.根据count_reducer创建store

4.创建action

5.在组件中使用 store.dispatch(action)更新数据 ,store.getState()获取数据

注意:

  • redux 只负责管理状态,状态改变驱动页面展示要自己写
  • 可以在 index.js 中统一监听状态变化,也可以在组件中单独监听。注意不能直接 this.render() 调用 render 函数,要通过 this.setState({}) 间接调用
  • reducerstore 自动触发首次调用,传递的 preStateundefinedaction{type: '@@REDUX/ININT_a.5.v.9'} 类似的数据格式,只有 type

异步编程

1.延迟的动作不想交给组件,而是 action
2.当操作状态所需数据要靠异步任务返回时,可用异步 action
3.创建 action 的函数返回一个函数,该函数中写异步任务
4.异步任务完成后,分发一个同步 action 操作状态
5.异步 action 不是必要的,完全可以在组件中等待异步任务结果返回在分发同步 action

安装中间件

npm i redux-thunk@2.3.0

对上面的求和案例进行修改

  • store.js
import { createStore, applyMiddleware } from 'redux'
import countReducer from './count_reducer'
import thunk from 'redux-thunk' //引入中间件
//创建store的时候,使用中间件
export default createStore(countReducer, applyMiddleware(thunk))
  • count_action.js
import { INCREMENT, DECREMENT } from './constant.js'

export const createIncrementAction = (data) => ({ type: INCREMENT, data })
export const createDecrementAction = (data) => ({ type: DECREMENT, data })

// 异步 action 返回一个函数
export const createIncrementAsyncAction = (data, time) => {
  return (dispatch) => {
    setTimeout(() => {
      dispatch(createIncrementAction(data))
    }, time)
  }
}
  • Count.jsx
incrementAsync = () => {
  const { value } = this.selectNumber
  store.dispatch(createIncrementAsyncAction(value * 1))
}

整个过程简单理解:store 在分发 action 时,发现返回一个函数,那它知道这是个异步 action 。因此 store 帮忙执行这个函数(ps:使用redux-thunk才会执行,否则报错),同时给这个函数传递 dispatch 方法,等待异步任务完成取到数据后,直接调用 dispatch 方法分发同步 action

react-redux

React-Redux 是一个插件库,用于简化 React 中使用 Redux 。

image-20221228140202071

React-Redux 将组件分为两类:

  • UI 组件
    • 只负责 UI 呈现,不带有业务逻辑
    • 通过 props 接收数据
    • 不能使用 Redux 的 API
    • 保存在 components 文件夹下
  • 容器组件
    • 负责管理数据和业务逻辑,和 Redux 通信,将结果交给 UI 组件
    • 可使用 Redux 的 API
    • 保存在 containers 文件夹下

基本使用

要点:

  • 装包:npm i react-redux
  • connect()() :创建容器组件
  • mapStateToProps(state) :映射状态为 UI 组件标签属性,即传递状态
  • mapDispatchToProps(dispatch) :传递操作状态的方法
  • 容器组件中的 store 是靠 props 传进去,而不是在容器组件中直接引入

【react-redux使用案例】

Count 容器组件

// containers/Count/index.jsx
// Count 容器组件

import CountUI from '../../components/Count'
import { connect } from 'react-redux'

import { createIncrementAction, createDecrementAction, createIncrementAsyncAction } from '../../redux/count_action'

function mapStateToProps(state) {
  return {
    count: state,
  }
}

function mapDispatchToProps(dispatch) {
  return {
    add: (number) => dispatch(createIncrementAction(number)),
    sub: (number) => dispatch(createDecrementAction(number)),
    addAsync: (number) => dispatch(createIncrementAsyncAction(number, time)),
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(CountUI)

App.jsx

// App.jsx
import React, { Component } from 'react'
import Count from './containers/Count'
import store from './redux/store.js'

export default class App extends Component {
  render() {
    return (
      <div>
        <Count store={store} />
      </div>
    )
  }
}

Count UI 组件

// components/Count/index.jsx
// Count UI 组件

increment = () => {
  const { value } = this.selectNumber
  this.props.add(value * 1)
}

decrement = () => {
  const { value } = this.selectNumber
  this.props.sub(value * 1)
}

incrementAsync = () => {
  const { value } = this.selectNumber
  this.props.addAsync(value * 1, 500)
}

优化写法

mapDispatchToProps 可以写成对象形式,React-Redux 底层会帮助自动分发。

// 函数写法
export default connect(
  state => ({count:state}),
  dispatch => ({
    add: number => dispatch(createIncrementAction(number)),
    sub: number => dispatch(createDecrementAction(number)),
    addAsync: (number,time) => dispatch(createIncrementAsyncAction(number,time)),
  })
)(CountUI)

// 对象写法
export default connect(
  state => ({ count: state }),
  {
    add: createIncrementAction,
    sub: createDecrementAction,
    addAsync: createIncrementAsyncAction,
  }
)(CountUI)

React-Redux 容器组件可以自动监测 Redux 状态变化,因此 index.js 不需要手动监听:

//不用写下面这些代码了
store.subscribe(() => {
  ReactDOM.render(<App />, document.getElementById('root'))
})

Provider 组件的使用:让所有组件都能获得状态数据,不必一个一个传递

// index.js

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { Provider } from 'react-redux'
import store from './redux/store'

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

整合容器组件和 UI 组件为一个文件:

import React, { Component } from 'react'
import {createIncrementAction,createDecrementAction} from '../../redux/count_action'
import {connect} from 'react-redux'

// 定义 UI 组件
class Count extends Component {
  ...
}

// 创建容器组件
export default connect(
  state => ({count: state}),
  {
    add: createIncrementAction,
    sub: createDecrementAction
  }
)(Count)

多组件数据共享

首先规范化文件结构,容器组件和 UI 组件合为一体后放在 containers 文件夹。redux 文件夹新建 actionsreducers 文件夹分别用于存放每个组件对应的 actionreducer

新建 Person 组件对应的 actionreducer

// redux/actions/person.js

import { ADD_PERSON } from '../constant.js'

export const createAddPersonAction = (personObj) => ({ type: ADD_PERSON, data: personObj })
// redux/reducers/person.js

import { ADD_PERSON } from '../constant.js'

const initState = [{ id: 'lsfd', name: 'china', age: '9999' }]
export default function personReducer(preState = initState, action) {
  const { type, data } = action
  switch (type) {
    case ADD_PERSON:
      return [data, ...preState]
    default:
      return preState
  }
}

关键步骤:在 store.js 中使用 combineReducers() 整合多个 reducer 来创建 store 对象。

这样 Redux 中就以对象的形式存储着每个组件的数据。类似于这样:

{
  total: 0,
  personList: []
}
// redux/store.js

import { createStore, applyMiddleware, combineReducers } from 'redux'
import countReducer from './reducers/count'
import personReducer from './reducers/person'
import thunk from 'redux-thunk'

const Reducers = combineReducers({ //关键代码!!!
  total: countReducer,
  personList: personReducer,
})

export default createStore(Reducers, applyMiddleware(thunk))

Person 组件中获取 Redux 保存的状态,包括其他组件的数据。

import React, { Component } from 'react'
import { connect } from 'react-redux'
import { createAddPersonAction } from '../../redux/actions/person'
import { nanoid } from 'nanoid'

class Person extends Component {
  addPerson = () => {
    const name = this.nameInput.value
    const age = this.ageInput.value
    const personObj = { id: nanoid(), name, age }
    this.props.addPerson(personObj)
    this.nameInput.value = ''
    this.ageInput.value = ''
  }

  render() {
    return (
      <div>
        <h2>在Person组件拿到Count组件的数据:{this.props.count}</h2>
        <input type="text" ref={(c) => (this.nameInput = c)} placeholder="Please input name" />
        <input type="text" ref={(c) => (this.ageInput = c)} placeholder="Please input age" />
        <button onClick={this.addPerson}>添加</button>
        <ul>
          {this.props.personList.map((item) => {
            return (
              <li key={item.id}>
                {item.name} -- {item.age}
              </li>
            )
          })}
        </ul>
      </div>
    )
  }
}

export default connect(
  // state 是 Redux 保存的状态对象
  // 容器组件从 Redux 中取出需要的状态,并传递给 UI 组件
  state => ({personList: state.personList, count: state.total}),
  {
    addPerson: createAddPersonAction
  }
)(Person)

一个细节,在 personReducer 中,是按如下方式修改状态的,而没有使用 unshift 方法。在第二种方式,React 会认为状态没有变化从而不会重新渲染页面,因为 preState 保存的是数组地址值,返回的地址和之前的地址是一样的,尽管数组内容发生了改变。而第一种方式返回一个新的数组的地址值,和之前不一样,因此会重新渲染页面。

// 方式一
switch (type) {
  case ADD_PERSON:
    return [data, ...preState]
  default:
    return preState
}

// 方式二
switch (type) {
  case ADD_PERSON:
    preState.unshift(data)
    return preState
  default:
    return preState
}

纯函数

概念:输入同样的参数,返回同样的输出。

约束:

  • 不能修改参数数据
  • 不产生任何副作用,如网络请求、输入和输出设备
  • 不能调用 Date.now()Math.random() 等不纯的方法

reducer 的函数必须是纯函数。

redux开发工具

  • Chrome 安装 Redux DevTools 开发者工具

  • 项目下载依赖包 npm i redux-devtools-extension --save-dev

  • 最后在 store.js 进行配置:

import { composeWithDevTools } from 'redux-devtools-extension'
...
export default createStore(Reducers, composeWithDevTools(applyMiddleware(thunk)))
// 不需要异步中间件
export default createStore(Reducers, composeWithDevTools())

react扩展

setState 更新状态的两种写法

对象式:setState(stateChange, [callback])

  • stateChange 为状态改变对象(该对象可以体现出状态的更改)
  • callback 是可选的回调函数, 它在状态更新完毕、界面也更新后才被调用

函数式:setState(updater, [callback])

  • updater 为返回 stateChange 对象的函数。
  • updater 可以接收到 state 和 props。

说明:

  • React 状态更新是异步的。下述代码打印的 count 值是上一次的值,而非更新后的。可在第二个参数回调中获取更新后的状态。
add = () => {
  this.setState({ count: this.state.count + 1 })
  console.log(this.state.count)
}

add = () => {
  this.setState({ count: this.state.count + 1 }, () => {
    console.log(this.state.count)
  })
}
  • callback 回调在 componentDidMount 钩子之后执行
  • 对象式写法可以看做函数式写法的语法糖
add = () => {
  this.setState((state, props) => {
    return { count: state.count + props.step }
  })
}

路由组件懒加载 lazyLoad

import React, { Component, lazy, Suspense } from 'react'
import Loading from './Loading'

// 通过 lazy 函数配合 import() 函数动态加载路由组件
// 路由组件代码会被分开打包
const Home = lazy(() => import('./Home')) 
const About = lazy(() => import('./About'))

export default Demo extends Component {
  render() {
    return (
      <div>
        <h1>Demo 组件</h1>
        <Link to="/home">Home</Link>
        <Link to="/about">About</Link>

        // 通过 <Suspense> 指定在加载得到路由打包文件前显示一个自定义 Loading 界面
        <Suspense fallback={Loading}>
          <Switch>
            <Route path="/home" component={Home}>
            <Route path="/about" component={About}>
          </Switch>
        </Suspense>
      </div>
    )
  }
}

React Hook

Hook 是 React 16.8.0 增加的新特性,让我们能在函数式组件中使用 state 和其他特性

State Hook

  • State Hook 让函数式组件也可拥有 state 状态。
  • 语法:const [Xxx, setXxx] = React.useState(initValue)
  • useState() 参数:状态初始化值;返回值:包含 2 个元素的数组,分别为状态值和状态更新函数
  • setXxx() 的 2 种用法:
setXxx(newValue)
setXxx(value => newValue)

注意!新状态值会覆盖原状态值!因此若有多个状态,只能多次调用 React.useState ,不能使用对象!

import React from 'react'

export default function StateHook() {
    const [count, setCount] = React.useState(0);
    const add = () => setCount(count + 1);
    const sub = () => setCount((count) => count - 1)
    return (
        <div>
            <h2>状态值:{count}</h2>
            <button onClick={add}>+1</button>
            <button onClick={sub}>-1</button>
        </div>
    )
}

Ref Hook

  • Ref Hook 可以在函数式组件存储或查找组件内的标签或其他数据
  • 语法:const refContainer = React.useRef()
  • 保存标签对象的容器,和 React.createRef() 类似,也是专人专用
import React from 'react'

export default function RefHook() {
  const myref = React.useRef()
  const submit = () => console.log('myref.current.value', myref.current.value)
  return (
    <div>
        <input type="text"  ref={myref}/>
        <button onClick={submit}>submit</button>
    </div>
  )
}

Effect Hook

  • Effect Hook 让我们能在函数式组件中执行副作用操作(就是模拟生命周期钩子)
  • 副作用操作:发送 Ajax 请求、定时器、手动更改真实 DOM
  • Effect Hook 可以模拟三个钩子:componentDidMountcomponentDidUpdatecomponentWillUnmount
  • React.useEffect 第一个参数 return 的函数相当于 componentWillUnmount ,若有多个会按顺序执行
// 语法
React.useEffect(() => {
  ...
  return () => {
    // 组件卸载前执行,即 componentWillUnmount 钩子
    ...
  }
}, [stateValue])

// 模拟 componentDidMount
// 第二个参数数组为空,表示不监听任何状态的更新
// 因此只有页面首次渲染会执行输出
React.useEffect(() => {
  console.log('DidMount')
  return () => {
    console.log('WillUnmount 1')
  }
}, [])

// 模拟全部状态 componentDidUpdate
// 若第二个参数不写,表示监听所有状态的更新
React.useEffect(() => {
  console.log('All DidUpdate')
  return () => {
    console.log('WillUnmount 2')
  }
})

// 模拟部分状态 componentDidUpdate
// 第二个参数数组写上状态,表示只监听这些状态的更新
React.useEffect(() => {
  console.log('Part DidUpdate')
  return () => {
    console.log('WillUnmount 3')
  }
}, [count, name])

// 若调用 ReactDOM.unmountComponentAtNode(document.getElementById('root'))
// 会输出 WillUnmount 1、2、3

Fragment

  • Fragment 标签本身不会被渲染成一个真实 DOM 标签,有点像 Vue 的 template
  • 用空标签也有相同效果,但是空标签不能传递任何属性,Fragment 标签可以传递 key 属性,遍历时候可用。
import React, { Component, Fragment } from 'react'

export default class Demo extends Component {
  render() {
    return (
      <Fragment key={1}>
        <input type="text" />
        <input type="text" />
      </Fragment>
    )

    // 或
    return (
      <>
        <input type="text" />
        <input type="text" />
      </>
    )
  }
}

Context (了解)

Context 是一种组件间通信方式,常用于祖父组件与子孙组件。实际开发一般不用,一般用 React-Redux

用法说明:

1) 创建Context容器对象:
const XxxContext = React.createContext()

2) 渲染子组时,外面包裹xxxContext.Provider, 通过value属性给后代组件传递数据:
<XxxContext.Provider value={数据}>
  子组件
</XxxContext.Provider>

3) 后代组件读取数据:

// 第一种方式:仅适用于类组件
// 声明接收context
static contextType = xxxContext
// 读取context中的value数据
this.context

//第二种方式: 可用于函数组件与类组件
<XxxContext.Consumer>
  {
    // value就是context中的value数据
    value => (
      ...
    )
  }
</XxxContext.Consumer>

举个例子:

// context.js

import React from 'react'
export const MyContext = React.createContext()
export const { Provider, Consumer } = MyContext
// A.jsx

import React, { Component } from 'react'
import B from './B.jsx'
import { Provider } from './context.js'

export default class A extends Component {
  state = { username: 'tom', age: 18 }

  render() {
    const { username, age } = this.state
    return (
      <div>
        <h3>A组件</h3>
        <h4>用户名是:{username}</h4>
        <Provider value={{ username, age }}>
          <B />
        </Provider>
      </div>
    )
  }
}
// B.jsx

import React, { Component } from 'react'
import C from './C.jsx'

export default class B extends Component {
  render() {
    return (
      <div>
        <h3>B组件</h3>
        <C />
      </div>
    )
  }
}
// C.jsx

import React, { Component } from 'react'
import { MyContext } from './context.js'

export default class C extends Component {
  static contextType = MyContext
  render() {
    const { username, age } = this.context
    return (
      <div>
        <h3>C组件</h3>
        <h4>
          从A组件接收到的用户名:{username},年龄:{age}
        </h4>
      </div>
    )
  }
}
// C.jsx 为函数式组件

import { Consumer } from './context.js'
export default function C() {
  return (
    <div>
      <h3>我是C组件</h3>
      <h4>
        从A组件接收到的用户名:
        <Consumer>{(value) => `${value.username},年龄是${value.age}`}</Consumer>
      </h4>
    </div>
  )
}

组件渲染优化

问题:

  • 只要调用 setState() ,即使没有修改状态,组件也会重新 render()
  • 只要父组件重新渲染,即使子组件没有使用父组件的状态,也会重新 render()

原因:

  • shouldComponentUpdate() 钩子默认总是返回 true

改进:

  • 只有组件的 stateprops 的数据发生改变时才重新渲染

方式:

  1. 手动重写 shouldComponentUpdate(nextProps, nextState) 的逻辑,只有数据发生改变才返回 true
  2. 使用 PureComponent ,它重写了 shouldComponentUpdate() , 只有 stateprops 数据有变化才返回 true

TIP

  • 它只是进行 stateprops 数据的浅比较, 如果只是数据对象内部数据变了, 返回 false。即对于引用数据类型,比较的是地址引用
  • 不要直接修改 state 数据, 而是要产生新数据
import React, { PureComponent } from 'react'

class Demo extends PureComponent {
  ...
  addStu = () => {
    // 不会渲染
    const { stus } = this.state
    stus.unshift('小刘')
    this.setState({ stus })

    // 重新渲染
    const { stus } = this.state
    this.setState({ stus: ['小刘', ...stus] })
  }
  ...
}

render props (插槽)

类似于 Vue 中的插槽技术

如何向组件内部动态传入带内容的结构(即标签或组件)?

  • Vue:插槽技术
  • React:
    • 使用 children props:通过组件标签体传入结构
    • 使用 render props:通过组件标签属性传入结构,可携带数据

children props 方式:

  • 组件标签体内容会存储到 this.props.children
  • 缺点:A 组件无法向 B 组件传递数据
import React, { Component } from 'react'

export default class Parent extends Component {
  render() {
    return (
      <div>
        <h3>Parent组件</h3>
        <A>
          <B />
        </A>
      </div>
    )
  }
}

class A extends Component {
  state = { name: 'tom' }
  render() {
    return (
      <div>
        <h3>A组件</h3>
        {this.props.children}
      </div>
    )
  }
}

class B extends Component {
  render() {
    return (
      <div>
        <h3>B组件</h3>
      </div>
    )
  }
}

render props 方式:

  • <A render={(name) => <B name={name} />} />
  • {this.props.render(name)}
import React, { Component } from 'react'

export default class Parent extends Component {
  render() {
    return (
      <div>
        <h3>Parent组件</h3>
        <A render={(name) => <B name={name} />} />
      </div>
    )
  }
}

class A extends Component {
  state = { name: 'tom' }
  render() {
    const { name } = this.state
    return (
      <div>
        <h3>A组件</h3>
        {this.props.render(name)}
      </div>
    )
  }
}

class B extends Component {
  render() {
    return (
      <div>
        <h3>B组件,{this.props.name}</h3>
      </div>
    )
  }
}

错误边界

TIP

错误边界(Error boundary):用来捕获后代组件错误,渲染出备用页面。

注意:只在生产环境(项目上线)起效

特点:

  • 只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误
  • 简单理解就是只能捕获后代组件生命周期钩子里面代码的错误
import React, { Component } from 'react'
import Child from './Child'

export default class Parent extends Component {
  state = {
    //用于标识子组件是否产生错误
    hasError: '',
  }

  // 当子组件出现错误,会触发调用,并携带错误信息
  static getDerivedStateFromError(error) {
    // render 之前触发
    // 返回新的 state
    return { hasError: error }
  }

  // 子组件产生错误时调用该钩子
  componentDidCatch(error, info) {
    console.log(error, info)
    console.log('此处统计错误,反馈给服务器')
  }

  render() {
    return (
      <div>
        <h2>Parent组件</h2>
        {this.state.hasError ? <h2>网络不稳定,稍后再试</h2> : <Child />}
      </div>
    )
  }
}

组件通信方式总结

  • props
  • 消息订阅发布:pubs-sub
  • 集中管理:Redux、dva 等
  • Context

推荐搭配:

  • 父子组件:props
  • 兄弟组件:消息订阅-发布、集中式管理
  • 祖孙组件(跨级组件):消息订阅-发布、集中式管理、conText(开发用的少,封装插件用的多即 React-Redux)

react-router6

官方文档

React Router 发布了三个不同的包:

  • react-router:路由核心库,提供许多组件、钩子
  • react-router-dom:包括了 react-router 所有内容,同时添加了用于 DOM 的组件,如 <BrowserRouter>
  • react-router-native:包括了 react-router 所有内容,同时添加了用于 ReactNative 的 API,如 <NativeRouter>

与 React Router 5.x 版本的区别:

  • 内置组件的变化:移除 <Switch/>,新增 <Routes/>……
  • 语法变化:component={About} 变成 element={<About/>}……
  • 新增 hook:useParamsuseNavigateuseMatch……
  • 官方明确表示推荐使用函数式组件

基本使用

安装 6 版本的 React Router。

npm install react-router-dom

index.js 文件引入 <BrowserRouter>

// 从 react-dom/client 引入 ReactDOM
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'

// React 18 的语法发生改变了
ReactDOM.createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
)

App.js 设置路由链接和注册路由。<Route caseSensitive> 属性用于指定匹配时是否区分大小写(默认为 false)

import { NavLink, Routes, Route } from 'react-router-dom'
import About from './components/About'
import Home from './components/Home'

// React 18 默认使用函数式组件了
export default function App() {
  return (
    <div>
      <NavLink to="/about">About</NavLink>
      <NavLink to="/home">Home</NavLink>
      <hr />
      <Routes>
        <Route path="/about" element={<About />}></Route>
        <Route path="/home" element={<Home />}></Route>
      </Routes>
    </div>
  )
}

BrowserRouter

<BrowserRouter> 用于包裹整个应用。

import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'

ReactDOM.createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
)

HashRouter

作用与 <BrowserRouter> 一样,但 <HashRouter> 修改的是地址栏的 hash 值。

6.x 版本中 <HashRouter><BrowserRouter> 的用法与 5.x 相同。

Routes

6 版本中移出了 <Switch>,引入了新的替代者:<Routes>

<Routes><Route> 要配合使用,且必须要用 <Routes> 包裹 <Route>

只要 <Navigate> 组件被渲染,就会修改路径,切换视图。可用于路由重定向

replace 属性用于控制跳转模式(push 或 replace,默认是 push)。

import { NavLink, Routes, Route, Navigate } from 'react-router-dom'
import About from './components/About'
import Home from './components/Home'

export default function App() {
  return (
    <div>
      <NavLink to="/about">About</NavLink>
      <NavLink to="/home">Home</NavLink>
      <hr />
      <Routes>
        <Route path="/about" element={<About />}></Route>
        <Route path="/home" element={<Home />}></Route>
        <Route path="/" element={<Navigate to="/about" />}></Route>
      </Routes>
    </div>
  )
}
import React, { useState } from 'react'
import { Navigate } from 'react-router-dom'

export default function Home() {
  const [sum, setSum] = useState(1)
  return (
    <div>
      <h1>Home</h1>
      {/* 根据sum的值决定是否切换视图 */}
      {sum === 1 ? <h4>sum的值为{sum}</h4> : <Navigate to="/about" replace={true} />}
      <button onClick={() => setSum(2)}>将sum变为 2</button>
    </div>
  )
}

useRoutes() 路由表

路由规则可以单独抽出一个模块。

// 路由表
// routes/index.js
import { Navigate } from 'react-router-dom'
import About from '../components/About'
import Home from '../components/Home'

const routes = [
    {
        path: '/about',
        element: <About />
    },
    {
        path: '/home',
        element: <Home />
    },
    {
        path: '/',
        element: <Navigate to="/home" />
    }
]

export default routes
// 引入路由表
// App.js
import { NavLink, useRoutes } from 'react-router-dom'
import routes from './routes'

export default function App() {
  // 生成路由规则
  const element = useRoutes(routes)

  return (
    <div>
      <NavLink to="/about">About</NavLink>
      <NavLink to="/home">Home</NavLink>
      <hr />
      {element}
    </div>
  )
}

Outlet 嵌套路由

  • 嵌套路由中,需要使用 <Outlet> 设置子路由的路由出口,即在何处渲染子路由。
  • 设置二级路由链接时,可以是 to="news"to="./news",但不能是 to="/news"

不使用路由表的嵌套路由:

// App.js
export default function App() {
  return (
    <div>
      <NavLink to="about">About</NavLink>
      <NavLink to="home">Home</NavLink>
      <hr />
      <Routes>
        <Route path="about" element={<About />} />
        <Route path="home" element={<Home />}>
          <Route path="news" element={<News />} />
          <Route path="message" element={<Message />} />
        </Route>
        <Route path="/" element={<Navigate to="about" />} />
      </Routes>
    </div>
  )
}

使用路由表的嵌套路由:

// 路由表
const routes = [
  {
    path: '/about',
    element: <About />,
  },
  {
    path: '/home',
    element: <Home />,
    // 定义二级路由,注意不要加 /
    children: [
      {
        path: 'news',
        element: <News />,
      },
      {
        path: 'message',
        element: <Message />,
      },
    ],
  },
  {
    path: '/',
    element: <Navigate to="/about" />,
  },
]
// Home 子组件
import React, { Fragment } from 'react'
import { NavLink, Outlet } from 'react-router-dom'

export default function Hello() {
  return (
    <Fragment>
      <h2>I am Hello!</h2>
      {/* 子路由链接 */}
      <NavLink to="news">News</NavLink>
      <NavLink to="message">Message</NavLink>
      <hr />
      {/* 子路由出口 */}
      <Outlet></Outlet>
    </Fragment>
  )
}

实现导航的 “高亮” 效果,6 版本不能直接指定高亮类名,需要通过函数返回。该函数传入一个对象,类似于 {isActive: true} 标识路由是否被激活。

默认情况下,当 Home 的子组件匹配成功,Home 的导航也会高亮,end 属性可移除该效果。

// NavLink 默认类名是 active,下面是指定自定义类名

//自定义样式
<NavLink
    to="login"
    className={({ isActive }) => {
        console.log('home', isActive)
        return isActive ? 'base MyClass' : 'base'
    }}
>about</NavLink>

// 默认情况下,当 Home 的子组件匹配成功,Home 的导航也会高亮
// 当 NavLink 上添加了 end 属性后,若 Home 的子组件匹配成功,则 Home 的导航没有高亮效果。
<NavLink to="home" end >home</NavLink>

路由传参

传递 params参数

注册路由时声明 params 参数,和 React Router 5 一样。

export default function App() {
  return (
    <div>
      <Routes>
        <Route path="home" element={<Home />}>
          <Route path="message" element={<Message />}>
            <Route path="detail/:id/:name/:age" element={<Detail />} />
          </Route>
        </Route>
      </Routes>
    </div>
  )
}

传递参数

<Link to={`detail/${item.id}/${item.name}/${item.age}`}>{item.name}</Link>

使用 useParams() 接收 params 参数。useParams() 返回一个参数对象。

import React from 'react'
import { useParams, useMatch } from 'react-router-dom'

export default function Detail() {
  // 解构赋值
  const { id, name, age } = useParams()
  return (
    <div>
      <li>id:{id}</li>
      <li>name:{name}</li>
      <li>age:{age}</li>
    </div>
  )
}

传递 search参数

和 5 版本一样,正常注册路由即可。

<Route path="detail" element={<Detail />} />

传递参数。

<Link to={`detail?id=${item.id}&name=${item.name}&age=${item.age}`}>{item.name}</Link>

使用 useSearchParams() 接收参数。该方法返回一个包含两个元素的数组:search 参数和修改 search 参数的方法。

import React from 'react'
import { useSearchParams } from 'react-router-dom'

export default function Detail() {
  // 数组的解构赋值
  const [searchParams, setSearchParams] = useSearchParams()
  // 需要调用 get() 方法获取对应的参数值
  const id = searchParams.get('id')
  const name = searchParams.get('name')
  const age = searchParams.get('age')

  function change() {
    setSearchParams('id=666&name=Lily&age=888')
  }

  return (
    <div>
      <li>id:{id}</li>
      <li>name:{name}</li>
      <li>age:{age}</li>
      <button onClick={change}>Change search params</button>
    </div>
  )
}

传递 state 参数

和 5 版本一样,正常注册路由即可。

<Route path="detail" element={<Detail />} />

传递参数,这里相较于 5 版本有所不同,不必写到一个对象里面。

<Link to="detail" state={{ id: item.id, name: item.name, age: item.age }}>
  {item.name}
</Link>

使用 useLocation() 接收参数。该方法返回路由组件的 location 对象,就是 5 版本路由组件的 location 属性,其中包含 state 参数。

import { useLocation } from 'react-router-dom'

export default function Detail() {
  // 连续解构赋值
  const {
    state: { id, name, age },
  } = useLocation()

  return (
    <div>
      <li>id:{id}</li>
      <li>name:{name}</li>
      <li>age:{age}</li>
    </div>
  )
}

useNavigate() 编程式路由导航

useNavigate() 返回一个函数,调用该函数实现编程式路由导航。函数有两种参数传递方式。

第一种方式传递两个参数:路由和相关参数。参数只能设置 replacestate想要传递 paramssearch 参数直接在路由带上

第二种方式传递数字,代表前进或后退几步。

import React, { useState } from 'react'
import { Outlet, useNavigate } from 'react-router-dom'

export default function Message() {
  const [list] = useState([
    { id: 1, name: 'Bruce', age: 33 },
    { id: 2, name: 'You', age: 3 },
    { id: 3, name: 'React', age: 333 },
  ])

  const navigate = useNavigate()

  function showDetail(item) {
    navigate('detail', {
      replace: true,
      state: {
        id: item.id,
        name: item.name,
        age: item.age,
      },
    })
  }

  function back() {
    navigate(1)
  }

  function forward() {
    navigate(-1)
  }

  return (
    <div>
      <ul>
        {list.map((item) => {
          return (
            <li key={item.id}>
              <button onClick={() => showDetail(item)}>查看详情</button>
              <button onClick={back}>后退</button>
              <button onClick={forward}>前进</button>
            </li>
          )
        })}
      </ul>
      <Outlet></Outlet>
    </div>
  )
}

Other Hooks(了解)

useMatch()

返回路由组件的 match 数据,即 5 版本中的 match 属性。

必须传入该组件对应的路由规则才能正确返回,否则返回 null

// Detail.jsx
import { useParams, useMatch } from 'react-router-dom'

export default function Detail() {
  const match = useMatch('home/message/detail/:id/:name/:age')
  console.log(match)
  return (
    <div>
      <li>id</li>
    </div>
  )
}

/*
params: {id: '1', name: 'tom', age: '33'}
pathname: "/home/message/detail/1/tom/33"
pathnameBase: "/home/message/detail/1/tom/33"
pattern: {path: 'home/message/detail/:id/:name/:age', caseSensitive: false, end: true}
*/

useInRouterContext

如果组件在 <Router> 的上下文中呈现,则 useInRouterContext 钩子返回 true,否则返回 false。即组件有没有被包裹在 <BrowserRouter> 这种东西里面。这个对第三方组件库有用处。

useNavigationType

返回当前的导航类型(用户是如何来到当前页面的)。

返回值:POPPUSHREPLACE

POP 是指在浏览器中直接打开了这个路由组件(刷新页面)。

useOutlet

用来呈现当前组件中渲染的嵌套路由。

const result = useOutlet()
console.log(result)
// 如果嵌套路由没有挂载,则返回 null
// 如果嵌套路由已经挂载,则展示嵌套的路由对象

useResolvedPath

给定一个 URL 值,解析其中的:pathsearchhash 值。

const res = useResolvedPath('/user?id=001&name=tom#React')
console.log(res)

/*
hash: '#React'
pathname: '/user'
search: '?id=001&name=tom'
*/
------ 本文结束,感谢您的阅读 ------
本文作者: 贺刘芳
版权声明: 本文采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。