• React + TypeScript实战(二)hooks用法


    本文采用的react相关技术为:

    • react@18.2.0
    • typescript@4.7.4
    • 脚手架create-react-app

    一、函数式组建的声明方式

    import react, { FC } from 'react'
    
    type IProps = {
        message: string
    }
    
    const Test: FC = (props) => {
      return 
    {props.message}
    } export default Test
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • React.FC(可以直接写为FC)显式地定义了返回类型,其他方式是隐式推导的
    • React.FC 对静态属性:displayName、propTypes、defaultProps 提供了类型检查和自动补全

    如果没有手动为FC声明类型,则会报类型错误,此时我们需要手动进行类型的二次转换,比如:

    // wrong
    const Test: React.FC<{}> = () => 'hello'
    
    // correct
    onst Test: React.FC<{}> = () => ('hello' as unknown) as JSX.Element
    
    • 1
    • 2
    • 3
    • 4
    • 5

    二、useState

    useState()接受一个参数为默认值,该方法返回一个数组,第一个值为定义data的值,第二个为更新data的方法,他们总是成对出现的:

    import react, { FC, useState } from 'react'
    
    type IProps = {
        message?: string
    }
    
    const Test: FC = (props) => {
      // 大部分情况下,TS 会自动为你推导 state 的类型
      // 这里会自动将 name 推导为 string 类型
      let [name, setName] = useState("Ryuko_黑猫几绛")
    
      return (
        
    ) } export default Test
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2.1 useState使用注意点

    2.1.1 useState 是异步的

    修改state之后无法拿到最新的状态,要等到下一个事件循环周期执行时,状态才是最新的:

    const Test: FC = props => {
      const [people, setPeople] = useState<{ name: string, age: number }>({ name: "张三", age: 1 })
    
      const handleUpdate = () => {
        setPeople({ ...people, name: '王五' });
        console.log(people.name); // 张三
      }
    
      return (
        
    { // dom 上面的数据会改变为 '王五' } {people.name}
    ) } export default Test;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    但是在state不影响DOM的前提下,你是可以同步使用它:

    const Test: FC = props => {
      const [people, setPeople] = useState<{ name: string, age: number }>({ name: "张三", age: 1 })
    
      const handleUpdate = () => {
        setPeople({ ...people, name: '王五' });
        people.name = "王五";
        console.log(people.name); // 王五
      }
    
      return (
        
    { // dom 上面的数据会改变为 '王五' } {people.name}
    ) } export default Test;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    2.1.2 useState 根据地址判断更新

    useState要触发页面的更新,是比较新的值和旧的值是否一致(对于引用类型而言,比较是新旧对象的内存地址是否一致),如果不一致才会更新页面,所以若两次传入同一对象则不会触发组件更新

    2.1.3 useState 将新值直接覆盖掉旧值,而不是合并

    const [temp,setTemp] = useState({a: 1, b: 2});
    setTemp({a: 2}); // temp = {a: 2}
    
    • 1
    • 2

    因此,如果是处理复杂的对象数据,我们可以这样做:

    // 同样含义的变量可以合并成一个 state,代码可读性会提升很多
    const [userInfo, setUserInfo] = useState({
      firstName,
      lastName,
      school,
      age,
      address
    })
    
    setUserInfo(s=> ({
      ...s,
      fristName,
    }))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    三、useRef

    const refContainer = useRef(initialValue);
    
    • 1

    useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。

    一个常见的用例便是命令式地访问子组件:

    const TextInputWithFocusButton: FC = () => {
      const inputEl = useRef(null);
      const onButtonClick = () => {
        // `current` 指向已挂载到 DOM 上的文本输入元素
        inputEl.current.focus();
      };
      return (
        <>
          
          
        
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    注意:无法直接通过ref来引用函数组件,因为函数组件没有对象

    const ref = useRef(null)
    
    // wrong!
    
    
    // correct!
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    不过,我们可以通过父子组件通信的思想:父组件转发自己的ref给子组件,然后在父组件中通过这个ref(传递过去后会被子组件绑定)操作子组件的dom

    通过查文档可知,如果想实现上述的引用思想,需要为子组件函数声明为:ForwardRef类型:

    function forwardRef<T, P = {}>(render: ForwardRefRenderFunction<T, P>): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;
    
    • 1

    可以看到这个方法需要接收一个ForwardRefRenderFunction的函数参数,这个函数的声明为:

    interface ForwardRefRenderFunction<T, P = {}> {
        (props: P, ref: ForwardedRef<T>): ReactElement | null;
        displayName?: string | undefined;
        // explicit rejected with `never` required due to
        // https://github.com/microsoft/TypeScript/issues/36826
        /**
             * defaultProps are not supported on render functions
             */
        defaultProps?: never | undefined;
        /**
             * propTypes are not supported on render functions
             */
        propTypes?: never | undefined;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这是一个函数式接口,总的来看,我们需要为forwardRef提供一个函数参数,这个参数需要:

    • 参数值
      • props: P
      • ref:ForwardedRef
    • 返回值:ReactElement | null

    因此我们可以这样声明子组件:

    import react, { FC, forwardRef } from 'react'
    
    /**
     *  @params props 父组件传递进来的数据
     *  @params ref 在父组件中定义的ref,交给子组件来绑定
     */
    const Son = forwardRef((props,ref)=>{
      return (
        <>
          
        
      )
    })
    
    export default Son
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    现在子组件目标元素绑定了父组件传递进来的ref数据,我们可以回到父组件直接进行操作了:

    import react, { FC, useRef } from 'react'
    import Son from './Son'
    
    const Father: FC = () => {
      const inputRef = useRef(null)
      return (
        <>
          {
          	// 给子组件绑定 ref 
          }
          
          
        
      )
    }
    
    export default Father
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    3.1 比 ref 更有用

    从前面的例子来看,useRef似乎和ref相同,都是获取到实例dom节点

    注意这一句话:ref 对象在组件的整个生命周期内持续存在并且保持不变,因此当更新 current 值时并不会 re-render ,这是与 useState 不同的地方。

    根据useRef的这个性质,我们可以用来模拟实现全局变量

    下面看看这个例子,需求是点击按钮让点赞数 + 1,然后点击Alert弹框显示当前点赞数

    import React, { useState } from "react";
    const LikeButton: React.FC = () => {
        const [like, setLike] = useState(0)
        function handleAlertClick() {
            setTimeout(() => {
                alert(`you clicked on ${like}`) 
            }, 3000)
        }
        return (
            <>
                
                
            
        )
    }
    export default LikeButton
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    上面的代码看起来没有问题,但是由于在handleAlertClick中形成了闭包,所以弹出来的是第一次触发函数时的like值。也就是说,当我点赞数为5时,点击Alert,然后迅速点赞到10,最终弹出的值为5。

    为什么不是界面上like的实时状态?
    当我们更改状态的时候,React会重新渲染组件,每次的渲染都会拿到独立的like值,并重新定义个handleAlertClick函数,每个handleAlertClick函数体里的like值也是它自己的,所以当like为6时,点击alert,触发了handleAlertClick,此时的like是6,哪怕后面继续更改like到10,但alert时的like已经定下来了。

    总结:不同渲染之间无法共享state状态值

    3.1.1 采用全局变量

    在组件前定义一个类似 global 的变量

    import React from "react";
    let like = 0;
    const LikeButton: FC = () => {
      function handleAlertClick() {
        setTimeout(() => {
          alert(`you clicked on ${like}`);
        }, 3000);
      }
      return (
        <>
          
          
        
      );
    };
    export default LikeButton;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    总结:由于like变量是定义在组件外,所以不同渲染间是可以共用该变量,所以3秒后获取的like值就是最新的like值,该示例同时也说明:非state变量不会引起重新render

    3.1.2 采用 useRef

    import React, { useRef } from "react";
    const LikeButton: FC = () => {
      // 定义一个实例变量
      let like = useRef(0);
      function handleAlertClick() {
        setTimeout(() => {
          alert(`you clicked on ${like.current}`);
        }, 3000);
      }
      return (
        <>
          
          
        
      );
    };
    export default LikeButton;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    总结:由于 useRef更改不会re-render,所以用useRef 作为组件实例的变量,保证多个不同渲染过程中,获取到的数据肯定是最新的。

    3.2 useImperativeHandle

    useImperativeHandle 可以让你在使用 ref 时给父组件暴露的指定的值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用

    以这一章最开始的例子来说,还是先从子组件入手:

    import react, { FC, forwardRef, useImperativeHandle } from 'react'
    
    const Son = forwardRef((props,ref)=>{
      useImperativeHandle(ref, ()=>({
        talk: ()=>{
          console.log("hi Ryuko!");
        }
      }))
      return (
        <>
          
        
      )
    })
    
    export default Son
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    此时可以在父组件中直接调用子组件内部的方法:

    import react, { FC, useRef } from 'react'
    import Son from './Son'
    
    export interface InputRefProps{
      talk(): void
    }
    
    const Father: FC = () => {
      const inputRef = useRef(null)
      return (
        <>
          
          
        
      )
    }
    
    export default Father
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    四、useEffect

    首先看看这个hook的函数定义:

    function useEffect(effect: EffectCallback, deps?: DependencyList): void;
    
    • 1

    在useRef这一章的例子中我们知道了,funcion component每次Render的内容都会形成一个快照并保存下来,因此当状态变更而re-render时,会形成N个Render状态,而每个 Render 状态都拥有自己固定不变的 Props 与 State。即:函数在每次渲染时是独立的

    useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具有相同的用途,只不过被合并成了一个 API。

    4.1 每次 Render 都有自己的 Effects

    useEffect是react提供的一个专门来帮我们处理副作用的钩子

    useEffect 在实际 DOM 渲染完毕后执行,它每次渲染时对应的数据也是独立的

    虽然 React 在 DOM 渲染时会 diff 内容,只对改变部分进行修改,而不是整体替换,但却做不到对 Effect 的增量修改识别。因此需要开发者通过 useEffect 的第二个参数告诉 React 用到了哪些外部变量:

    useEffect(() => {
      document.title = "Hello, " + name;
    }, [name]); // Our deps
    
    • 1
    • 2
    • 3

    直到 name 改变时的 Rerender,useEffect 才会再次执行。

    • 如果我们没有给useEffect第二个参数,回调函数会在每次第一次创建组件componentDidMount和组件数据改变componentDidUpdate时执行
    • 如果第二个参数为 [ ],则只在第一次创建组件时执行

    4.2 清除副作用

    4.2.1 无需清理的副作用

    有时候,我们只想**在 React 更新 DOM 之后运行一些额外的代码。**比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。

    需求:监听url的变化来发送网络请求,保存返回结果

    import React, { FC, useState, useEffect } from "react";
    import ajax from '@utils/ajax'
    
    type IProps = {
      location:string
    }
    
    const Example: FC = (props) => {
      const [data, setData] = useState({});
    
      useEffect(() => {
        getData();
      }, [props.location]);
    
      const getData = () => {
        ajax.post().then(res => {
          setData(res);
        })
      }
      return 
    {data}
    } export default Example;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    当location发生变化时,useEffect中函数就会自动执行。

    4.2.2 需要清理的副作用

    const timer = useRef()
    
    //componentDidMount 和 componentDidUpdate
    //可以在useEffect中来处理副作用
    useEffect(() => {
        console.log("useEffect执行了")
        //副作用可以分为两类:
        //1.无需清理的副作用: 发送网络请求,获取服务器相应数据,修改state
        //2.需要清理的副作用: 订阅服务 定时器 ,只需要在useEffect中返回一个函数就可以在这个函数里面清除副作用
        timer.current = setInterval(()=>{
            console.log("定时器执行了")
        },1000)
    
        //componentWillUnmount
        //这边返回的清除函数的执行时机:是在下一次函数组件re-render之后,useEffect之前执行
        return () => {
            console.log("清除函数执行了")
            clearInterval(timer.current)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    4.3 useLayoutEffect 和 useInsertionEffect

    useEffect和useLayoutEffect的异同:

    • useLayoutEffect是在虚拟DOM构建完成后立即执行,useEffect是在真实DOM构建完成后立即执行

    • useLayoutEffect是同步执行,useEffect是异步执行

    useInsertionEffect可以向页面中插入dom元素
    useLayoutEffect可以在绘制屏幕前修改dom元素的样式
    
    • 1
    • 2

    在这里插入图片描述

    五、useMemo & useCallback

    首先看看这个hook的函数定义:

    function useMemo(factory: () => T, deps: DependencyList | undefined): T;
    
    • 1

    useMemo的泛型显示的制定了参数的返回类型,例如:

    // wrong!
    // 不能将类型“number”分配给类型“string”。ts(2322)
    const result = React.useMemo<string>(() => 1, [])
    
    • 1
    • 2
    • 3

    useMemo类似于Vue的计算属性,如果有一些属性值是可以根据其他值推导出来的,我们就可以使用useMemo

    它的参数有两个:

    • 一个函数,函数的返回值就是useMemo的结果
    • 数组依赖项,表示触发第一个函数参数的条件
    const length = useMemo(() => {
        return list.length;
    }, [list]);
    
    const [count, setCount] = useState(0);
    
    //double依赖于count,当count改变时,double自动改变
    let double = useMemo(() => {
        return count * 2
    }, [count]);   
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    5.1 缓存变量

    回顾一下,函数组件什么时候发生重新渲染:

    1. 组件重新被创建
    2. 当前组件state的内存地址发生了变化,无论dom中是否使用了state
    3. 父组件更新,子组件也会自动的更新
    4. 组件更新时,会卸载所有function,并重新创建function (执行函数组件的所有逻辑)
    
    • 1
    • 2
    • 3
    • 4

    注意第三点,父组件更新,子组件也会自动的更新

    这意味着,如果父组件有变量a,b,并将b传递给了子组件;如果修改了a的值,会触发父组件的更新,此时子组件也会自动更新,例如:

    // 父组件
    import React, { useState, useMemo } from "react";
    import Son from "./components/Son";
    
    function Father() {
      const [count, setCount] = useState(100);
      //useMemo()可以实现类似于Vue中的计算属性的功能,还可以用来缓存数据
      const obj = {
        name: "zhangsan",
      };
        
      //下面的obj2在函数组件每一次重新渲染的时候都是同一个对象,没有重新初始化
      const obj2 = useMemo(() => {
        return {
          name: "zhangsan",
        };
      }, []);
    
      return (
        
    { // 点击按钮以后,由于函数会重新执行,父组件中将创建一个新的obj,传递给子组件的obj也会改变 // 因此会触发子组件的自动更新 } { // 不会触发更新 }
    ); } export default Father;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    memo() 是一个高阶组件,高阶组件其实就是一个函数,只不过这个函数的参数是一个组件,函数的返回值是一个新的组件

    memo()可以实现类似于类组件的React.PureComponent功能

    只要父亲传递给孩子的props发生了变化就应该刷新子组件(如果父亲没有给子组件传递props或者父亲给子组件传递的props没有改变,则子组件不应该刷新

    父组件如果使用了useMemo,子组件一定要配套使用memo函数

    import React, { FC, useState, memo } from "react";
    
    type IProps = Readonly<{
      obj?: {
        name: string;
      };
    }>;
    
    const Son: FC = () => {
      const [message, setMessage] = useState("hello");
      return (
        
    Son
    ); }; export default memo(Son);
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    5.2 缓存函数

    在下面这个例子中,父组件将一个函数传给了子组件,子组件通过props.onChange,将数据传递回父组件,然后父组件修改text数据:

    // Father.tsx
    import react, { FC, useState } from 'react'
    import Son from './Son'
    
    export default const Father: FC<{}> = () => {
      const [text,setText] = useState("")
      const changeHandler = (event:React.ChangeEvent)=>{
        setText(event.target.value)
      }
      return (
        <>
          
    text文本为:{text}
    ) } // Son.tsx import react, { FC, forwardRef, useImperativeHandle } from 'react' export default const Son: FC<{ onChange?: (event: React.ChangeEvent) => void; }> = memo((props) => { return ( <> ) })
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    但是,每一次子组件输入内容,父组件的text文本发生变化,造成父组件被重新渲染,从而造成父组件传递给子组件的changeHandler方法(props)发生了变化,从而造成子组件的重新渲染。

    此时我们可以通过useCallback来解决这个问题:

    const changeHandler = useCallback((event:React.ChangeEvent)=>{
        setText(event.target.value)
    },[])
    
    • 1
    • 2
    • 3

    5.3 区别和联系

    实际上useCallback是基于useMemo实现的,useMemo是返回callback执行后的结果

    function useCallback(callback, args) {
    	return useMemo(() => callback, args);
    }
    
    • 1
    • 2
    • 3

    六、useContext

    跨级组件通信,实现同一子树下所有节点可统一共享子树根节点的数据

    useContext接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 value 决定。

    当组件上层最近的 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。

    基本用法:

    #1.src/context/index.ts
    import raect, { createContext, Context } from 'react'
    
    type ContextType = {
      name: string,
      age: number
    }
    
    let context: Context  = createContext(null)
    
    // 暴露出来一个 context 对象
    export default context
    
    
    #2 index.tsx
    import Context from './context/index'
    root.render(
      {name: "黑猫几绛", age: 100}}
      >
      	
      
    )
    
    #3 components/Son.tsx
    import Context from '../context/index'
    
    const Son: FC<{}> = (props) => {
      // 在子组件中获取根组件暴露的数据
      const contextValue = useContext(MyContext);
      return (
        
    text文本为:{contextValue.name}
    ) }; export default Son
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    七、useReducer

    首先看看这个hook的函数定义:

    function useReducer>(
        reducer: R,
        initialState: ReducerState,
        initializer?: undefined
    ): [ReducerState, Dispatch>];
    
    • 1
    • 2
    • 3
    • 4
    • 5

    必要的参数有:

    • 一个继承自Reducer类型的reducer纯函数
    • 初始化state状态
    • 可选参数init,负责惰性计算state初始值

    返回值为一个元组,表示为[state, dispath分发函数]

    接下来看看reducer纯函数的定义,需要初始值以及action:

     type Reducer = (prevState: S, action: A) => S;
    
    • 1

    所以,useReducer是在函数组件中实现类似 Redux 功能的一个Hook。他接收两个参数,第一个参数是一个recuder(纯函数),第二个参数是state的初始值。

    他返回一个状态 state和 dispath,state是返回状态中的值,而 dispatch 是一个可以发布事件来更新 state 的。

    注意

    import React, { useState, useReducer } from "react";
    
    type StateType = Readonly<{
      count: number;
    }>;
    
    //创建一个纯函数
    const reducer = (state: StateType, action: any) => {
      switch (action.type) {
        case "ADD":
          return { count: state.count + 1 };
        case "SUB":
          return { count: state.count - 1 };
        default:
          return state;
      }
    };
    
    function App() {
      // 使用useReducer(纯函数) ,得到state和dispatch
      const [state, dispatch] = useReducer(reducer, { count: 1000 });
      return (
        
    {state.count}
    ); } export default App;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    八、useReduce & useContext 实现全局数据共享

    在六、七章中不难发现,我们可以创建一个全局的Context对象,在这个对象中放入全局数据,比如:

    import react, { createContext } from 'react'
    
    let context = createContext({
        name: 'Ryuko_黑猫几绛',
        age: 18
    })
    
    export default context
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    然后父组件采用的形式将数据暴露给后代,在子组件中只需要引入这个context,然后通过useState将对象值取出就可以使用了。

    如果想要更改这样的全局变量,第一反应是通过某个函数来触发更改。useReduce恰好提供了这样的功能。

    总的来说:

    • useContext负责向子孙组件暴露数据
    • useReducer提供全局的state、reducers、dispatch等

    8.1 创建全局context

    首先回顾useReduce的函数定义:

    function useReducer<R extends Reducer<any, any>>(
        reducer: R,
        initialState: ReducerState<R>,
        initializer?: undefined
    ): [ReducerState<R>, Dispatch<ReducerAction<R>>];
    
    // Dispatch>其实就是一个函数,这个函数接收一个参数泛型 A
    type Dispatch<A> = (value: A) => void;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    他可以返回一个包裹着statedispatch函数的元组,这个函数接收ReducerAction类型的泛型。

    凭印象大体写出这样的代码:

    import react, { createContext } from 'react'
    
    let context = createContext()
    
    export default context
    
    • 1
    • 2
    • 3
    • 4
    • 5

    通过查看createContext的函数定义发现,这个方法需要传入一个参数表示默认变量值,同时这个方法接收一个泛型。

    现在想想,我们需要使用到什么样的泛型定义呢?

    为了满足useReducer的元组返回类型,首先需要一个全局state表示仓库的管理,同时还需要一个action表示对数据的处理操作:

    import react, { createContext, Dispatch } from 'react'
    
    export type StateType = {
      userInfo: {
        name: string,
        avater: string
      },
      position: string
    }
    
    // 声明Action的泛型
    export type ActionType = {
      type: string,
      payload: any
    }
    
    export type ContextType = [
      StateType,
      Dispatch
    ]
    
    // 可以把这里的 context 当作一个仓库,
    let context = createContext([
        {position:""},()=>{}
    ])
    
    export const reducer = (state: StateType, action: ActionType) => {
      switch (action.type) {
        case "LOACTION":
          return { ...state, position: action.payload.position }
        // 切记 需要返回默认值
        default:
          return state
      }
    }
    
    export default context
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    现在我们到根目录的index.tsx文件中进行配置。

    也许你会想说,在这里直接引入Context全局对象,使用useReducer计算出state和dispath,然后通过context.provider向子组件传递公共value={[state, dispatch]}就可以了。但需要注意的是,react hook 只能执行在函数式组件中:

    // ...
    import MyRedux from './components/MyRedux'
    
    const root = ReactDOM.createRoot(
      document.getElementById('root') as HTMLElement
    );
    
    root.render(
     
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    8.2 reducer内容分发

    所以我们创建一个叫做MyRedux的对象,将这个对象作为中转容器,然后向他的子组件传递全局中的数据

    import react, { FC, useReducer } from 'react'
    import MyContext, { reducer } from '../context'
    import Son from './Son.tsx'
    
    const MyRedux: FC<{}> = () => {
      const [state, dispatch] = useReducer(reducer, {position:""})
      return (
        <>
          
            
          
        
      )
    }
    
    export default MyRedux
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    现在内容分发给了子组件,在组组件中直接通过useContext注册属性以及方法即可:

    import react, { FC, useContext } from 'react'
    import context from '../context'
    
    const Son: FC<{}> = () => {
      const [state, dispatch] = useContext(context)
      return (
        <>
          
    {JSON.stringify(state)}
    ) } export default Son
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    九、自定义 Hooks

    import React, { useState, useCallback, useEffect } from "react";
    
    export const useWinSize = () => {
      // 1. 使用useState初始化窗口大小state
      const [size, setSize] = useState({
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight
      });
    
      const changeSize = useCallback(() => {
        // useCallback 将函数缓存起来 节流
        setSize({
          width: document.documentElement.clientWidth,
          height: document.documentElement.clientHeight
        });
      }, []);
      // 2. 使用useEffect在组件创建时监听resize事件,resize时重新设置state (使用useCallback节流)
      useEffect(() => {
        //绑定一次页面监听事件 组件销毁时解绑
        window.addEventListener("resize", changeSize);
        return () => {
          window.removeEventListener("resize", changeSize);
        };
      }, []);
    
      return size;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    参考文章

  • 相关阅读:
    基于乐鑫 ESP32 的创客夏令营徽章
    什么是反向代理,反向代理是如何工作的?
    将excel表中的英文自动翻译成中文
    现代架构设计:构建可伸缩、高性能的系统
    资源有限的大型语言模型的全参数微调
    使用百度EasyDL实现明厨亮灶厨师帽识别
    【JS】对象字面量 代替 switch 方法
    【C语言】扫雷小游戏(保姆教程)
    智能制造容器平台架构设计与实战
    飞书API 2-1:如何通过 API 创建文件夹?
  • 原文地址:https://blog.csdn.net/flow_camphor/article/details/125899194