岁岁年,碎碎念

React Hooks 草稿

2023.07.24     933

前言

React v16.8 版本引入了全新的 React Hook API,颠覆了以前的用法。看了很多文章,都认为是 React 的未来,简单介绍介绍。

类组件

看个简单类组件例子

import React, { Component } from 'react';

class ClassComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  incrementCount = () => {
    this.setState((prevState) => ({
      count: prevState.count + 1,
    }));
  };

  decrementCount = () => {
    this.setState((prevState) => ({
      count: prevState.count - 1,
    }));
  };

  render() {
    const { count } = this.state;

    return (
      <div>
        <h1>Class Component Example</h1>
        <p>Count: {count}</p>
        <button onClick={this.incrementCount}>Increment</button>
        <button onClick={this.decrementCount}>Decrement</button>
      </div>
    );
  }
}

export default ClassComponent;

可以看出,代码还是很重的。什么?看不出来?

用 React Hooks 重写下

import React, { useState } from 'react';

function FunctionalComponent() {
  const [count, setCount] = useState(0);

  const incrementCount = () => {
    setCount(prevCount => prevCount + 1);
  };

  const decrementCount = () => {
    setCount(prevCount => prevCount - 1);
  };

  return (
    <div>
      <h1>Functional Component Example with React Hook</h1>
      <p>Count: {count}</p>
      <button onClick={incrementCount}>Increment</button>
      <button onClick={decrementCount}>Decrement</button>
    </div>
  );
}

export default FunctionalComponent;

是不是简洁一些,如果功能复杂起来,多个类组件层级变多,状态变多,代码也会变得很复杂。

Redux 的作者 Dan Abramov 总结 了组件类的几个缺点。

  • 大型组件很难拆分和重构,也很难测试。
  • 业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
  • 组件类引入了复杂的编程模式,比如 render props 和高阶组件。

Master GPT Q&A

为什么叫 React Hooks?

React Hooks 之所以被称为"Hooks",是因为它们提供了一种让你“钩入”(hook into)React特性和生命周期的方式。它们允许你在函数组件中使用 React 的特性,而不需要转换为类组件。

通常,在 React 中,组件是通过类或函数进行定义的。类组件使用类声明,可以使用生命周期方法来处理状态和副作用。而函数组件是简单的 JavaScript 函数,之前无法直接拥有状态和生命周期方法。

React Hooks 引入后,函数组件也能够使用状态和处理副作用等特性,同时提供了一组可以“钩入”到 React 的功能的函数。这些函数可以让你在函数组件中实现与类组件相似的功能,例如管理状态、处理副作用、访问上下文等。

通过这种"钩入"的方式,Hooks 可以让你在函数组件中执行类似于类组件的行为,但更加简洁和直观。这样的设计思想使得 React Hooks 成为一个功能强大、易于使用的工具,为 React 开发带来了更多灵活性和可维护性。

什么是副作用?

在计算机科学和编程中,副作用(Side Effect)是指函数或表达式在执行过程中,除了返回一个值之外,还对其它外部状态进行了改变或产生了一些可观察的影响。这些外部状态可以是函数外部的变量、文件、网络请求、浏览器窗口、全局对象等。

在React中,常见的副作用包括但不限于以下情况:

  1. 数据获取与订阅:从服务器或其他数据源获取数据,进行异步操作时,会产生副作用。

  2. DOM 操作:直接操作 DOM 元素,改变元素的样式、属性等,也是副作用。

  3. 事件监听与处理:添加事件监听器,例如监听鼠标点击、键盘输入等,也会产生副作用。

  4. 定时器操作:设置定时器或延时操作,从而在一段时间后触发特定操作。

  5. 状态管理:使用全局状态管理工具(如 Redux)或 Context API,在组件之间共享状态。

为了避免副作用导致不可预测的行为,React 推荐在合适的时机使用 useEffect Hook 来处理副作用。useEffect Hook 允许在函数组件中执行副作用操作,并在组件渲染时或状态变化时触发,同时还支持副作用的清除,以确保副作用的正确执行和资源的释放。

使用 useEffect 可以帮助开发者在函数组件中完成数据获取、订阅、DOM 操作等副作用操作,同时保持代码的清晰和可维护性。

为什么 React 需要 React Hooks?

React Hooks 是为了解决 React 中组件之间状态逻辑复用的问题而引入的。在 React 16.8 版本中,React Hooks 被正式引入,并提供了一种新的方式来在函数组件中管理状态和副作用,取代了之前类组件中的状态管理和生命周期方法。

在引入 Hooks 之前,组件的状态逻辑复用主要依靠高阶组件(Higher-Order Components)和渲染属性(Render Props)等模式。虽然这些模式在某些场景下能够发挥作用,但它们往往需要创建辅助组件,导致代码变得复杂,尤其是在涉及多个状态和副作用的情况下。

React Hooks 提供了一种更简洁、直观的方式来在函数组件中处理状态和副作用,让组件之间的逻辑复用变得更加容易。它解决了以下一些问题:

  1. 状态逻辑复用:通过使用 useStateuseReducer 等 Hook,我们可以在函数组件中使用状态,而不需要将组件转换为类组件或使用其他模式来实现状态管理。

  2. 副作用处理:以前,在类组件中处理副作用(如数据获取、订阅和取消订阅等)需要在生命周期方法中处理。使用 Hooks 的 useEffect,我们可以更直观地定义副作用,使得代码更易于理解和维护。

  3. 更简洁的代码:相比于类组件,使用 Hooks的函数组件通常会更加简洁,因为不再需要定义繁琐的生命周期方法。

  4. 避免类组件的限制:在 React 之前的版本中,只有类组件才能拥有状态和生命周期方法。引入 Hooks 后,函数组件获得了与类组件相媲美的功能,使得函数组件成为编写组件的首选方式。

总之,React Hooks的出现是为了提供一种更现代、更灵活的方式来处理组件的状态和副作用,使得 React 组件的编写更加简单、直观,并促进代码的可重用性和可维护性。

React Hooks 核心是什么?

React Hooks 的核心概念是用于在函数组件中添加状态管理和处理副作用的特殊函数。这些函数由 React 提供,并以 use 开头,如 useStateuseEffectuseContext 等。

以下是 React Hooks 的核心概念:

  1. useState: useState 是最基本的 Hooks 之一,用于在函数组件中添加状态。它返回一个状态值和一个更新该状态值的函数,让你可以在函数组件中保存和更新状态。

  2. useEffect: useEffect 用于处理副作用,比如数据获取、订阅和取消订阅等。它在组件渲染到屏幕后执行,可以帮助你在组件的生命周期中执行类似于 componentDidMountcomponentDidUpdatecomponentWillUnmount 等生命周期方法的操作。

  3. useContext: useContext 用于访问 React 的上下文(Context),让你能够在组件中跨层级共享数据,而不需要通过组件树逐层传递 props。

  4. useReducer: useReducer 是另一种状态管理 Hook,它类似于 Redux 中的 reducer。useReducer 让你能够更复杂地管理组件的状态,尤其是当状态之间存在关联性时。

  5. useCallback: useCallback 用于在组件渲染时缓存回调函数,避免在每次渲染时重新创建新的回调函数,从而优化性能。

  6. useMemo: useMemo 用于在组件渲染时缓存计算结果,避免在每次渲染时重复计算相同的值,也是为了性能优化而提供的 Hook。

  7. useRef: useRef 返回一个可变的 ref 对象,可以用于在组件渲染之间存储持久值,而不会触发重新渲染。

通过使用这些 Hook,React 允许我们在函数组件中拥有状态和处理副作用的能力,从而更好地管理组件的行为和状态,使代码更加简洁、可维护,并且提高组件的性能。

详细了解常见 Hooks

先看官网介绍,中文官网在这里,讲得非常好 👏🏻

让我们逐个来讲解这些具体的 Hooks:

  1. useState: useState 是最常用的 Hook 之一,用于在函数组件中添加状态。它返回一个状态值和一个更新该状态值的函数。使用方式如下:

    import React, { useState } from 'react';
    
    function Counter() {
      const [count, setCount] = useState(0);
    
      const increment = () => {
        setCount(count + 1);
      };
    
      return (
        <div>
          <p>Count: {count}</p>
          <button onClick={increment}>Increment</button>
        </div>
      );
    }
    
  2. useEffect: useEffect 用于处理副作用,如数据获取、订阅和取消订阅等。它在每次组件渲染完成后执行。可以通过返回一个清除函数来清理副作用。使用方式如下:

    import React, { useState, useEffect } from 'react';
    
    function DataFetcher() {
      const [data, setData] = useState([]);
    
      useEffect(() => {
        const fetchData = async () => {
          const response = await fetch('https://api.example.com/data');
          const data = await response.json();
          setData(data);
        };
    
        fetchData();
    
        return () => {
          // Clean up any subscriptions or timers here
        };
      }, []);
    
      return (
        <div>
          <ul>
            {data.map(item => (
              <li key={item.id}>{item.name}</li>
            ))}
          </ul>
        </div>
      );
    }
    
  3. useContext: useContext 用于访问 React 的上下文(Context),让你能够在组件中跨层级共享数据,而不需要通过组件树逐层传递 props。使用方式如下:

    import React, { useContext } from 'react';
    
    const UserContext = React.createContext();
    
    function UserProfile() {
      const user = useContext(UserContext);
    
      return (
        <div>
          <h2>User Profile</h2>
          <p>Name: {user.name}</p>
          <p>Email: {user.email}</p>
        </div>
      );
    }
    
    function App() {
      const user = { name: 'John Doe', email: 'john@example.com' };
    
      return (
        <UserContext.Provider value={user}>
          <UserProfile />
        </UserContext.Provider>
      );
    }
    
  4. useReducer: useReducer 是另一种状态管理 Hook,类似于 Redux 中的 reducer。useReducer 可以用于更复杂的状态管理,尤其是当状态之间存在关联性时。使用方式如下:

    import React, { useReducer } from 'react';
    
    const initialState = { count: 0 };
    
    const reducer = (state, action) => {
      switch (action.type) {
        case 'INCREMENT':
          return { count: state.count + 1 };
        case 'DECREMENT':
          return { count: state.count - 1 };
        default:
          return state;
      }
    };
    
    function Counter() {
      const [state, dispatch] = useReducer(reducer, initialState);
    
      const increment = () => {
        dispatch({ type: 'INCREMENT' });
      };
    
      const decrement = () => {
        dispatch({ type: 'DECREMENT' });
      };
    
      return (
        <div>
          <p>Count: {state.count}</p>
          <button onClick={increment}>Increment</button>
          <button onClick={decrement}>Decrement</button>
        </div>
      );
    }
    
  5. useCallback: useCallback 用于在组件渲染过程中缓存一个回调函数,以避免在每次渲染时创建新的回调函数。这在遇到需要传递给子组件的回调函数时非常有用,因为这样可以确保子组件不会因为父组件重新渲染而触发不必要的重新渲染。使用方式如下:

    import React, { useState, useCallback } from 'react';
    
    function ParentComponent() {
      const [count, setCount] = useState(0);
    
      // 使用 useCallback 来缓存 increment 函数
      const increment = useCallback(() => {
        setCount(prevCount => prevCount + 1);
      }, []);
    
      return (
        <div>
          <p>Count: {count}</p>
          <ChildComponent onIncrement={increment} />
        </div>
      );
    }
    
    function ChildComponent({ onIncrement }) {
      return (
        <div>
          <button onClick={onIncrement}>Increment</button>
        </div>
      );
    }
    

    在这个例子中,我们使用 useCallback 来缓存 increment 函数,确保 increment 不会在父组件重新渲染时重新创建,从而避免不必要的子组件重新渲染。

  6. useMemo: useMemo 用于在组件渲染过程中缓存计算结果,以避免在每次渲染时重复计算相同的值。这在处理复杂的计算或昂贵的操作时很有用,可以优化性能。使用方式如下:

    import React, { useState, useMemo } from 'react';
    
    function ComplexCalculationComponent() {
      const [num1, setNum1] = useState(0);
      const [num2, setNum2] = useState(0);
    
      // 使用 useMemo 来缓存计算结果
      const result = useMemo(() => {
        console.log('Calculating...');
        return num1 + num2;
      }, [num1, num2]);
    
      return (
        <div>
          <input
            type="number"
            value={num1}
            onChange={e => setNum1(Number(e.target.value))}
          />
          <input
            type="number"
            value={num2}
            onChange={e => setNum2(Number(e.target.value))}
          />
          <p>Result: {result}</p>
        </div>
      );
    }
    

    在这个例子中,我们使用 useMemo 来缓存 result 变量的计算结果,只有当 num1num2 发生变化时,才会重新计算 result

  7. useRef: useRef 返回一个可变的 ref 对象,用于在组件渲染之间存储持久值,而不会触发重新渲染。useRef 创建的引用在组件的整个生命周期内保持不变。使用方式如下:

    import React, { useRef } from 'react';
    
    function FocusableInput() {
      const inputRef = useRef(null);
    
      const handleButtonClick = () => {
        // 在按钮点击时,聚焦到输入框
        inputRef.current.focus();
      };
    
      return (
        <div>
          <input ref={inputRef} type="text" />
          <button onClick={handleButtonClick}>Focus Input</button>
        </div>
      );
    }
    

    在这个例子中,我们使用 useRef 来创建 inputRef,并将其赋值给 <input> 元素的 ref 属性。这样,我们可以在按钮点击时,通过 inputRef.current 访问到 <input> 元素,并聚焦到它,而不会触发组件的重新渲染。

其中 useCallbackuseMemouseRef 这些 Hook 是用于性能优化和缓存的利器。使用它们可以减少不必要的计算、避免不必要的组件重新渲染,从而提高 React 应用的性能和用户体验。

自定义 Hooks

自定义 Hooks 应该以 use 开头,并使用其他 React Hooks。

创建一个自定义的 Hook,用于处理窗口大小的变化:

import React, { useState, useEffect } from 'react';

// 自定义 Hook,用于获取窗口大小
function useWindowSize() {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    // 创建一个事件处理函数,用于更新窗口大小
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    // 添加窗口大小变化的事件监听
    window.addEventListener('resize', handleResize);

    // 在组件卸载时清除事件监听
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空数组表示只在组件挂载和卸载时运行 useEffect

  return windowSize;
}

function WindowSizeComponent() {
  // 使用自定义的 useWindowSize Hook 获取窗口大小
  const windowSize = useWindowSize();

  return (
    <div>
      <h2>Window Size</h2>
      <p>Width: {windowSize.width}px</p>
      <p>Height: {windowSize.height}px</p>
    </div>
  );
}

export default WindowSizeComponent;

在上面的示例中,我们创建了一个名为 useWindowSize 的自定义 Hook。它利用了 useStateuseEffect 这两个 React Hooks。useWindowSize Hook 返回一个包含当前窗口宽度和高度的对象。

WindowSizeComponent 组件中,我们使用了自定义的 useWindowSize Hook 来获取窗口大小,并将它展示在组件中。每当窗口大小变化时,useWindowSize Hook 将会更新窗口的宽度和高度,从而使组件能够动态显示窗口大小。

这样,通过创建自定义的 React Hooks,你可以将逻辑抽象成可重用的函数,并在应用程序中多次使用它们,提高代码的复用性和可维护性。

资料

[★★★★★] https://react.dev/reference/react

[★★★★☆] https://zh-hans.react.dev/reference/react

[★★★★☆] https://www.ruanyifeng.com/blog/2019/09/react-hooks.html