React Hook介绍

31 阅读9分钟

React Hook介绍

简介

介绍:legacy.reactjs.org/docs/hooks-…

使用:react.dev/reference/r…

什么是 Hook?

Hook 是 React 16.8 的新增特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。其本质上就是一类特殊JavaScript函数,它们约定以 use 开头,可以为 React Function Component 注入一些功能,赋予 Function Component 一些类似 Class Component 所具备的能力,比如状态管理和生命周期

为什么引入 Hook?

  • 在组件之间复用状态逻辑很难
  • 复杂组件变得难以理解
  • 难以理解的 class

常见Hooks

  • useState
    • 作用:用于在函数组件中添加状态。可以给任何类型的值添加状态(基础数据类型、引用数据类型、自定义数据结构等)
    • 如何使用:
    • 使用场景:当你需要在组件中存储一些可变的值,并且当这些值变化时需要重新渲染组件
    • 注意事项:1、更新状态会使用新的状态值请求另一个渲染,但并不影响你已经运行的函数中的 状态的 的值。

2、引用数据类型更新时,需赋值新对象,而不是在旧对象上做修改。

3、存储 一个函数,需要加上 () => ,否则会被直接调用(见下方源码)

// 声明一个叫 “count” 的 state 变量。
  const [count, setCount] = useState(0);
  
  function addCount(){
    console.log("addCount start:", count);
    setCount(count + 1);
    console.log("addCount end:", count);
    
     //先保存在一个变量中
    // const nextCount = count + 1;
    // setCount(nextCount);
    // console.log(count); // 0
    // console.log(nextCount); // 1
  }

 function startCountInterval1(){
    setInterval(() => {
      console.log("startCountInterval start:", count);
      setCount(count + 1);
    }, 1000);
  }
  function startCountInterval2() {
    let count = 10;
    setInterval(() => {
      console.log("startCountInterval start:", count);
      count++;
    }, 1000);
  }

/**
 * React 会在 `person` 对象的内存引用发生变化时触发更新。
 * 如果直接修改 `person` 对象的属性而没有生成一个新的对象引用,
 * React 将不会识别状态已经改变,因此不会重新渲染组件。
 */
const [person,setPreson] = useState({name: '张三', age: 18});

function onSetPreson(){
    //页面会刷新
    setPreson({...person, name: '李四'});
    
    //页面不会刷新
    person.name = '李四';
    setPreson(person);
  }

//源码:如果传入一个函数,会被直接调用,生存初始值,所以想存储一个函数的状态,需要使用()=>
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    const initialStateInitializer = initialState;
    // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
    initialState = initialStateInitializer();
    if (shouldDoubleInvokeUserFnsInHooksDEV) {
      setIsStrictModeForDevtools(true);
      // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
      initialStateInitializer();
      setIsStrictModeForDevtools(false);
    }
  }
  • useEffect
    • 作用:用于处理副作用,类似于类组件的componentDidMount,componentDidUpdate, 和componentWillUnmount生命周期方法。副作用是指那些与组件的渲染无直接关联的操作,比如数据获取、订阅、或手动更改DOM等。
    • 如何使用:
    • 使用场景:1、数据获取:在组件渲染后获取异步数据。

2、订阅事件:在组件订阅外部事件,比如 Redux store 的变化。

3、手动操作 DOM:在组件渲染后对 DOM 进行操作。

  • 注意事项: 1、注意闭包陷阱,正确添加依赖(所有响应式值的列表),安装eslint插件,会帮助检测依赖

2、正确处理副作用的清理,特别是订阅和定时器

3、分离不想依赖的响应式值(useEffectEvent

const [count, setCount] = useState(0);
const [count2, setCount2] = useState(0);
const [seconds, setSeconds] = useState(0)

useEffect(() => {
    console.log("每次页面渲染都会执行")
  });
  useEffect(() => {
    console.log("只在页面初次渲染执行")
  }, []);
  useEffect(() => {
    console.log("只在 count 变化时执行")
  }, [count])


//注意闭包陷阱
useEffect(() => {
    //设置一个定时器
    console.log("设置一个定时器")
    let timer = setInterval(() => {
      setSeconds(preSec => {
        console.log('preSec:', preSec)
        return preSec + 1
      })
        //setSeconds(seconds + 1);
        //这里会打印什么
       //console.log('seconds:', seconds)
    }, 1000)
    // 移除副作用
    return () => {
      console.log("移除定时器")
      clearInterval(timer)
    }
  }, [])

function Page({ url ,numberOfItems}) {
    useEffect(() => {
        logVisit(url, numberOfItems);
    }, [url]); // 🔴 React Hook useEffect 缺少依赖项: ‘numberOfItems’
    // ...
}
  • useContext
    • 作用:用于让你访问 React 的 Context API,以便在组件树中传递数据。
    • 如何使用:1、创建context:createContext;2、使用CountContext.Provider 给value 赋值;3、在子组件中使用useContext
    • 使用场景:当需要在 React 应用中跨多层组件传递数据时,比如修改主题,用户身份验证,多语言支持
    • 注意事项:1、确保不要滥用 context,可能会导致组件不必要的重新渲染。当需要在多层嵌套的组件中共享全局数据时使用

2、确保提供了默认值,避免在没有 Provider 的情况下出错

//1、创建context:createContext
const  ThemeContext = createContext('light');

function UseContextComponent() {
  const [theme,setTheme]= useState('light');
  
  return (
    //2、使用CountContext.Provider  给value 赋值
    <ThemeContext.Provider value={theme}>
      <div >
        <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>修改主题</button>
        <ChildComponent/>

      </div>
    </ThemeContext.Provider>
  );
}
export default UseContextComponent;


function ChildComponent() {
  //3、在子组件中使用useContext
  const theme = useContext(ThemeContext);
  return (
    <div className={`div-${theme}`}>
        <p>agajfi</p>
        <br/>
        <button className={`button-${theme}`}>添加按钮</button>
        <br/>
        <br/>
    </div>
  );
}
  • useRef
    • 作用:用于在函数组件中创建一个可变的ref对象,可以用来保存任何可变值,类似于在类组件中使用实例字段
    • 如何使用:
    • 使用场景:1、通过 ref 操作 DOM;2、当需要在函数组件中保存一个可变值,但又不希望因为该值的改变而导致组件重新渲染时
    • 注意事项:1、当 ref 对象内容变化时,不会触发组件重新渲染

2、除了初始化外不要在渲染期间写入或者读取ref.current,否则会使组件行为变得不可预测。

3、无法获取自定义组件的dom节点 ,需要借助forwardRef

//1、通过 ref 操作 DOM  
//1.1、声明一个 初始值 为 null 的 ref 对象
  const inputRef = useRef<HTMLInputElement>(null);

 //1.3 当 React 创建 DOM 节点并将其渲染到屏幕时,React 将会把 DOM 节点设置为 ref 对象的 current 属性。
  //现在可以借助 ref 对象访问 <input> 的 DOM 节点
  const showDom = () => {
    if (inputRef.current !== null) {
      console.log(inputRef.current.value);
    }
  };

  <label>
        请输入文本内容
        {/* 1.2 将 ref 对象作为 ref 属性传递给想要操作的 DOM 节点的 */}
        <input type="text" ref={inputRef} />
        <button onClick={showDom}>获取dom</button>
   </label>


  //2、下面代码中 timeoutID是普通变量,在组件重新渲染时,会重新置为null,所以停止不了延时消息,这个时候需要使用useRef
  const [isSending, setIsSending] = useState(false);
  let timeoutID: NodeJS.Timeout;
  // const timeoutID = useRef<NodeJS.Timeout>();

  console.log("渲染");
  function handleSend() {
    setIsSending(true);
    timeoutID = setTimeout(() => {
      console.log("已发送");
      setIsSending(false);
    }, 3000);
    console.log("点击发送", timeoutID);
  }

  function handleUndo() {
    setIsSending(false);
    clearTimeout(timeoutID);
    console.log("终止发送", timeoutID);
  }
  • useCallback
    • 作用:用于在函数组件中缓存函数,以避免不必要的重复渲染。
    • 如何使用:
    • 使用场景:1、将函数作为 props 传递给包装在 [memo] 中的组件。如果 props 未更改,则希望跳过重新渲染。Memoization允许组件仅在依赖项更改时重新渲染

2、传递的函数可能作为某些 Hook 的依赖。比如依赖于useEffect中的函数。

3、优化自定义 Hook

  • 注意事项:1、过度使用可能会导致性能问题,useCallback 钩子需要在内存中保持函数的引用,这意味着增加了内存的使用,如果你不确定是否需要它,最好是先不用,等到性能成为问题时再考虑引入 useCallback

2、正确添加依赖,注意闭包陷阱。遗漏了依赖项,那么当这些依赖项改变时,缓存的函数可能会引用旧的变量或状态值,导致错误或不一致的行为。添加了不必要的依赖项,函数可能会过于频繁地重新创建。

3、不允许在循环中为每一个列表项调用 useCallback 函数

function UseCallBackComponent() {

  const [count,setCount] = useState(0);
  const [count1,setCount1] = useState(0);

  const callbackFun = useCallback(()=>{
    console.log('count',count);
  },[count]);

  // const callbackFun = ()=>{
  //   console.log('count',count);
  // };

  return (
    <div >
    <button onClick={() => setCount(count + 1)}>click count: {count}</button>
    <button onClick={() => setCount1(count1 + 1)}>click count1: {count1}</button>
    <ChildComponent callbackFun={callbackFun}/>
    </div>
  );
}

const ChildComponent = memo(function ChildComponent(props:any) {
  console.log('child render',props.callbackFun);
  return <div>
    我是子组件
    <button onClick={props.callbackFun}>callback</button>
  </div>
});
  • useMemo
    • 作用:用于在函数组件中缓存计算结果
    • 如何使用:
    • 使用场景:1、跳过代价昂贵的重新计算(如何衡量计算过程的开销是否昂贵)

你在 useMemo 中进行的计算明显很慢,而且它的依赖关系很少改变

  • 注意事项:

1、页面初次渲染或者依赖发生变化时,会执行useMemo的计算。

2、不保证被记忆,React 可能会在必要时清除记忆化的值。

3、仅在性能优化成为问题时使用,不要过度使用以避免增加复杂性

4、正确添加依赖。

5、不允许为循环中的每个列表项调用 useMemo

function UseMemoComponent() {
  const [count, setCount] = useState(0);
  const [price, setPrice] = useState(10);

  const countSum = useMemo(() => {
    //复杂运算
    console.log("useMemo count", count, "price", price);
    return count * price;
  }, [count]);

  // const countSum = ()=>{
  //   console.log("count", count,'price',price);
  //   return count * price;
  // }

  const getSum = () => {
    console.log("countSum", countSum);
    // console.log("countSum", countSum());
  };

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>click count: {count}</button>
      <button onClick={() => setPrice(price + 1)}>changePrice: {price}</button>
      <button onClick={getSum}>getAdd</button>
    </div>
  );
}
  • useReducer
    • 作用:用于在函数组件中添加状态,适用于状态逻辑复杂的场景,或者当下一个状态依赖于之前的状态时。
    • 如何使用:
    • 使用场景:1、状态逻辑复杂,涉及多个子值或下一个状态依赖于之前的状态。

2、状态更新逻辑可能在多个回调中重用。

3、你想要将组件的业务逻辑外提到组件以外,以便进行单元测试。

总的来说,useState 适用于简单的状态管理,而 useReducer 则适用于处理更复杂的状态逻辑

  • 注意事项:1、包含useState的注意事项

2、对于简单的状态管理,使用 useState 就足够了,只有当状态逻辑变得复杂时才考虑使用 useReducer。

// 1、定义reducer函数
function reducer(state: { count: number }, action: any) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

export default function UseReducerComponanent() {
  // 2、使用useReducer并传入reducer函数和初始状态
  const [state, dispatch] = useReducer(reducer, { count: 5 });

  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </>
  );
}
  • 自定义hook

react hooks中的自定义hook

Hooks 使用的注意事项

  • 只在最顶层使用 Hook,不要在循环、条件或嵌套函数中调用 Hook:确保 Hook在每次组件渲染时都以相同的顺序被调用。
  • 只在 React 函数中调用 Hook:在 React 的函数组件中调用 Hook,在自定义 Hook 中调用其他 Hook。
  • 为了避免不必要的渲染,正确使用依赖项数组:尤其是在useEffect,useCallback, 和useMemo中。
  • 优化性能:使用useCallbackuseMemo时,确保它们是必要的,因为它们会增加应用程序的内存使用。
  • 规则的遵守:使用 ESLint 插件来强制执行 Hooks 规则,帮助避免常见错误。
  • 严格模式:在开发模式下,当组件被包裹在 <React.StrictMode> 组件中时,React 会对组件进行两次初始化,useEffect 和 useLayoutEffect 的回调会在每次渲染后执行两次,useState 和其他 Hooks 的初始化会调用两次,这是为了帮助开发者发现组件中的一些常见问题,比如副作用(side effects)不应该在组件的初始化阶段执行。

hook源码解析:cloud.tencent.com/developer/a…

源码:github.com/facebook/re…