ReactHook学习(第三篇-N)

54 阅读10分钟

[上篇地址,两篇有关联,因篇幅限制分开](ReactHook学习(第二篇-N) - 掘金 (juejin.cn))

示例2:获取窗口宽高变化

实现目标:通过 useWindowSize()来实时获取窗口的宽高

新建一个hook文件useWindowSize.ts,代码如下:

 import { useEffect, useState } from "react";
 ​
 //定义size对象
 interface WindowSize {
     width: number,
     height: number
 }
 const useWindowSize = () => {
     const [size, setSize] = useState<WindowSize>({
         width: document.documentElement.clientWidth,
         height: document.documentElement.clientHeight
     })
 ​
     useEffect(() => {
         const fun = () => {
             setSize({
                 width: document.documentElement.clientWidth,
                 height: document.documentElement.clientHeight
             })
         }
         window.addEventListener('resize', fun)
         return () => {
             window.removeEventListener('resize', fun)
         }
     },[])
     return size
 }
 ​
 export default useWindowSize

组件中这样使用:

 import useWindowSize from './hooks/useWindowSize';
 ​
 function App() {
 ​
   const size = useWindowSize()
 ​
   return (
     <div>
       <div>页面宽度:{size.width}</div>
       <div>页面高度:{size.height}</div>
     </div>
   )
 }
 ​
 export default App

在浏览器拖动放大缩小时,页面上的数据可动态变化

示例2:获取滚动偏移量变化

目标:通过 useWindowScroll()来实时获取页面的滚动偏移量

新建一个hook文件useWindowScroll.ts,代码如下:

 import { useEffect, useState } from "react"
 ​
 //定义偏移量对象
 interface ScrollOffset {
     x: number,
     y: number
 }
 ​
 const useWindowScroll = () => {
     const [off, setOff] = useState<ScrollOffset>({
         x: window.scrollX, 
         y: window.scrollY
     })
     useEffect(() => {
     
         const fun = () => {
             setOff({
                 x: window.scrollX,
                 y: window.scrollY
             })
         }
         //监听
         window.addEventListener('scroll', fun)
         return () => {
             //移除监听
             window.removeEventListener('scroll', fun)
         }
     })
     return off
 }
 ​
 export default useWindowScroll

组件中这样使用:

 import useWindowScroll from './hooks/useWindowScroll';
 ​
 function App() {
 ​
   const offSet = useWindowScroll()
 ​
   return (
     <div style={{height: '10000px', width: '10000px'}}>
       <div>滚动y:{offSet.y}</div>
       <div>滚动x:{offSet.x}</div>
     </div>
   )
 }
 ​
 export default App

示例:自动同步至localStorage

目标:通过 const [value, setValue] = useLocalStorage('key', 'value')可以传入默认的初始value和key,且每次修改value可以自动同步到localStorage中

新建一个hook类useLocalStorage,代码如下:

 import { useEffect, useState } from "react"
 ​
 const useLocalStorage = (key, defaultValue)  => {
     const [value, setValue] = useState(defaultValue)
     useEffect(() => {
         window.localStorage.setItem(key, value)
     },[key, value])
     return [value, setValue]
 }
 ​
 export default useLocalStorage

组件中使用:

 import useLocalStorage from './hooks/useLocalStorage';
 ​
 function App() {
 ​
   const [value, setValue] = useLocalStorage('key', 'react')
 ​
   return (
     <div>
 ​
     <button onClick={() => {
         //点击修改value,会自动同步至本地
         setValue('vue')
       }}>点击</button>
       <div>{ value }</div>
     </div>
   )
 }
 ​
 export default App

其他Hook函数

正确理解 useMemo、useCallback、memo 的使用场景

  • useMemo是一个 React Hook,可让您在重新渲染之间缓存计算结果。

     const cachedValue = useMemo(calculateValue, dependencies)
    
  • useCallback是一个 React Hook,可让您在重新渲染之间缓存函数定义。

     const cachedFn = useCallback(fn, dependencies)
    

在我们平时的开发中很多情况下我们都在滥用 useMemo、useCallback这两个 hook, 实际上很多情况下我们不需要甚至说是不应该使用,因为这两个 hook 在首次 render 时需要做一些额外工作来提供缓存,减少react里不必要的re-render

同时既然要提供缓存那必然需要额外的内存来进行缓存,综合来看这两个 hook 其实并不利于页面的首次渲染甚至会拖慢首次渲染,这也是我们常说的“不要在一开始就优化你的组件,出现问题的时候再优化也不迟”的根本原因。

那什么时候应该使用呢,无非以下两种情况:

  1. 缓存 useEffect 的引用类型依赖;
  2. 缓存子组件 props 中的引用类型。

缓存 useEffect 的引用类型依赖

 import { useEffect } from 'react'
 export default () => {
   const msg = {
     info: 'hello world',
   }
   useEffect(() => {
     console.log('msg:', msg.info)
   }, [msg])
 }
 ​

此时 msg 是一个对象该对象作为了 useEffect 的依赖,这里本意是 msg 变化的时候打印 msg 的信息。但是实际上每次组件在render 的时候 msg 都会被重新创建,msg 的引用在每次 render 时都是不一样的,所以这里 useEffect 在每次render 的时候都会重新执行,和我们预期的不一样,此时 useMemo 就可以派上用场了:

 import { useEffect, useMemo } from "react";
 const App = () => {
   const msg = useMemo(() => {
     return {
       info: "hello world",
     };
   }, []);
   useEffect(() => {
     console.log("msg:", msg.info);
   }, [msg]);
 };
 ​
 export default App;
 ​

同理对于函数作为依赖的情况,我们可以使用 useCallback:

 import { useEffect, useCallback } from "react";
 const App = (props) => {
   const print = useCallback(() => {
     console.log("msg", props.msg);
   }, [props.msg]);
   useEffect(() => {
     print();
   }, [print]);
 };
 ​
 export default App;
 ​

缓存子组件 props 中的引用类型。

做这一步的目的是为了防止组件非必要的重新渲染造成的性能消耗,所以首先要明确组件在什么情况下会重新渲染。

  1. 组件的 props 或 state 变化会导致组件重新渲染
  2. 父组件的重新渲染会导致其子组件的重新渲染

这一步优化的目的是:在父组件中跟子组件没有关系的状态变更导致的重新渲染可以不渲染子组件,造成不必要的浪费。

大部分时候我们是明确知道这个目的的,但是很多时候却并没有达到目的,存在一定的误区:

误区一:

 import { useCallback, useState } from "react";
 ​
 const Child = (props) => {};
 const App = () => {
   const handleChange = useCallback(() => {}, []);
   const [count, setCount] = useState(0);
   return (
     <>
       <div
         onPress={() => {
           setCount(count + 1);
         }}
       >
         increase
       </div>
       <Child handleChange={handleChange} />
     </>
   );
 };
 ​
 export default App;
 ​

项目中有很多地方存在这样的代码,实际上完全不起作用,因为只要父组件重新渲染,Child 组件也会跟着重新渲染,这里的 useCallback 完全是白给的。

误区二:

 import { useCallback, useState, memo } from "react";
 ​
 const Child = memo((props) => {});
 const App = () => {
   const handleChange = () => {};
   const [count, setCount] = useState(0);
   return (
     <>
       <div
         onPress={() => {
           setCount(count + 1);
         }}
       >
         increase
       </div>
       <Child handleChange={handleChange} />
     </>
   );
 };
 ​
 export default App;
 ​

对于复杂的组件项目中会使用 memo 进行包裹,目的是为了对组件接受的 props 属性进行浅比较来判断组件要不要进行重新渲染。这当然是正确的做法,但是问题出在 props 属性里面有引用类型的情况,例如数组、函数,如果像上面这个例子中这样书写,handleChange 在 App 组件每次重新渲染的时候都会重新创建生成,引用当然也是不一样的,那么势必会造成 Child 组件重新渲染。所以这种写法也是白给的。

正确姿势:

 import { useCallback, useState, memo, useMemo } from "react";
 ​
 const Child = memo((props) => {});
 const App = () => {
   const [count, setCount] = useState(0);
   const handleChange = useCallback(() => {}, []);
   const list = useMemo(() => {
     return [];
   }, []);
   return (
     <>
       <div
         onPress={() => {
           setCount(count + 1);
         }}
       >
         increase
       </div>
       <Child handleChange={handleChange} list={list} />
     </>
   );
 };
 ​
 export default App;
 ​
 ​

其实总结起来也很简单,memo 是为了防止组件在 props 没有变化时重新渲染,但是如果组件中存在类似于上面例子中的引用类型,还是那个原因每次渲染都会被重新创建,引用会改变,所以我们需要缓存这些值保证引用不变,避免不必要的重复渲染。

useContext 使用注意事项

  • createContext 能让你创建一个 context 以便组件能够提供和读取。

     const SomeContext = createContext(defaultValue)
    
  • useContext 是一个 React Hook,可以让你读取和订阅组件中的 context

     const value = useContext(SomeContext)
    
  • useReducer是一个 React Hook,可让您向组件添加化简器

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

在项目中我们已经重度依赖于 useContext 这个 api,同时结合 useReducer 代替 redux 来做状态管理,这也引入了一些问题。我们把官方Demo整合下,先来看看如何结合使用 useContext 和 useReducer。

 import React, { createContext, useContext, useReducer } from "react";
 ​
 const ContainerContext = createContext({ count: 0 });
 const initialState = { count: 0 };
 ​
 function reducer(state, action) {
   switch (action.type) {
     case "increment":
       return { count: state.count + 1 };
     case "decrement":
       return { count: state.count - 1 };
     default:
       throw new Error();
   }
 }
 ​
 function Counter() {
   const { state, dispatch } = useContext(ContainerContext);
   return (
     <>
       Count: {state.count}
       <button onClick={() => dispatch({ type: "decrement" })}>-</button>
       <button onClick={() => dispatch({ type: "increment" })}>+</button>
     </>
   );
 }
 ​
 function Tip() {
   return <span>计数器</span>;
 }
 ​
 function Container() {
   const [state, dispatch] = useReducer(reducer, initialState);
   return (
     <ContainerContext.Provider value={{ state, dispatch }}>
       <Counter />
       <Tip />
     </ContainerContext.Provider>
   );
 }
 ​
 export default Container;
 ​

使用起来非常方便,乍一看似乎都挺美好的,但是其实有不少陷阱或者误区在里面。

useContext 的机制是使用这个 hook 的组件在 context 发生变化时都会重新渲染。这样会导致一些问题,我把我遇到过的和能想到的问题总结到下面,如果有补充的可以再讨论。

1. Provider 单独封装

在上面的 demo 中我们应该看到了在 Provider 中有两个组件,Counter 组件在 state 发生变化的时候需要重新渲染这个没什么问题,那 Tip 组件呢,在 Tip 组件里面显然没有用到 Context 实际上是没有必要进行重新渲染的。但是现在这种写法每次state变化都会导致 Provider 中所有的子组件都跟着渲染。有没有什么办法解决呢,实际上也很简单,我们把状态管理单独封装到一个 Provider 组件里面,然后把子组件通过 props.children 的方式传进去

 ...
 function Provider(props) {
   const [state, dispatch] = useReducer(reducer, initialState);
   return (
     <ContainerContext.Provider value={{ state, dispatch }}>
       {props.children}
     </ContainerContext.Provider>
   );
 }
 ​
 const App = () => {
   return (
     <Provider>
       <Counter />
       <Tip />
     </Provider>
   );
 };
 ...
 ​

这个时候 APP 组件就成为了无状态组件,state 变化的时候 props.children 不会改变,不会被重新渲染,这个时候再看 Tip 组件,状态更新的时候就不会跟着重新渲染了。

那这样是不是就万事大吉呢,对不起没有,还有坑,接着看第二点。

2. 缓存 Provider value

官方文档里面也提到了这个坑,简单说就是,如果 Provider 组件还有父组件,当 Provider 的父组件进行重渲染时,Provider 的value 属性每次渲染都会重新创建,原理和上面 useMemo useCallback 中提到的一样,所以最好的办法是对 value 进行缓存:

 ...
 function Provider(props) {
 const [state, dispatch] = useReducer(reducer, initialState);
 const value = useMemo(() => ({ state, dispatch }), [state]);
   return (
     <ContainerContext.Provider value={value}>
       {props.children}
     </ContainerContext.Provider>
   );
 }
 ...
 ​

3. memo 优化直接被穿透,不再起作用

在开发中我们会使用 memo 来对组件进行优化,如上文中提到的,但是很多时候我们又会在使用 memo 的组件中使用 context,用 context 的地方在context发生变化的时候无论如何都会发生重新渲染,所以很多时候会导致 memo 优化实效,具体可以看这里的讨论,react 官方解释说设计如此,同时也给出了相应的建议,我们项目中主要解决方案是把 context 往上提,然后通过属性传递,就是说我们的组件一开始是这样写的:

 React.memo(()=> {
  const {count} = useContext(ContainerContext);
  return <span>{count}</span>
 })
 ​

这个时候context更新了,memo 属于是白给,我们把 context 往上提一层,其实就可以解决这个问题:

 const Child = useMemo((props)=>{
     ....
 })
 function Parent() {
   const {count} = useContext(ContainerContext);
   return <Child count={count} />;
 }
 ​

这样保证了 Child 组件的外部状态的变化只会来自于 props,这样当然 memo 可以完美工作了。

4. 对 context 进行拆分整合

context 的使用场景应该是为一组享有公共状态的组件提供便利来获取状态的变化。 但是随着业务代码越来越复杂,在不经意间我们就会把一些不相关的数据放在同一个context 里面。这样就导致了context 中任何数据的变化都会导致使用这个 context 的组件重新 render。这显然不是我们想看到的。这种情况下我们应该要对contex 进行更细粒度的拆分,把真正相关的数据整合在一起,然后再提供给组件,至少这样不相关组件的状态变化不会相互影响,也就不会导致多余的重复渲染。

useRef && forwardRef && useImperativeHandle

useRef

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

这个ref对象只有一个current属性,你把一个东西保存在内,它的地址一直不会变。

  • 如果你需要一个值,在组件不断render时保持不变

    • 初始化:const count = useRef(0)
    • 读取:count.current
  • 为什么需要current?

    • 为了保证两次useRef是同一个值(只有引用能做到)
  • 和变更数据有关的hooks

示例1

就是相当于全局作用域,一处被修改,其他地方全更新

 import React, { useRef } from "react";
 export default function App() {
   const r = useRef(0);
   console.log(r);
   const add = () => {
     r.current += 1;
     console.log(`r.current:${r.current}`);
   };
   return (
     <div className="App">
       <h1>r的current:{r.current}</h1>
       <button onClick={add}>点击+1</button>
     </div>
   );
 }

useRef变化不会主动使页面渲染,点击上方的按钮,让current+1,可以看到页面没有主动渲染,但是新的current的值已经变成了1。

 const refContainer = useRef(initialValue);

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

如果需要一个值,在组件不断render时保持不变,那就可以使用 useRef

本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。

我们改变的只是 refContainer.current 这个属性的值。refContainer 在每次渲染时的地址是不变的。useRef 会在每次渲染时返回同一个 ref 对象

请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。只能自己手动渲染。比如在变更 .current 之后,再随便setState一个数据,这会使App再次执行。

示例2

普遍操作,用来操作dom

 function CustomTextInput(props) {
   // 这里必须声明 textInput,这样 ref 才可以引用它
   const textInput = useRef(null);//创建一个包含current属性的对象
 ​
   function handleClick() {
     textInput.current.focus();
   }
 ​
   return (
     <div>
       <input type="text" ref={textInput} />
       <input type="button" value="Focus the text input" onClick={handleClick} />
     </div>
   );
 }

我们可以发现只要把ref挂到某个react元素上,就可以拿到它的dom。

一共分两个步骤

  • useRef创建一个ref对象
  • ref={xx}挂到react元素上

然后就可以使用这个元素了。官方例子是取到这个元素并且通过点击按钮让元素聚焦。我们目前大概理解了ref是怎样用的。

forwardRef

forwardRef让您的组件使用 ref 向父组件公开 DOM 节点,返回值是react组件

 import { forwardRef } from 'react';
 const MyInput = forwardRef(function MyInput(props, ref) {
   // ...
 });

何时使用 Ref?下面是几个适合使用 ref 的情况:

  • 管理焦点,文本选择或媒体播放。
  • 触发强制动画。
  • 集成第三方 DOM 库。
 function CustomTextInput(props) {
   // 这里必须声明 textInput,这样 ref 才可以引用它
   const textInput = useRef(null);//创建一个包含current属性的对象
 ​
   console.log(textInput);
   function handleClick() {
     textInput.current.focus();
   }
 ​
   return (
     <div>
       <input type="text" ref={textInput} />//挂到内部dom上
       <input type="button" value="Focus the text input" onClick={handleClick} />
     </div>
   );
 }

子组件上使用ref,上面的方法不能直接在子组件上使用,也许你会这样写

 <Child ref={textInput} />

但是这样还拿不到子组件的DOM,我们需要使用forwardRef配合使用。

如果你的函数组件想要接受别人传来的ref参数,就必须把函数组件用 forwardRef 包起来。这样就可以接受ref作为第二个参数。不然就只有props这一个参数。forwardRef 会把别人传给你的ref帮你传进来。

上面的例子可以写成这样

 function CustomTextInput(props) {
   // 这里必须声明 textInput,这样 ref 才可以引用它
   const textInput = useRef(null);
 ​
   function handleClick() {
     textInput.current.focus();
   }
   return (
     <div>
       <Child ref={textInput} />  //**依然使用ref传递**
       <input type="button" value="Focus the text input" onClick={handleClick} />
     </div>
   );
 }
 const Child = forwardRef((props, ref) => {  //** 看我 **
   return <input type="text" ref={ref} />;//** 看我挂到对应的dom上 **
 });

上面是通过forwardRefChild函数包起来,然后传入第二个参数ref最后挂载ref={ref} 这样就可以拿到对应的DOM了,控制台打印一下看看

current:

拿到子组件的DOM元素了。

useImperativeHandle

什么是 useImperativeHandle

当我们需要操作子组件中的某个 DOM 节点时,forwardRef 能很好的满足我们的需求。但是,如果我们要操作子组件中的某些方法或属性该怎么办呢?

useImperativeHandle 是 React 中的一个钩子函数,它可以暴露一个组件的 ref,从而使得父组件可以调用子组件的某些方法和属性。

useImperativeHandle 钩子函数有着非常广泛的用途,灵活运用这个钩子函数能为我们开发带来极大的便利。比如,我们在子组件中封装了一个播放器,父组件可能需要控制播放器的播放、暂停、停止等操作,这时就可以使用 useImperativeHandle 将这些操作暴露给父组件。

再比如上面通过回调函数暴露子组件中 otherOperate 的示例,我们完全可以使用 useImperativeHandle 来实现,同时父组件还能直接访问子组件的内部状态和属性。

useImperativeHandle 的基本用法

useImperativeHandle(ref, createHandle, [deps]);

useImperativeHandle 接受三个参数:

  • ref:一个 Ref 对象,通常来说,是从父组件传递过来的。
  • createHandle:一个回调函数,该函数返回一个对象,这个对象的属性和方法会被暴露给父组件。
  • [deps]:可选参数,一个数组,用于指定回调函数的依赖项。当这些依赖项发生变化时,回调函数会被重新执行。如果不指定依赖项,则回调函数只会在组件首次渲染时执行一次。

在子组件中使用 useImperativeHandle 钩子函数时,我们需要将 ref 从父组件传递过来,并在回调函数中返回一个对象。这个对象中的属性和方法会被暴露给父组件以供使用。需要注意的是,只有在回调函数中返回的对象属性和方法才会暴露出去,而子组件中的其他属性和方法则不会。

在使用 useImperativeHandle 时,我们还可以通过 [deps] 参数指定回调函数的依赖项,从而避免不必要的重复渲染。当这些依赖项发生变化时,回调函数才会被重新执行。而如果不指定依赖项,则回调函数只会在组件首次渲染时执行一次。

计数器示例

首先,我们编写计数器组件,代码如下:

import React, { forwardRef, useImperativeHandle, useState } from 'react';

// 使用 forwardRef 函数创建一个 Counter 组件,并将 ref 参数传递下去
const Counter = forwardRef((props, ref) => {
  // 使用 useState 创建一个名为 count 的状态,初始值为 0
  const [count, setCount] = useState(0);

  // 创建 increase 函数,用于增加计数器的值
  const increase = () => {
    setCount(count + 1);
  };

  // 创建函数 decrease,用于减少计数器的值
  const decrease = () => {
    setCount(count - 1);
  };

  // 使用useImperativeHandle hook,将ref暴露给父组件,并返回一个对象,对
  // 象中包含了increase和decrease两个方法,使得父组件可以直接调用这两个方法
  // 来修改计数器的值
  useImperativeHandle(ref, () => ({
    increase,
    decrease,
  }));

  // 返回一个包含当前计数器值的div元素
  return <div>{count}</div>;
});

// 导出 Counter 组件
export default Counter;

在上面的代码中,我们使用了 useImperativeHandle 来暴露 increasedecrease 两个方法,使得父组件可以直接调用这两个方法来修改计数器的值。注意,在回调函数中返回的对象属性和方法才会被暴露出来,而其他属性和方法则不会。在这里,我们只暴露了 increasedecrease 两个方法,而 count 状态则没有被暴露出来。

接下来,在父组件中引用这个计数器组件 Counter,并演示如何调用它暴露的方法来操作计数器的值。代码如下:

 import React, { useRef } from 'react';
 import Counter from './Counter';
 ​
 const App = () => {
   // 使用 useRef hook 创建一个名为 counterRef 的引用
   const counterRef = useRef();
 ​
   // 创建一个名为 handleIncrease 的函数,用于增加计数器的值
   const handleIncrease = () => {
     // 通过 counterRef.current 获取 Counter 组件实例,并调用它暴露的 increase 方法
     counterRef.current.increase();
   };
 ​
   // 创建一个名为 handleDecrease 的函数,用于减少计数器的值
   const handleDecrease = () => {
     // 通过 counterRef.current 获取 Counter 组件实例,并调用它暴露的 decrease 方法
     counterRef.current.decrease();
   };
 ​
   // 返回一个包含 Counter 组件和两个按钮的 div 元素,
   // 点击按钮会触发子组件暴露出来的 handleIncrease 和 handleDecrease 函数,从而操作计数器的值
   return (
     <div>
       <Counter ref={counterRef} />
       <button onClick={handleIncrease}>Increase</button>
       <button onClick={handleDecrease}>Decrease</button>
     </div>
   );
 };
 ​
 export default App;

在上面的代码中,我们使用 useRef 创建了一个 Ref 对象 counterRef,并将它传递给了 Counter 组件。接着,我们定义了 handleIncreasehandleDecrease 两个函数,函数内部通过 counterRef.current 分别调用计数器组件暴露出来的 increasedecrease 方法。这样,我们就可以通过父组件中的这两个按钮来增加或减少子组件计数器的值了。

怎么样,和用回调函数的方式相比是不是这种方法更加灵活呢。其实用回调函数有许多弊端,如果一个子组件接收好多个回调函数,我么维护起来会非常难受的。而使用 useImperativeHandle 钩子函数就能避免给子组件传入多个回调函数。再者,回调函数只能在触发特定的事件后才能访问到子组件暴露出来的某些方法或属性,而 useImperativeHandle 则可以随时让我们访问到子组件中的方法和属性。因此,总的来说,如果遇到需要在父组件中访问子组件中方法和属性的场景,直接上 useImperativeHandle 肯定没错。

高阶组件(HOC)

什么是高阶组件(HOC)

在 React 中,高阶组件(HOC)是一个接收组件作为参数并返回一个新组件的函数。换句话说,它是一种组件的转换器。高阶组件通常用于在组件之间复用逻辑,例如状态管理、数据获取、访问控制等。

HOC 的一个常见示例是 React-Redux 的 connect 函数,它将 Redux store 连接到 React 组件,使组件可以访问和更新 store 中的状态。

创建和使用高阶组件

让我们通过一个简单的示例来了解如何创建和使用高阶组件。我们将创建一个名为 withLoading 的高阶组件,它将在加载状态下显示一个加载指示器。

创建 withLoading 高阶组件

import React from "react";

function withLoading(WrappedComponent) {
  return function WithLoadingComponent({ isLoading, ...props }) {
    if (isLoading) {
      return <div>Loading...</div>;
    } else {
      return <WrappedComponent {...props} />;
    }
  };
}

export default withLoading;

在上述代码中,我们定义了一个 withLoading 函数,它接受一个组件作为参数(WrappedComponent),并返回一个新的组件 WithLoadingComponent。新组件接收一个名为 isLoading 的属性,如果 isLoadingtrue,则显示一个加载指示器;否则,渲染 WrappedComponent

使用 withLoading 高阶组件

假设我们有一个名为 DataList 的组件,它接收一个名为 data 的属性,并将其渲染为一个列表。我们希望在获取数据时显示一个加载指示器。为此,我们可以使用 withLoading 高阶组件。

import React from "react";
import withLoading from "./withLoading";

function DataList({ data }) {
  return (
    <ul>
      {data.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
}

const DataListWithLoading = withLoading(DataList);

export default DataListWithLoading;

在这个示例中,我们首先导入了 withLoading 高阶组件,然后使用它来包装我们的 DataList 组件。这将创建一个名为 DataListWithLoading 的新组件,它在加载状态下显示一个加载指示器,否则显示数据列表。现在我们可以在其他组件中使用 DataListWithLoading 组件,例如:

import React, { useState, useEffect } from "react";
import DataListWithLoading from "./DataListWithLoading";

function App() {
  const [data, setData] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);
      const response = await fetch("https://api.example.com/data");
      const data = await response.json();
      setData(data);
      setIsLoading(false);
    };

    fetchData();
  }, []);

  return (
    <div>
      <h1>Data List</h1>
      <DataListWithLoading data={data} isLoading={isLoading} />
    </div>
  );
}

export default App;

在这个 App 组件中,我们使用 useStateuseEffect Hooks 来获取数据,并在数据获取过程中设置 isLoadingtrue。我们将 dataisLoading 作为属性传递给 DataListWithLoading 组件。当数据正在加载时,组件将显示加载指示器;当数据加载完成时,组件将显示数据列表。

这个示例展示了如何使用高阶组件来为现有组件添加额外的功能(在本例中是加载状态)而无需修改现有组件的代码。

高阶组件的应用场景

高阶组件在实际应用中有多种用途:

  1. 复用逻辑:HOC 可以帮助我们在组件之间复用逻辑,避免重复代码。在上面的示例中,我们可以将加载状态的逻辑复用在多个组件中,而无需在每个组件中单独实现。
  2. 修改 props:HOC 可以用来修改传递给组件的 props,从而改变组件的行为。例如,我们可以使用 HOC 来根据权限级别显示或隐藏组件的某些部分。
  3. 条件渲染:HOC 可以用来根据特定条件决定是否渲染组件。例如,在上面的示例中,我们根据 isLoading 属性的值来决定是渲染加载指示器还是渲染 WrappedComponent
  4. 提供额外的功能:HOC 可以用来为组件提供额外的功能,例如错误处理、性能监控或者数据获取。

高阶组件示例

接下来,我们将介绍一些更高级的高阶组件示例,以展示其在实际项目中的应用。

示例 1:权限控制

假设我们有一个应用程序,需要根据用户权限来显示或隐藏某些组件。我们可以创建一个名为 withAuthorization 的高阶组件来实现此功能。

import React from "react";

function withAuthorization(WrappedComponent, requiredPermission) {
  return function WithAuthorizationComponent({ userPermission, ...props }) {
    if (userPermission >= requiredPermission) {
      return <WrappedComponent {...props} />;
    } else {
      return <div>您没有查看此内容的权限。</div>;
    }
  };
}

export default withAuthorization;

在这个高阶组件中,我们接受一个 WrappedComponent 和一个 requiredPermission 作为参数。然后,我们返回一个新的组件,该组件检查 userPermission 是否大于等于 requiredPermission。如果满足条件,则渲染 WrappedComponent;否则,显示一条权限不足的消息。

我们可以使用 withAuthorization 高阶组件来包装需要进行权限控制的组件,例如:

import React from "react";
import withAuthorization from "./withAuthorization";

function AdminDashboard({ data }) {
  // ... 管理面板
}

const AdminDashboardWithAuthorization = withAuthorization(AdminDashboard, 3);

export default AdminDashboardWithAuthorization;

在这个示例中,我们将 AdminDashboard 组件与 withAuthorization 高阶组件结合使用,要求用户具有 3 级权限才能查看此组件。

示例 2:错误边界

在 React 中,错误边界是一种用于捕获子组件树中发生的错误并显示友好错误信息的技术。我们可以使用高阶组件来实现一个通用的错误边界组件。

 import React, { Component } from "react";
 ​
 function withErrorBoundary(WrappedComponent) {
   return class WithErrorBoundaryComponent extends Component {
     constructor(props) {
       super(props);
       this.state = { hasError: false };
     }
 ​
     static getDerivedStateFromError() {
       return { hasError: true };
     }
 ​
     componentDidCatch(error, info) {
       // 处理错误记录
       console.error("Error:", error, "Info:", info);
     }
 ​
     render() {
       if (this.state.hasError) {
         return <div>Something went wrong. Please try again later.</div>;
       }
       return <WrappedComponent {...this.props} />;
     }
   };
 }
 ​
 export default withErrorBoundary;

在这个高阶组件中,我们返回一个类组件,因为错误边界需要使用生命周期方法 componentDidCatch 和静态方法 getDerivedStateFromError。我们在组件的状态中记录是否发生了错误,并在渲染方法中根据 hasError 的值来决定是显示错误消息还是渲染 WrappedComponent

我们可以使用 withErrorBoundary 高阶组件来包装任何需要捕获错误的组件,例如:

 import React from "react";
 import withErrorBoundary from "./withErrorBoundary";
 ​
 function UserProfile({ user }) {
   // ... 用户配置文件
 }
 ​
 const UserProfileWithErrorBoundary = withErrorBoundary(UserProfile);
 ​
 export default UserProfileWithErrorBoundary;

在这个示例中,我们将 UserProfile 组件与 withErrorBoundary 高阶组件结合使用。当 UserProfile 组件或其子组件发生错误时,用户将看到一个友好的错误消息。

示例 3:性能监控

假设我们想要跟踪某些组件的性能指标,例如渲染时间。我们可以使用高阶组件来实现这个功能。

import React, { useEffect, useRef } from "react";

function withPerformance(WrappedComponent) {
  return function WithPerformanceComponent(props) {
    const startTime = useRef(Date.now());

    useEffect(() => {
      const endTime = Date.now();
      const renderTime = endTime - startTime.current;
      console.log(`${WrappedComponent.name} render time: ${renderTime} ms`);
    }, []);

    return <WrappedComponent {...props} />;
  };
}

export default withPerformance;

在这个高阶组件中,我们使用 useRefuseEffect Hooks 来计算 WrappedComponent 的渲染时间。当组件被渲染时,我们记录开始时间,然后在 useEffect 中计算渲染所花费的时间,并将结果打印到控制台。

我们可以使用 withPerformance 高阶组件来包装需要监控性能的组件,例如:

 import React from "react";
 import withPerformance from "./withPerformance";
 ​
 function ExpensiveComponent({ data }) {
   // ... 渲染组件
 }
 ​
 const ExpensiveComponentWithPerformance = withPerformance(ExpensiveComponent);
 ​
 export default ExpensiveComponentWithPerformance;

在这个示例中,我们将 ExpensiveComponent 组件与 withPerformance 高阶组件结合使用。每当 ExpensiveComponent 组件被渲染时,我们将在控制台中看到渲染时间。

结论

高阶组件是 React 中一种强大的模式,可以帮助我们在组件间复用逻辑、修改 props、实现条件渲染以及提供额外的功能。通过熟练掌握高阶组件的概念和使用方法,我们可以提高代码的可维护性和可读性,构建更加健壮、高效的应用程序。在实际项目中,我们可能会遇到各种高阶组件的应用场景,因此掌握高阶组件的使用方法对于 React 开发者来说至关重要。