目录

一、React在HTML里的运用

二、React框架的常用操作

项目打包

1、JSX基础语法规则

2、state数据的使用

state的描述:

useState():

注意事项:

在类组件中的state:

3、生命周期

4、数据的双向绑定与Ref

通过ref获取并操作原生dom:

5、PropTypes验证规则

6、React里的插槽

7、错误边界

8、直接操作refs元素

9、高阶组件的运用案例

10、性能优化

11、Hook生命周期钩子的使用

12、useMemo

13、React函数里的事件

14、Portal在body中显示组件

15、WebStorm里的快捷创建命令

16、CSS模块化使用

17、useEffect

18、通过useEffect实现防抖案例

19、useReducer

20、React.memo

21、useCallback

22、自定义钩子函数

23、React.forwardRef

24、useImperativeHandle

25、useLayoutEffect和useInsertionEffect

26、useDebugValue

三、组件之间的传值

1、Props父子组件之间传值

2、子向父传值

3、context实现跨层级通信

4、createContext和useContext的使用

方法一利用Consumer标签获取:

方法二利用useContext钩子获取:

Provider的使用:

结合state一起使用:

四、网络请求框架使用

五、React路由的使用

常用API

声明式导航

编程式导航

编程式跳转

六、Anti Desgin的使用实现UI界面

七、Redux中央仓库的使用

1、新建src/redux/index.js用于存放redux的文件

2、在src/index.js里引入

3、可以通过logger查看redux日志

4、Redux的模块化管理

5、注意事项

6、总结Redux使用的完整步骤

八、Redux Tool使用总结


一、React在HTML里的运用

React在HTML里的使用核心就是导入3个依赖:

<!--React核心库-->
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<!--React-dom用于支持react操作DOM-->
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!--引入babel用于将jsx转为js-->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

案例代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>test</title>
</head>
<body>
<div id="root"></div>
</body>
<!--React核心库-->
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<!--React-dom用于支持react操作DOM-->
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!--引入babel用于将jsx转为js-->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<!--这里一定要用这个type,让babel翻译script标签中的代码-->
<script type="text/babel">
    //1、创建虚拟Dom
    // 方式一  JSX
    const VDOM = (
        <h1 id="title">
            <span>Hello,React</span>
        </h1>
    )
    //方式二 js语法创建DOM
    const VDOM2 = React.createElement("h1", {id: "title"}, React.createElement("span", {}, "Hello,React"))

    //2、将虚拟DOM转成真实DOM,并插入页面 React17
    // ReactDOM.render(VDOM, document.getElementById("root"))

    //3、React18后,推荐使用这种
    //获取根元素
    const root = ReactDOM.createRoot(document.getElementById("root"))
    //将元素在根元素中显示
    /**
     * root.render()
     * 根元素里说有的内容都会被删除,替换为react元素
     * 当重复调用render时,react会将两次渲染的结果进行比较,
     * 它确保只修改那些发生变化的元素,对DOM做最小的修改
     */
    root.render(VDOM2)
    // root.render(VDOM)
</script>
</html>

运行效果:

执行流程如下:

React通过虚拟DOM,将React元素和原生DOM,进行映射,虽然操作的是虚拟DOM,但这些操作都会在真实DOM中体现。

虚拟DOM的好处

1、降低API复杂度

2、解决兼容性问题

3、提升性能(减少DOM的不必要操作)

每当我们调用root.render()时,页面就会发生重新渲染:

React通过diffing算法,将新元素和旧元素进行比较,通过比较找到发生变化的元素,并且只对发生变化的元素进行修改,没发生变化的不予处理。

二、React框架的常用操作

脚手架创建:

使用详情查看官网,这里只记录我的笔记:React 官方中文文档 – 用于构建用户界面的 JavaScript 库

npx create-react-app projectName

项目打包

 打包时为了路径对应需要到package.json中添加"homepage":"./"这一属性。

1、JSX基础语法规则

案例代码:object对象不能执行渲染,arr可以通过map函数进行遍历输出。

import React from 'react';
 function App () {
     const num=16
     const bool=true
     const name="string"
     const arr=[1,2,3,4,"Jack",true]
     const obj={name:"Mary",age:12}
     
     return(
         <div style={{marginLeft:"20px"}}>
             <div>num:{num}</div>
             <div>bool:{bool}</div>
             <div>name:{name}</div>
             <div>arr:{arr}</div>
             <div>arr遍历: {
                 arr.map((item,index)=>(
                     <p key={index}><span color={"red"}>{item}</span></p>
                 ))
             }</div>
             {/*对象不能直接打印*/}
             {/*<div>obj:{obj}</div>*/}
             <div>obj.name:{obj["name"]}</div>
         </div>
     )
 }

 export default App;

效果:

2、state数据的使用

state的描述:

普通数据在组件中发生改变,页面是无法感知的,也就是说普通数据在js里虽然改变了,但在页面上是没有重新更新渲染的功能的。

state只有当前组件可以访问。state是可变的,当state里的数据发生变化时,对应页面显示的变量也会随之重新渲染刷新。

state相当于一个变量,React会监控这个变量的变化,当state发生变化时,会触发组件的重新渲染,使得我们的修改可以在页面中呈现。

函数组件使用钩子函数useState设置,类组件通过this.state进行设置。

修改state时,state的值相同,React在一些情况下也会继续执行当前组件的渲染,但这次渲染不会触发其他子组件的渲染。(通常发生在值第一次相同时,这种情况基本不会对性能有什么影响)

修改state时,state的值相同,非第一次比较相同值,都不会进行渲染组件。

修改state时,若state的值不相同,则会发生重新渲染。

useState():

useState({your obj})返回值是一个数组,第一个参数是你设置的初始值,第二个元素是一个函数(通常命名为setXxx,这个函数用来修改state,调用其修改state后会触发组件的重新渲染)。

注意:直接修改state的值,虽然变量值改变了,但组件并不会重新渲染。所以需要通过setXxx的函数来进行设置,并重新渲染

useState本质就是在通过deff算法比对后,判断发生变化后,再调用了root.render()来进行更新渲染。(若先后设置的值都相同,则不会调用render来渲染更新)

注意事项:

1、只有state的值发生改变时,才会发生重新渲染。

2、当state的值是一个对象时,修改时使用新对象去替换已有对象

(如果直接修改state对象,由于对象还是那个对象【对象地址没发生改变】,所以不会生效【更新渲染】)

解决方案:

1)通过Object.assign来创建浅拷贝的新对象

    user={name:"孙悟空",age:12}
    const newUser=Object.assign({},user)  //浅拷贝 将user的所有属性赋值到新对象里
    newUser.name="猪八戒"
    setUser(newUser)

2)通过解构重构来实现

    user={name:"孙悟空",age:12}
    setUser({...user,name:"猪八戒"})  //解构再重构成新对象

这种两种情况下,每次生成的newUser都是新对象,地址都不同,所以每次都会重新渲染。

3、setState修改的不是当前的state,而是它修改的组件下次渲染的state;

4、setState修改state数据是异步执行的。

(所以,当调用setState去修改一个state时,一定要注意,有可能出现计算错误的情况。

为了避免这种错误,我们可以通过给setState传递回调函数的形式来修改state。

解决方案: setState中,回调函数的返回值将成为新的state值,回调函数执行时,React会将最新的state值作为参数传递

核心:

setCounter((prevCounter)=>{  //preCounter为counter之前的最新值,由react管理返回

        return prevCounter + 1

})

简写:

setCounter(prevCounter=> prevCounter + 1)

 const [counter, setCounter] = useState(1)
    const addHandler = () => {
        setTimeout(() => {
            setCounter((prevCounter) => {
                //setState中,回调函数的返回值将成为新的state值,
                //回调函数执行时,React会将最新的state值作为参数传递
                return prevCounter + 1
            })
        }, 1000)
    }

5、多次调用重复调用setState,只会执行最后一次

参考文档:用 State 响应输入 – React 中文文档

在类组件中的state:

使用this.state获取state,使用this.setState修改state

上面聊到的异步问题,在类组件中这样写:

    this.setState(prevState=>{
        return {
            count: prevState.count+1
        }        
    })

注意:当state里有多个属性时,通过setState修改其中一个属性,且该属性是state的直接属性,仅会修改该属性,其他属性不会被修改。

     state={
        count:0,
        test:"哈哈"
    }
    
    clickHandler=()=>{
        setState({count: 1}) //这里只会修改count属性,不会修改test属性
    }

这段代码修改后的state是{count:1,test:"哈哈“}而不是{count:1}

非直接属性修改时

    state = {
        count: 0,
        test: "哈哈",
        obj: {
            name: "孙悟空",
            age: 18
        }
    }

    clickHandler = () => {
        setState({
            obj: {name: "沙和尚"}
        })
    }

修改后,state里的值是

state = {
    count: 0,
    test: "哈哈",
    obj: {
        name: "沙和尚"
    }
}

此时obj的age属性并没有被保留,而是直接被{name: "沙和尚"}对象替换了。

3、生命周期

发送网络请求一般在componentDidMount里执行,

销毁组件开销一般在componentWillUnmount里执行。

4、数据的双向绑定与Ref

方法一: 

class Test extends Component {
    
    constructor (props, context) {
        super(props, context)
        this.state={
            val: "233"
        }
    }
    
    changeVal=(e)=>{
        console.log(e)
        this.setState({
            val: e.target.value
        })
        console.log(e.target.value)
    }
    
    render () {
        return (
            <div>
                <input type="text" onChange={this.changeVal} value={this.state.val} />
            </div>
        )
    }
}

方法二:通过ref实现

class Test extends Component {
    
    constructor (props, context) {
        super(props, context)
        this.state={
            val: "233"
        }
        //1、在构造函数里创建ref的语法
        this.myRef=React.createRef()
    }
    
    search=()=>{
        //3、获取DOM元素
        console.log(this.myRef.current.value)
    }
    
    render () {
        return (
            <div>
                {/*2、绑定到元素上去*/}
                <input type="text" ref={this.myRef} onChange={this.search}/>
            </div>
        )
    }
}
通过ref获取并操作原生dom:

在原生js中,获取对象时我们常用document.getElementById这些方法来获取并操作Dom原生,document.getElement之类的方法会对页面进行查询再获取,在React里使用时,有时并非最优性能的解。这里我们可以使用ref来实现。

函数组件中的使用:

 一般使用useRef()这个钩子函数, 使用方法查看官方文档:

useRef – React 中文文档

使用React获取DOM对象有以下几个步骤:

1、创建一个存储DOM的容器;

const myRef=useRef() //创建一个存储DOM对象的容器

2、在DOM标签中设置ref属性与useRef()容器绑定;

<div id="dom" ref={myRef}>xxx</div>

React会自动将当前元素的DOM对象,设置为容器的current属性。

    <div id="dom" ref={myRef}>xxx</div>


    const myRef=useRef() //创建一个存储DOM对象的容器
    const log=()=>{
        const dom=document.getElementById("dom")
        console.log(myRef.current)  //DOM元素存在ref的current属性里
        console.log(myRef.current===dom)   //true    证明React获取的DOM和document获取的是同一个DOM对象
        myRef.current.innerText="嘻嘻"  //修改成功
    }

- useRef() 返回的就是一个JS对象

- {current: undefined}

- 甚至你直接把useRef() 改为 {current: undefined} 都能正常运行

- 直接创建对象取接受和使用useRef取接受的区别:

1)我们创建的对象,每次渲染都会重新创建一个新对象;

2)useRef创建对象,可以确保每次渲染获取到的都是同一个对象。

- 当你需要一个对象不会因为组件的重新渲染而改变时,你可以使用useRef()

类组件中的使用:

1、创建DOM容器对象

myRef=React.createRef()

2、获取DOM元素ref

<div id="dom" ref={this.myRef}>xxx</div>

其他特性和函数组件里的描述基本一致。

类组件的refs的操作参考:

Refs and the DOM – React

5、PropTypes验证规则

参考:

使用 PropTypes 进行类型检查 – React

6、React里的插槽

参考:

组合 vs 继承 – React

7、错误边界

避免一错全不渲染的情况

参考:错误边界 – React

8、直接操作refs元素

函数组件里使用:

参考:Refs 转发 – React

9、高阶组件的运用案例

参考:高阶组件 – React

实现组件加载时间的复用

import React from "react"

export default function showTime (Comp) {
    return class extends React.Component {
        constructor (props, context) {
            super(props, context)
            this.state = {
                startTime: new Date().getTime(),
                loadingTime: 0
            }
        }
        
        componentDidMount () {
            let endTime = new Date().getTime()
            this.setState({
                loadingTime: endTime - this.state.startTime
            })
        }
        
        render () {
            return (
                <Comp loadingTime={this.state.loadingTime}/>
            )
        }
    }
}

这样便实现了高阶组件的复用,需要使用这个功能时,只需要调用该函数对象进行功能追加即可。

10、性能优化

参考:性能优化 – React

1、新版本以后Component用PureComponent,

PureComponent自带state和props的检查,但其中有变化时才重写渲染,否则不重新渲染,提升性能。

函数组件中的优化:相当于pureComonent

React.memo()生命周期钩子,相当于pureCompnent会在props和state变化时才发生更新。

案例代码:

import React from "react"

const componentTwo = React.memo((props) => {
    return (
        <div>
        
        </div>
    )
})

export default componentTwo

2、组件用完后资源记得释放

11、Hook生命周期钩子的使用

钩子函数只能用于函数组件和自定义钩子。不能直接放到其他个别方法里使用,会直接报错。

参考:Hook API 索引 – React

使用函数组件的效率一般会比类组件效率高一些,但在函数组件(无状态组件)中又没有state等属性,所以这里诞生了Hook为函数组件添加state和生命周期等元素。

Hook 简介 – React

案例一: useState  修改状态

import React,{useState} from "react"

function FunComponentOne () {
    //定义一个变量,初始值为0,可通过setCount方法修改值
    //第一个位置变量名,第二个位置方法名:修改变量的方法
    const [count,setCount]=useState(0)
    
    return (
        <div>
            <h2>{count}</h2>
            <button onClick={()=>setCount(count+1)}>+</button>
        </div>
    )
}

export default FunComponentOne

案例二:useEffect  生命周期钩子

import React,{useState,useEffect} from "react"

function FunComponentOne () {
    
    //定义一个变量,初始值为0,可通过setCount方法修改值
    //第一个位置变量名,第二个位置方法名
    const [count,setCount]=useState(0)
    
    //第二个参数为空,相当于生命周期钩子:componentDidMount+componentDidUpdate
    //加载完成和数据修改都会走该函数
    useEffect(() => {
        console.log("=======================")
    })
    
    //第二个参数为空数组[],相当于componentDidMount,可以用于网络请求
    useEffect(() => {
        console.log("++++++++++++++++++++++++++++")
    }, [])
    
    //第二个参数可以传入有值的数组,当数组里的变量修改,会调用该函数
    useEffect(() => {
        console.log("!!!!!!!!!!!!!!!!!!!!!!!!!")
    }, [count])
    
    //当第二个参数为空数组[],且有返回函数时相当于componentWillUnMount
    //一般用于销毁主键
    useEffect(() => {
        return function clearUp(){
            console.log("clearUp")
        }
    },[])
    
    
    return (
        <div>
            <h2>{count}</h2>
            <button onClick={()=>setCount(count+1)}>+</button>
        </div>
    )
}

export default FunComponentOne

案例3: Hook reducer类似于升级版的useState

参考:Hook API 索引 – React

案例4:自定义Hook 降低耦合度

参考:自定义 Hook – React

可以将频繁调用的hook定义到自己的hook里,注意要用use开头

12、useMemo

相当于Vue里的Computed

与useCallback十分相似,useCallback是用来缓存函数对象,useMemo是用来缓存函数的执行结果。

不使用useMemo的执行效果:

const sum = (a, b) => {
    console.log("求和执行了")
    return a + b
}

function App() {
    console.log(sum(1,2))
    console.log(sum(1,2))

    return (
        <>
           <div></div>
        </>
    );
}

效果:每次调用都会重新执行。

使用了useMemo:

const sum = (a, b) => {
    console.log("求和执行了")
    return a + b
}

function App() {
    let a=1
    let b=2

    // 每次组件渲染时,都会执行
    // useMemo用于缓存函数的执行结果
    const result = React.useMemo(() => {
        return sum(a, b)
    },[])

    console.log(result)
    console.log(result)

    return (
        <>
            <div />
        </>
    );
}

效果:函数只执行了一次,对于开销很大的函数,使用useMemo可以很好地改善性能。

当useMemo里传入变量时:

const sum = (a, b) => {
    console.log("求和执行了")
    return a + b
}

function App() {
    const [count, setCount] = useState(1)

    let b = 2

    // 每次组件渲染时,都会执行
    // useMemo用于缓存函数的执行结果
    const result = React.useMemo(() => {
        return sum(count, b)
    }, [])

    useEffect(() => {
        console.log("count:", count)
        console.log(result)
        console.log(result)
    }, [count])

    return (
        <>
            <div>
                <button onClick={() => setCount(prev => prev + 1)}>a加1</button>
            </div>
        </>
    );
}

打印:可以看到每次执行都是第一次的结果,变量改变也没有重新缓存result。

解决上面问题的方法:

 const result = React.useMemo(() => {
        return sum(count, b)
    }, [count])  // 第二个参数数组中传入对应变量,当变量发生变化时,会重新调用useMemo进行结果缓存更新。

对于上述情况,如果修改过于频繁,基本使用不到缓存效果,就不推荐这种情况使用了。

另外,useMemo也可以像React.memo一样返回组件缓存:

function App() {
    const el = useMemo(() => {
        return <div><p>hello</p></div>
    }, [])

    return (
        <>
            <div>
                {el}
            </div>
        </>
    );
}

13、React函数里的事件

在React中,函数接收到DOM元素触发的事件参数event不再是DOM对象,而是由react已封装好的事件对象。

像原生JS里阻止默认行为的return false写法不会再生效。

React里事件的常用阻止方法:

event.preventDefault();   //取消默认行为   类似原生js里的return false的操作效果,如阻止a标签跳转

event.stopPropagation();    //取消事件冒泡   取消alter操作的弹出

14、Portal在body中显示组件

通过portal可以将组件渲染到页面中的指定位置。

使用方法:

1、在index.html中添加一个新元素

2、修改组件的渲染方式:

1)通过ReactDOM.createPortal来作为返回值创建元素;

2)参数:1. JSX (修改前return后的代码);2.目标位置(DOM元素)

使用场景:一些需要层级较高的组件的显示,如对话框。

实现参考官方文档:

createPortal – React 中文文档

15、WebStorm里的快捷创建命令

rsi  函数组件(带props)

rsc 函数组件(不带props)

rcc 类组件

16、CSS模块化使用

1)创建一个 xxx.module.css文件;

2)在对应组件中引入css;

import styles from "../styles/test.module.css";

3)通过引入的参数来设置类型

class模块可动态地设置唯一的class值,避免不同组件类名重复的问题。

17、useEffect

useEffect为函数组件里的生命周期钩子,在组件渲染完毕后执行。用于处理一些具有副作用影响的代码和生命周期里的执行,如:

以下代码在执行时,会报too many render的错误,原因是setState函数是一个有副作用的函数,当调用setState时,由于值不相同,页面会进行重新渲染,每次重新渲染又会重新执行该逻辑,从而陷入死循环。

const ChildTwo=()=>{
    const [data,setData]=useState(1)

    setData(233)

    return <div><p>hello</p></div>
}

不用useEffect的解决思路,可以通过异步函数来解决:

const ChildTwo = () => {
    const [data, setData] = useState(1)

    setTimeout(() => {
        setData(233)
    }, 0)

    return <div><p>hello</p></div>
}

但这样并不规范,通过useEffect可以轻松解决。

const ChildTwo = () => {
    const [data, setData] = useState(1)
    
    useEffect(() => {
        setData(233)
    })

    return <div><p>hello</p></div>
}

Effect调用的时机:

1、默认,每次组件重新渲染都会调用,并且是每次渲染完成后调用;

2、useEffect(()=>{})在只有第一个函数参数时,会在页面完成渲染后和页面更新渲染完成后执行,相当于Compent组件里:componentDidMount 和 componentDidUpdate 一起的生命周期;

//1
useEffect(()=>{
    console.log(props.number)
    setNumber(props.number)
}) //所有更新都执行

3、useEffect(()=>{},[])可以传入第二个参数数组为空数组,此时只会在初始化页面渲染完毕时执行,相当于componentDidMount 的生命周期,只在第一次 render 的时候执行func;

//2
useEffect(()=>{
  console.log(props)
},[]) //仅在挂载时候执行

4、useEffect(()=>{},[a,b,c])可以在第二个参数数组中传入需要监听的参数,当监听的参数发生变化时,就会重新调用该func,即在初始化渲染完毕和参数更新发生重新渲染完毕后执行,相当于Compent组件里:componentDidMount 和 componentDidUpdate 一起的生命周期,但只监听对应数组里的参数,setState等方法不要放如监听数组,设不设置都一样;

//3
useEffect(()=>{
  console.log(count)
},[count]) //count更新时执行

//4
const Asynchronous : React.FC=({number})=>{
    const [number2,setNumber2] = useState(number);
    useEffect(()=>{
        console.log(number)
        setNumber2(number)
    },[number,setNumber2]) //监听props对象number的更改
    //setNumber2是useState返回的setter,所以不会在每次渲染时重新创建它,因此effect只会运行一次

        //所以,尽量不要把stateState设置到变化的依赖里,浪费代码空间
}

5、useEffect里可以数组return func()来作为组件卸载时的执行函数:类似于Compent组件的componentWillUnmount生命周期

const timer = setInterval(() => {
setCount(count + 1)
}, 1000)
// useEffect方法的第一个参数是一个函数,函数可以return一个方法,这个方法就是在组件销毁的时候会被调用
useEffect(() => {
    return () => {
        clearInterval(timer)
    }
}, [])

18、通过useEffect实现防抖案例

const ChildTwo = () => {
    const [data, setData] = useState("")

    console.log("渲染了Child")

    useEffect(() => {
        console.log(data)

        return () => {
            console.log("清理的函数执行了")
        }
    }, [data])

    const onChangeInput = e => {
        setData(e.target.value)
    }

    return <div>
        <input type="text" value={data} onChange={onChangeInput}/>
    </div>
}

打印:通过下图你可以发现不同区域的执行顺序

添加防抖操作:

const ChildTwo = () => {
    const [data, setData] = useState("")

    console.log("渲染了Child")

    useEffect(() => {
        const timer=setTimeout(()=>{
            console.log("effect触发了")
            console.log(data)
        },1000)

        return () => {
            clearTimeout(timer)
        }
    }, [data])

    const onChangeInput = e => {
        setData(e.target.value)
    }

    return <div>
        <input type="text" value={data} onChange={onChangeInput}/>
    </div>
}

执行结果:输入了多次setTimeout里的操作只执行了一次。

19、useReducer

useReducer(reducer, initialArg, init?)

const [state, dispatch] = useReducer(reducer, initialArg, init?)

reducer:需要一个整合函数,对当前state里的所有操作都应该在该函数中定义。该函数的返回值会成为state的新值。reducer执行时会收到两个参数,第一个是当前最新的state,第二个参数为dispatch被调用时传入的参数。

initialArg:state的初始值,作用和useState()里的值是一样的。

返回值:返回的是一个数组,第一个参数是state用于获取state里的值;第二个参数是state修改的派发器,具体修改行为由另一个函数(reducer)执行。

 官网文档:useReducer – React 中文文档

案例一:

const ChildTwo = () => {
   const [count,countDispatch]=useReducer(()=>{
       console.log("reducer执行了!")
   },1)

    return <div>
       <button onClick={()=>countDispatch()}>点击</button>
    </div>
}

打印:点击触发dispatch派发器,派发器调用reducer函数里的逻辑。

案例二:

在reducer里传入参数:

const ChildTwo = () => {
    const [count, countDispatch] = useReducer((state, action) => {
        if (action && action.num) {
            switch (action.type) {
                case "increase":
                    return state + action.num
                case "decrease":
                    return state - action.num
            }
        }
        return state
    }, 1)

    useEffect(() => {
        console.log(count)
    }, [count])

    return <div>
        <p>{count}</p>
        <button onClick={() => countDispatch({type: "increase", num: 10})}>加10</button>
        <button onClick={() => countDispatch({type: "decrease", num: 1})}>减1</button>
    </div>
}

实现效果:reducer第一个参数为最新的state数据。也可以在dispatch中传入参数action,reducer可以通过第二个参数接收对应传值的action来进行相关逻辑执行。

注意:为了避免Reducer在组件里声明会重复创建,所以尽量在组件外创建Reducer。

案例二更规范的写法:

const countReducer = (state, action) => {
    if (action && action.num) {
        switch (action.type) {
            case "increase":
                return state + action.num
            case "decrease":
                return state - action.num
        }
    }
    return state
}

const ChildTwo = () => {
    const [count, countDispatch] = useReducer(countReducer, 1)

    useEffect(() => {
        console.log(count)
    }, [count])

    return <div>
        <p>{count}</p>
        <button onClick={() => countDispatch({type: "increase", num: 10})}>加10</button>
        <button onClick={() => countDispatch({type: "decrease", num: 1})}>减1</button>
    </div>
}

20、React.memo

官方文档:https://zh-hans.react.dev/reference/react/memo

React.memo是一个高阶组件,它接收另一个组件作为参数,会返回一个包装过的新组件,包装后的新组件就会具有缓存作用。

包装后,只有组件的props发生变化,才会触发组件的重新渲染,否则总是返回缓存中的结果。

案例一:

function App() {
    console.log("App发生渲染")

    const [count, setCount] = useState(1)

    return (
        <>
            <div className="App">
                <Son/>
                <p>{count}</p>
                <button onClick={() => setCount(count => ++count)}>点击加1</button>
            </div>
        </>
    );
}

const Son = () => {
    console.log("Son发生渲染")
    return <div>
        <p>son</p>
        <GrandSon/>
    </div>
}

const GrandSon = () => {
    console.log("GrandSon发生渲染")

    return <div><p>GrandSon</p></div>
}

实现效果:每次修改App里的数据,子孙组件都发生了改变

案例二:

对案例一进行优化:

function App() {
    console.log("App发生渲染")

    const [count, setCount] = useState(1)

    return (
        <>
            <div className="App">
                <Son/>
                <p>{count}</p>
                <button onClick={() => setCount(count => ++count)}>点击加1</button>
            </div>
        </>
    );
}

const Son = React.memo(() => {
    console.log("Son发生渲染")
    return <div>
        <p>son</p>
        <GrandSon/>
    </div>
})

const GrandSon = () => {
    console.log("GrandSon发生渲染")

    return <div><p>GrandSon</p></div>
}

实现效果:可以看到,当Son组件被memo包裹时,Son一系列数下的组件都被缓存优化到了。每次修改父组件,子孙组件都未重新渲染。

案例三:

在App组件中,像子组件Son传入修改参数的setCount函数。

function App() {
    console.log("App发生渲染")

    const [count, setCount] = useState(1)

    const addOne = () => {
        setCount(count => ++count)
    }

    return (
        <>
            <div className="App">
                <Son addOne={addOne}/>
                <p>{count}</p>
                <button onClick={addOne}>点击加1</button>
            </div>
        </>
    );
}

const Son = React.memo(({addOne}) => {
    console.log("Son发生渲染")
    return <div>
        <p>son</p>
        <button onClick={addOne}>Son点击加1</button>
        <GrandSon/>
    </div>
})

const GrandSon = () => {
    console.log("GrandSon发生渲染")

    return <div><p>GrandSon</p></div>
}

实现效果:当Son组件中调用了父组件中的函数时,你会发现React.memo失效了,此时,子孙组件仍然会被重新渲染。因为App组件发生改变重新渲染,addOne函数也会重新定义,此时传入Son组件里的函数就相当于更新了,会使得Son进行重新渲染。但问题是Son里的内容并没有改变,有什么方法可以解决这种情况的问题呢?这就要用到下面介绍的useCallback钩子了。

21、useCallback

useCallback是一个钩子函数,用来创建React中的回调函数。创建的回调函数不会总在组件重新渲染时重新创建。简单来说,就是对回调函数做了一层缓存。

const cachedFn = useCallback(fn, dependencies)

useCallback的第一个参数是一个回调函数,第二个参数是依赖数组(当依赖数组中的变量发生变化时,回调函数才会重新创建;如果不指定依赖数组,回调函数每次都会重新创建,失去意义)。

注:如果使用时,不传第二个参数,函数仍然会在每次渲染时重新创建,和没使用没什么区别。

案例四:

function App() {
    console.log("App发生渲染")

    const [count, setCount] = useState(1)

    const addOne = useCallback(() => {
        setCount(count => ++count)
    },[])  // 这里的函数只会在组件初始化时创建,更新时不会再次创建

    return (
        <>
            <div className="App">
                <Son addOne={addOne}/>
                <p>{count}</p>
                <button onClick={addOne}>点击加1</button>
            </div>
        </>
    );
}

const Son = React.memo(({addOne}) => {
    console.log("Son发生渲染")
    return <div>
        <p>son</p>
        <button onClick={addOne}>Son点击加1</button>
        <GrandSon/>
    </div>
})

const GrandSon = () => {
    console.log("GrandSon发生渲染")

    return <div><p>GrandSon</p></div>
}

实现效果:React.memo案例三中的问题解决了

案例五:

function App() {
    console.log("App发生渲染")

    const [count, setCount] = useState(1)
    const [num, setNum] = useState(1)

    const addOne = useCallback(() => {
        setCount(count => count + num)
        setNum(num => num + 1)
    }, [num])

    return (
        <>
            <div className="App">
                <Son addOne={addOne}/>
                <p>{count}</p>
                <button onClick={addOne}>点击加1</button>
            </div>
        </>
    );
}

const Son = React.memo(({addOne}) => {
    console.log("Son发生渲染")
    return <div>
        <p>son</p>
        <button onClick={addOne}>Son点击加1</button>
        <GrandSon/>
    </div>
})

const GrandSon = () => {
    console.log("GrandSon发生渲染")

    return <div><p>GrandSon</p></div>
}

实现效果:在依赖数组中数组callback函数中使用到的变化,每当数组中的变量发生改变时,回调函数都会重新定义,执行时机和useEffect依赖数组里是类似的。但案例中这种情况已经失去了优化的初衷。所以,案例五中的情况,尽量不要用useCallback了,使用和不使用效果都一样,还多了一层缓存的耗时操作。

22、自定义钩子函数

官方文档:使用自定义 Hook 复用逻辑 – React 中文文档

官方案例:

使用use开头,后面为自定义名称,可以将需要集中处理的部分写到自定义钩子函数里,并且可以通过返回对象给调用自定义钩子的组件。

import { useState, useEffect } from 'react';

export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  return isOnline;
}

23、React.forwardRef

用来指定React组件向外暴露的ref

无法直接获取react组件的DOM对象,因为一个React组件中可能有多个Dom对象,React也不知道要给你谁。

案例一:

function App() {

    const sonRef=useRef()

    useEffect(()=>{
        console.log(sonRef)
    })

    return (
        <>
            <div>
                {/* 无法直接获取react组件的DOM对象
                       因为一个React组件中可能有多个Dom对象
                       React也不知道要给你谁
                 */}
                <Son ref={sonRef}/>
            </div>
        </>
    );
}

function Son(){
    const inputRef=useRef()

    const clickHandler=()=>{
        // 点击按钮获取文本
        console.log(inputRef.current.value);
    }

    return (
        <div>
            <h1>Son</h1>
            <input ref={inputRef} type="text"/>
            <button onClick={clickHandler}>点击</button>
        </div>
    )
}

效果:普通DOM对象可以被获取到,而React组件的DOM对象不能被直接获取到。

案例二:

对子组件进行改造:

const Son = React.forwardRef((props, ref) => {   // 通过forwardRef包装的函数会多一个ref参数,这个ref用于指明这个组件中需要返回的ref是谁。即当父组件调用这个组件的ref时,该组件就返回该ref。
    const inputRef = useRef()

    const clickHandler = () => {
        // 点击按钮获取文本
        console.log(inputRef.current.value);
    }

    return (
        <div>
            <h1 ref={ref}>Son</h1>
            <input ref={inputRef} type="text"/>
            <button onClick={clickHandler}>点击</button>
        </div>
    )
})

效果:父组件拿到了子组件返回的ref,且ref为子组件自定义指定的DOM元素的ref

24、useImperativeHandle

单纯使用forwardRef暴露组件内部的ref会存在一定的安全隐患,可以通过useImperativeHandle来实现自定义保留ref。

function App() {

    const sonRef = useRef()

    useEffect(() => {
        console.log(sonRef)
    })

    return (
        <>
            <div>
                {/* 无法直接获取react组件的DOM对象
                       因为一个React组件中可能有多个Dom对象
                       React也不知道要给你谁
                 */}
                <Son ref={sonRef}/>
            </div>
        </>
    );
}

const Son = React.forwardRef((props, ref) => {
    const inputRef = useRef()

    // useImperativeHandle 可以用来指定useRef返回的值
    useImperativeHandle(ref,()=>{
        // 回调函数的返回值,会成为ref的值
        return {name:"孙悟空"}
    })

    const clickHandler = () => {
        // 点击按钮获取文本
        console.log(inputRef.current.value);
    }

    return (
        <div>
            <h1>Son</h1>
            <input ref={inputRef} type="text"/>
            <button onClick={clickHandler}>点击</button>
        </div>
    )
})

效果:useImperativeHandle的回调函数的返回值是什么,回调的ref就是什么。

也可以这样返回DOM对象:

 // useImperativeHandle 可以用来指定useRef返回的值
    useImperativeHandle(ref,()=>{
        // 回调函数的返回值,会成为ref的值
        return inputRef.current
    })

但为了更好的管控DOM对象,一遍这样写:

// useImperativeHandle 可以用来指定useRef返回的值
    useImperativeHandle(ref, () => {
        // 回调函数的返回值,会成为ref的值
        return {
            changeInputValue(val) {
                inputRef.current.value = val
            }
        }
    })

25、useLayoutEffect和useInsertionEffect

使用场景:

1、useInsertionEffect一般用来在DOM加载前在原DOM上插入一些原生DOM元素之类的操作,这种操作一般不太合理,所以使用频率不高。(加载前,插入DOM元素首选)

2、uselayoutEffect在样式渲染前执行,可以在里面加一些样式改变的操作,并且页面只会渲染一次,若使用useEffect去设置样式,页面在渲染完成时会执行原有的一次,在加载到useEffect里的样式时又会渲染一次。这种样式渲染情况,推荐使用uselayoutEffect来处理。(渲染前,添加新样式首选)

3、在18版本以后useEffect做了一些调整,适应大多数情况的执行。样式出现闪烁情况时,可以考虑uselayoutEffect。(18以后尽量用useEffect,大多数情况首选)

先从一张图区别这两者和useEffect的执行时机差异:

useLayoutEffect 是 useEffect 的一个版本,在浏览器重新绘制屏幕之前触发。

官网文档:useLayoutEffect – React 中文文档

useInsertionEffect 可以在布局副作用触发之前将元素插入到 DOM 中。

官网文档:useInsertionEffect – React 中文文档

简单地讲,useInsertionEffect会在Dom改变前执行,useLayoutEffect会在Dom改变后执行,前两种都是在DOM渲染前执行的,而useEffect是在DOM渲染完毕后执行的。

案例一:

查看三个effect的执行先后:

function App() {
    const [count, setCount] = useState(1)
    const h3Ref = useRef()

    useEffect(() => {
        console.log("useEffect", h3Ref)
    })
    useLayoutEffect(() => {
        console.log("useLayoutEffect", h3Ref)
    })
    useInsertionEffect(() => {
        console.log("useInsertionEffect", h3Ref)
    })

    return (
        <div>
            <h3 ref={h3Ref}>{count}</h3>
        </div>
    );
}

打印:可以看到useInsertionEffect执行时DOM还没改变,此时DOM元素还未改变。

26、useDebugValue

这个钩子用得不多,一般是在开发中区别自定义钩子的标识。

const useMyHook=()=>{
    useDebugValue("哈哈")

    useEffect(()=>{
        console.log("gooooo")
    })
}

function App() {
    useMyHook()

    return (
        <div>
        </div>
    );
}

在React开发者插件工具中才能看到备注的hook:控制台直接是看不到的,一般在自定义钩子特别多时,开发区分时用到。

三、组件之间的传值

1、Props父子组件之间传值

通过props可以实现父组件传值给子组件。

在函数组件中定义了一个形参props,它指向的是一个对象,它包含了父组件中包含的所有参数。在类组件中同过this.props获取(类组件的props存储到类的实例对象中)。

官网文档介绍:

将 Props 传递给组件 – React 中文文档

通过组件传参的方式:

<ChildOne num={12}/>
<ChildTwo num={24}/>

子组件接收参数:

class ChildTwo extends Component {
    render () {
        return (
            <div>
                <h2>我是子组件ChildTwo: {this.props.name}</h2>
            </div>
        )
    }
}

function ChildOne (prop) {
     return(
         <div>
             <h2>ChildOne子组件:{prop.num}</h2>
         </div>
     )
}

2、子向父传值

通过父组件传递方法对象给子组件,子组件再调用该方法并传入对应参数和处理给父组件。

父组件:

function ParentOne () {
    function getSonData (data) {
        console.log(data)
    }
    
    return (
        <div>
            <ChildThree getSonData={getSonData} />
        </div>
    )
}

子组件:

 function ChildThree (prop) {
     function sendData(){
         prop.getSonData("我是子组件的数据")
     }
     return(
         <div>
             <button onClick={sendData}>点击向父组件传值</button>
         </div>
     )
 }

这里父组件将getSonData方法对象先传递给子组件,子组件拿到方法对象后可以通过prop进行调用并传入子组件的参数到方法里,此时会调用父组件里的方法以拿到子组件的数据。

3、context实现跨层级通信

参考:Context – React

 (1)首先,创建一个MyContext.js用来管理context环境变量

import React from "react"
//创建中间仓库
const MyContext=React.createContext(undefined)

export default MyContext

(2)案例

第一层父组件,提供数据

class LayerOne extends Component {
    
    constructor (props, context) {
        super(props, context)
        this.state={
            name: "cute Tom"
        }
    }
    
    render () {
        return (
            <div>
                <h1>我是one</h1>
                <MyContext.Provider value={this.state}>
                    <LayerTwo/>
                </MyContext.Provider>
            </div>
        )
    }
}

第二层:包含第三层

class LayerTwo extends Component {
    render () {
        return (
            <div>
                <h2>我是two</h2>
                <LayerThree/>
            </div>
        )
    }
}

第三层可以直接消费数据,注意这里需要声明一下是哪个MyContext

class LayerThree extends Component {
    render () {
        return (
            <div>
                <h3>我是three</h3>
                <MyContext.Consumer>
                    {value => <div><h2>{value.name}</h2></div>}
                </MyContext.Consumer>
            </div>
        )
    }
}

LayerThree.contextType=MyContext

效果:

context hook案例:

函数组件中的使用:Context Hook

4、createContext和useContext的使用

方法一利用Consumer标签获取:

1)创建Context共享仓库, React.createContext();

2)在需要Context的组件中引入Context;

3)  通过<Context名.Consumer>标签里的一个回调函数获取Context值,回调函数的第一个参数对应了Context的值。

const ShareBox = React.createContext({
    foods: ["vegetable", "fruit", "rice"],
    name: "大仓库",
    count: 100
})

const Test = () => {
    return (
        <div className={styles.main+" App"}>
            <ChildOne/>
            <p>Hello world</p>
        </div>
    );
};

const ChildOne = () => {
    return <div>
            <ShareBox.Consumer>
                {
                    (ctx) => {
                        return <div>
                            {ctx.name} - {ctx.age}
                            {
                                ctx.foods.map((item, index) => {
                                    return <div key={index}>{item}</div>
                                })
                            }
                        </div>
                    }
                }
            </ShareBox.Consumer>
    </div>
}

这里首先通过createContext创建仓库对象,对象内容传入函数:

const ShareBox = React.createContext({
    foods: ["vegetable", "fruit", "rice"],
    name: "大仓库",
    count: 100
})

在跨层级的子孙组件中,通过<仓库名.Consumer>标签包裹的方式,标签内必须是一个回调函数,其中参数对应了仓库里放的值。这种操作在每个组件中都可以用到。

<ShareBox.Consumer>
                {
                    (ctx) => {
                        return <div>
                            {ctx.name} - {ctx.age}
                            {
                                ctx.foods.map((item, index) => {
                                    return <div key={index}>{item}</div>
                                })
                            }
                        </div>
                    }
                }
  </ShareBox.Consumer>

打印:

方法二利用useContext钩子获取:

1)创建Context共享仓库, React.createContext();

2)在需要Context的组件中引入Context;

3)  通过<Context名.Consumer>标签里的一个回调函数获取Context值,回调函数的第一个参数对应了Context的值。

1和2参考方法一里的代码,第三步如下:

const ChildOne = () => {
    const ctx = useContext(ShareBox)
    return <div>
        {ctx.name} - {ctx.age}
        {
            ctx.foods.map((item, index) => {
                return <div key={index}>{item}</div>
            })
        }
    </div>
}

useContext(以Context作为参数) ,会将Context中的数据获取并作为返回值。

Provider的使用:

Provider表示数据的生产者,可以使用它来指定Context中的数据;通过value属性来指定Context中存储的数据,这样一来,在该组件的所以子组件中,都可以通过Context来指定它所指定的数据。

可以通过Context.Provider来修改Context的值。

当组件读取Context中数据时,它会读取离它最近的Provider中的value作为Context的数据。没有Provider则读取的是Context中的默认数据。

const ShareBox = React.createContext({
    foods: ["vegetable", "fruit", "rice"],
    name: "大仓库",
    count: 100
})

const Test = () => {
    return (
        <div className={styles.main + " App"}>
            <ShareBox.Provider value={
                {
                    foods: ["1", "2", "3"],
                    name: "小仓库",
                    count: 50
                }
            }>
                <ChildOne/>
            </ShareBox.Provider>
            <p>Hello world</p>
        </div>
    );
};

const ChildOne = () => {
    const ctx = useContext(ShareBox)
    return <div>
        {ctx.name} - {ctx.age}
        {
            ctx.foods.map((item, index) => {
                return <div key={index}>{item}</div>
            })
        }
    </div>
}

效果:父组件修改了Context里的值,子孙组件接收到的是<Xxx.Provider>的value属性里的值。

结合state一起使用:

context可以结合state一起使用来实现动态渲染:

const ShareBox = React.createContext({
    foods: ["vegetable", "fruit", "rice"],
    name: "大仓库",
    count: 100,
    addFood: () => {
    },
    removeFood: () => {
    }
})

const Test = () => {
    const [data, setData] = useState({
        foods: ["vegetable", "fruit", "rice"],
        name: "大仓库",
        count: 100
    })
    const addFood = () => {
        setData({...data, foods: ["vegetable", "fruit", "rice", "chicken"]})
    }
    const removeFood = () => {
        setData({...data, foods: ["vegetable", "fruit"]})
    }

    return (
        <div className={styles.main + " App"}>
            <ShareBox.Provider value={
                {
                    ...data,
                    addFood,
                    removeFood
                }
            }>
                <ChildOne/>
            </ShareBox.Provider>
            <p>Hello world</p>
        </div>
    );
};

const ChildOne = () => {
    const ctx = useContext(ShareBox)
    return <div>
        {ctx.name} - {ctx.age}
        {
            ctx.foods.map((item, index) => {
                return <div key={index}>{item}</div>
            })
        }
        <button onClick={ctx.addFood}>添加</button>
        <button onClick={ctx.removeFood}>删除</button>
    </div>
}

链接:useContext使用 - 简书 

四、网络请求框架使用

见我博客:

前端框架 网络请求 Fetch Axios_Dragon Wu的博客-CSDN博客

五、React路由的使用

常用API

官方文档:React Router: Declarative Routing for React.js

React Router可以将url地址和组件进行映射,当用户访问某个地址时,与其对应的组件会自动的挂载。

使用React Router的步骤:

1、安装依赖

2、在入口文件中引入BrowserRouter组件

3、将BrowserRouter设置为根组件

Route标签:

将路由与组件进行映射。

<Route path={"/home"} element={<Home />} />

// 有两个参数,path的值为映射的URL地址,element为要挂载的组件(版本5里叫component)

NavLink标签:

在版本5里是Link标签。

设置点击后的样式格式:

<NavLink to={"/about"}>关于</NavLink>

// 类似于a标签,点击后会发生页面跳转,不同在于a标签每次跳转会重新加载页面,而NavLink不会重新加载页面,Nav是局部跳转(速度比a标签快,网络开销小)。

<NavLink/> 与<Link/>类似,但增加了一个点击之后导航高亮的效果 ,它有一个默认的类名active ,通过这个类名可以修改高亮的颜色

<Link to> : 尾随/ 的问题


在v5中,如果当前 url 是/home, 那么<Link to="news"> 会渲染成 <a href="/news">; 而如果当前 url 是 /home/,那么又会渲染成 <a href="/home/news">

在v6中,无论当前 url 是/home还是/home/,<Link to="news">都会渲染成<a href='/home/news'>。

useParams:

通过useParams钩子函数可以获取URL里传入的参数。

function Detail () {

    console.log(useParams())
    return (
        <div>
            <h2>详情</h2>
            <p>id: {useParams().id}</p>
        </div>
    )
}

useLocation:

通过useLocation钩子函数可以获取URL的基本信息。

console.log(useLocation())

打印:

useMatch:

useMatch用于检查当前url是否匹配某个路由。

useMatch(路由字符串)

 console.log(useMatch("/detail/123"))

// 不匹配就返回null,匹配返回一个对象

useMatch("/user/:id")

// 也支持动态参数匹配

useNavigate:

useNavigate获取一个跳转页面的函数。相当于通过代码来执行跳转。默认是push方法,会产生历史记录;可以设置replace为true来实现redirect的效果(不参数历史记录)

使用方法:

function Detail() {
    const route = useNavigate()
    const go = () => {
        // route("/about")
        route("/about", {replace: true})
    }
    return (
        <div>
            <h2>详情</h2>
            <button onClick={go}>跳转</button>
        </div>
    )
}

Navigate标签:

组件用来跳转到对应的位置,默认使用push,会产生历史记录。可设置replace属性,变成redirect模式,将不会产生历史记录。

<Navigate to={"/about"} replace />

//当我们将这标签放到某个组件中是,组件在加载到这个标签的第一时间会产生跳转

参考:

React Router 6 (React路由) 最详细教程-阿里云开发者社区

6.v新特性:

react-router 6 新特性总结 - 知乎

首先安装依赖:

yarn add react-router-dom@6

声明式导航

(推荐使用编程式导航效率更高)

案例代码:一般使用BrowseRouter有history记录

import React, { Component } from "react"
import { BrowserRouter, Routes, Route, Navigate, NavLink, useParams,Link,Outlet } from "react-router-dom"

class Home extends Component {
    render () {
        return (
            <div>
                <h1>Home</h1>
            </div>
        )
    }
}

class About extends Component {
    render () {
        return (
            <div>
                <h1>About</h1>
            </div>
        )
    }
}

function Detail () {
    
    console.log(useParams())
    return (
        <div>
            <h2>详情</h2>
            <p>id: {useParams().id}</p>
        </div>
    )
}

function Main(){
    return (
        <div>
            <h1>文档</h1>
            <Link to={"/doc/one"}>文档1</Link>
            <Link to={"/doc/two"}>文档2</Link>
            <Link to={"/doc/three"}>文档3</Link>
            {/*嵌套路由时注意书写这个标签*/}
            <Outlet/>  {/* 指定路由组件呈现的位置 */}
        </div>
    )
}

function App () {
    
    return (
        <div>
            <BrowserRouter>
                <div>
                    <NavLink to={"/home"}>首页</NavLink>|
                    <NavLink to={"/about"}>关于</NavLink>|
                    <NavLink to={"/detail/123"}>详情</NavLink>|
                    <NavLink to={"/doc"}>文档</NavLink>
                </div>
                <Routes>
                    <Route path={"/home"} element={<Home />} />
                    <Route path={"/about"} element={<About />} />
                    {/*嵌套路由*/}
                    <Route path={"doc"} element={<Main/>}>
                        <Route path={"one"} element={<h3>one</h3>} />
                        <Route path={"two"} element={<h3>two</h3>} />
                        <Route path={"three"} element={<h3>three</h3>} />
                    </Route>
                    {/*动态路由*/}
                    <Route path={"/detail/:id"} element={<Detail />} />
                    {/*路由重定向,也可用于404处理*/}
                    <Route path="/*" element={<Navigate to="/home" replace />} />
                    {/*404处理*/}
                    <Route path="*" element={<NotFound />} />
                </Routes>
            </BrowserRouter>
        </div>
    )
}

export default App

// 用来作为 404 页面的组件
const NotFound = () => {
    return <div>你来到了没有知识的荒原</div>
}

编程式导航

(1)新建src/router/index.js文件

import { Navigate } from "react-router-dom"
import Home from "../page/home/Home"
import HomeLeft from "../page/home/HomeLeft"
import HomeRight from "../page/home/HomeRight"
import About from "../page/About"
import Detail from "../page/Detail"
const routes=[
    {
        path: "/",
        element: <Navigate to="/home" />
    },
    {
        path: "home",
        element: <Home />,
        children: [
            {
                index: true,
                element: <HomeLeft />
            },
            {
                path: "right",
                element: <HomeRight />
            }]
    },
    {
        path: "/about",
        element: <About />
    },
    {
        path: "/detail",
        element: <Detail />
    },
    { path: "*", element: <p>404页面不存在</p> }
]
export default routes

(2)为src/index.js添加BrowserRouter容器

注意: BrowserRouter必须在App标签的外层

 (3)App.js如下:

import React from 'react'
import routes from "./router/index.js"
import {useRoutes,NavLink } from "react-router-dom"

 function App () {
     // useRoutes可以用路由表生成<Routes>...</Routes>结构
     // 根据路由表生成对应的路由规则
     const element = useRoutes(routes)
     
     return(
         <div id="App">
             <div>
                 <NavLink to={"/home"} >首页</NavLink>|
                 <NavLink to={"/about"}>关于</NavLink>|
                 <NavLink to={"/detail"}>详情</NavLink>
             </div>
             <div>
                 {element}
             </div>
         </div>
     )
 }

 export default App
(4)若有嵌套路由要使用<Outlet/>标签标明子组件插入的位置
class Home extends Component {
    render () {
        return (
            <div>
                <h1>Home</h1>
                <Link to={"/home/right"}>homeRight</Link>
                <Link to={"/home"}>homeLeft</Link>
                {/*嵌套路由时注意书写这个标签*/}
                <Outlet/>  {/* 指定路由组件呈现的位置 */}
            </div>
        )
    }
}

编程式跳转

默认是push 模式

export default function HomeNews() {
  const navigate = useNavigate();
  const jump = ()=>{
    navigate('/home')
  }
  return (
      <News>
        <button onClick={jump}>点击跳转</button>
      </News>
  )
}

使用{replace:true} 就会变为replace模式

navigate('/home', { replace: true });

也可以使用 navigate(-1) 传入一个数字来进行跳转

navigate(1)//传入数字

六、Anti Desgin的使用实现UI界面

官方文档:Ant Design - A UI Design Language

1、添加到项目:

yarn add antd

2、样式引入

全局映入样式:在src/index.js里引入样式,不推荐,会导入很多无用的样式

import 'antd/dist/antd.css';  // or 'antd/dist/antd.less'

按需映入,推荐

下面两种方式都可以只加载用到的组件。

  • 使用 babel-plugin-import(推荐)。

  • yarn add babel-plugin-import
    // .babelrc or babel-loader option
    {
      "plugins": [
        ["import", { "libraryName": "antd", "style": "css" }] // `style: true` 会加载 less 文件
      ]
    }

  • 然后只需从 antd 引入模块即可,无需单独引入样式。等同于下面手动引入的方式。

    // babel-plugin-import 会帮助你加载 JS 和 CSS
    import { DatePicker } from 'antd';
  • 手动引入

    import DatePicker from 'antd/lib/date-picker';  // 加载 JS
    import 'antd/lib/date-picker/style/css';        // 加载 CSS
    // import 'antd/lib/date-picker/style';         // 加载 LESS

3、组件里直接调用即可

七、Redux中央仓库的使用

当我们的项目稍微复杂一些时,原生的state可能无法高效的管理和操作数据缓存,通过Redux可以将数据统一管理,并且减低代码耦合。

参考:入门 Redux | Redux 中文官网

 其工作原理与hook reducer类似

yarn add @reduxjs/toolkit

安装参考:安装 | Redux 中文官网

1、新建src/redux/index.js用于存放redux的文件

//1、引入redux
import { configureStore } from "@reduxjs/toolkit"

//2、创建仓库
const store = configureStore({reducer})

//3、reducer为store服务的执行者, action={type:"",data:5}
function reducer (preState = 10, action) {
    switch (action.type) {
        case "add":
            return preState + action.data
        case "sub":
            return preState - action.data
        default:
            return preState
    }
}

//4、使用store
console.log(store)
//获取仓库的数据
console.log(store.getState())
//触发action
store.dispatch({
    type: "add",
    data: 5
})
console.log(store.getState())

2、在src/index.js里引入

运行结果:可以看到数据已经被操作了

3、可以通过logger查看redux日志

yarn add redux-logger

添加logger后:

//1、引入redux
import { configureStore } from "@reduxjs/toolkit"
//引入日志
import { logger } from "redux-logger/src"

//2、创建仓库
// const store = configureStore({reducer})
const store = configureStore({
    reducer,
    middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger)
})

//3、reducer为store服务的执行者, action={type:"",data:5}
function reducer (preState = 10, action) {
    switch (action.type) {
        case "add":
            return preState + action.data
        case "sub":
            return preState - action.data
        default:
            return preState
    }
}

//4、使用store
console.log(store)
//获取仓库的数据
console.log(store.getState())
//触发action
store.dispatch({
    type: "add",
    data: 5
})
console.log(store.getState())

可以看到日志的打印:

合并多个reducer 

//1、引入redux
import { configureStore} from "@reduxjs/toolkit"
//引入日志
import { logger } from "redux-logger/src"

//2、创建仓库
// const store = configureStore({reducer})
const store = configureStore({
    //合并多个reducer
    reducer:{
        reducer,
        reducer2
    },
    middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger)
})

//3、reducer为store服务的执行者, action={type:"",data:5}
function reducer (preState = 10, action) {
    switch (action.type) {
        case "add":
            return preState + action.data
        case "sub":
            return preState - action.data
        default:
            return preState
    }
}

function reducer2 (preState = { user: "", num: 5 }, action) {
    const { type, data } = action
    let newState = { ...preState }
    switch (type) {
        case "addUser":
            newState.user = data.user
            return newState
        case "delUser":
            newState.user = ""
            return newState
        default:
            return newState
    }
}

//4、使用store
console.log(store)
//获取仓库的数据
console.log(store.getState())
// //触发action
store.dispatch({
    type: "add",
    data: 5
})
console.log(store.getState())

store.dispatch({
    type: "addUser",
    data: {
        user: "大猫"
    }
})

store.dispatch({
    type: "delUser"
})

运行结果:

4、Redux的模块化管理

项目书写时我们需要加上命名空间以避免重复:

另外,我们还需要将常量单独提取到一个文件里管理:

分类管理reducers:

 reducer/index.js: 引入所有reducers方便调用

import { count } from "./countReducer"
import {user} from "./userReducer"

export const reducers={
    count,
    user
}

redux/index.js引入reducer的索引文件即可:

//1、引入redux
import { configureStore} from "@reduxjs/toolkit"
//引入日志
import { logger } from "redux-logger/src"

//引入reducers
import { reducers } from "./reducers"

//2、创建仓库
// const store = configureStore({reducer})
const store = configureStore({
    //合并多个reducer
    reducer: reducers,
    middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger)
})

5、注意事项

返回值注意:

书写时最好按照这种格式的逻辑:

redux默认不支持异步操作,一般实现思路是等异步任务做完后同步回调时进行redux操作:

6、总结Redux使用的完整步骤

安装依赖:

yarn add @reduxjs/toolkit

yarn add react-redux

日志依赖:

yarn add redux-logger

(1)创建store仓库,reducers分开管理

reducers:  数据开发中type字符串应放入一个常量管理

export function count(preState={num:0},action){
    const {type,data}=action
    let newState={...preState}
    switch (type) {
        case "num/add":
            newState.num+=data.num
            return newState
        case "num/sub":
            newState.num-=data.num
            return newState
        default:
            return newState
    }
}
export function user(preState={user:{name:"",age:1}},action){
    const {type,data}=action
    let newState={...preState}
    switch (type) {
        case "user/update":
            newState.user=data.user
            return newState
        case "user/delete":
            newState.user={name:"",age:1}
            return newState
        default:
            return newState
    }
}

索引所有reducers:

import { count } from "./countReducer"
import { user } from "./userReducer"

export const reducers = {
    count,
    user
}

创建store仓库并开启日志:

//1、引入redux
import { configureStore } from "@reduxjs/toolkit"
//引入日志
import { logger } from "redux-logger/src"

//导入reducers
import { reducers } from "./reducers"

//2、创建仓库
// const store = configureStore({reducer})
const store = configureStore({
    //合并多个reducer
    reducer: reducers,
    middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger)
})


export default store

(2)导入到src/index.js文件,全局配置

(3)组件中的使用

import React from "react"
import { connect } from "react-redux"

function ReduxTestComp (props) {
    return (
        <div>
            <h1>count: {props.count.num}</h1>
            <h1>user:{props.user.user.age},{props.user.user.name}</h1>
            <button onClick={()=>props.updateUser({user:{ name: "Jack", age: 12 }})}>修改用户</button>
            <button onClick={()=> props.deleteUser()}>删除用户</button>
            <button onClick={()=>props.addNum()}>+</button>
        </div>
    )
}

export default connect((state) => {//读取仓库中所有state
    console.log(state)
    return {
        count: state.count,
        user: state.user
    }
}, (dispatch) => {//action操作
    console.log(dispatch)
    return {
        updateUser: (data) => {
            return dispatch({ type: "user/update", data: data })
        },
        deleteUser: ()=>{
            return dispatch({type:"user/delete"})
        },
        addNum:()=>{
            return dispatch({type:"num/add",data:{num:12}})
        }
    }
})(ReduxTestComp)
import React from "react"
import ReduxTestComp from "./component/reduxTest/ReduxTestComp"

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

export default App

至此,实现redux的全程应用。

八、Redux Tool使用总结

见我的这篇博客:前端框架 Redux tool RTK 总结-CSDN博客

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐