React Hooks

发布于 2024-11-22 13:04:26 字数 23558 浏览 0 评论 0

Hooks

Hook 可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性 ,所以 Hook 在 class 内部是 起作用的

使用 Hook 的原因是什么?

  1. class 是学习 React 的一大屏障,你必须去理解 JavaScript 中 this 的工作方式。还不能忘记绑定事件处理器,而 Hook 解决了这一点
  2. 代码复用和代码管理

    通过使用 Hook,你可以把组件内相关的副作用组织在一起(例如创建订阅及取消订阅),而不要把它们拆分到不同的生命周期函数里。

Hook 使用规则

Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:

  • 只能在 函数最外层 调用 Hook。

    不要在 循环条件判断 或者 子函数 中调用。

  • 只能在 React 的函数组件自定义的 Hook 中调用 Hook。

    不要在其他 JavaScript 函数中调用。

安装名为 eslint-plugin-react-hooks 的 ESLint 插件来强制执行这两条规则(注意: 自定义 Hook 必须以 “ use ” 开头 ,这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则)

npm install eslint-plugin-react-hooks --save-dev

// 你的 ESLint 配置
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // 检查 Hook 的规则
    "react-hooks/exhaustive-deps": "warn" // 检查 effect 的依赖
  }
}

什么时候用 Hook?

如果你在编写函数组件并意识到需要向其添加一些 state ,以前的做法是必须将其转化为 class

现在你可以在现有的 函数组件 中使用 Hook

一、基础 Hook

useState

useState 会返回一对值: 当前 状态和一个让你更新它的函数(更新函数类似 class 组件的 this.setState ,但是它不会把新的 state 和旧的 state 进行合并)

function ExampleWithManyStates() {
  // 声明多个 state 变量!
  const [count, setCount] = useState(0);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  // ...
  setCount(1) // 函数异步执行
    // 要想正确更新数据,则使用函数
  setCount((preCount) => {
    return preCount + 1
  })
}

在函数组件中,没有 this ,所以我们不能分配或读取 this.state 。在函数中,我们可以直接用 count :

<p>You clicked {count} times</p>
setState 的执行流程

setState 的执行流程(函数组件):

  1. 先判断组件当前处于什么状态
    • 渲染阶段:不会检查 state 值是否相同
    • 非渲染阶段(渲染结束):会检查 state 值是否相同
  2. 在非渲染阶段
    • 如果值不相同,则对组件进行重渲染
    • 如果值相同,则不对组件进行重渲染
      • 但是在某些情况下,如果值相同,React 会继续执行当前组件的重渲染,但这个渲染不会触发其子组件的重渲染,这次渲染不会产生实际的效果

useEffect

useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用(在 React 组件中执行过数据获取、订阅或者手动修改过 DOM) 的能力。

专门用来处理那些不能直接写在组件内部的代码,比如: ajax 请求数据、记录日志、检查登录、设置定时器等 ,总的来说,就是那些和组件渲染无关的,但可能对组件产生副作用的代码。

useEffect Hook 是 class 组件中 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合,因此具有更好的 代码复用代码管理

useEffect 中的回调函数,在每次组件渲染完毕后执行。回调函数通过返回一个函数来指定如何“ 清除 ”副作用。

格式useEffect(() => { /* 编写那些会产生副作用的代码 */ }, deps)

下面的示例中,React 会在组件销毁时取消对 ChatAPI 的订阅。

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => { // 可选的清除函数
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }
  // ...
为什么使用 useEffect ?
  1. 关注点分离

    为了解决在使用 class 创建组件的时候,class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题。

    而使用多个 useEffect 实现关注点分离,把相关逻辑代码组合在一起,方便查找,类比于 Vue3 的 setup

Effect 的清除副作用执行时机?

effect 的清除阶段在每次重新渲染 ( componentDidUpdate ) 时都会执行,而不是仅仅只在卸载组件( componentWillUnmount ) 的时候执行一次

因为 useEffect 的 默认处理是:它会在调用一个新的 effect 之前对前一个 effect 进行清理。

// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // 运行第一个 effect

// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // 运行下一个 effect

// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一个 effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // 运行下一个 effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一个 effect

此默认行为保证了一致性,避免了在 class 组件中因为没有处理更新逻辑而导致常见的 bug。

通过跳过 Effect 进行性能优化 (使用 useEffect 的第二参数,依赖项)

在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevPropsprevState 的比较逻辑解决:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

换算成 Effect

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新。
                         // 如果 count 的值是 5,而且我们的组件重渲染的时候 count 还是等于 5,React 将对前一次渲染的 [5] 和后一次渲染的 [5] 进行比较。因为数组中的所有元素都是相等的(5 === 5),React 会跳过这个 effect,这就实现了性能的优化。

useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅

注意:

  1. 要使用此优化方式,请确保数组中包含了 所有外部作用域中会随时间变化并且在 effect 中使用的变量 ,否则你的代码会引用到先前渲染中的旧变量。
  2. 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组( [] )作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,effect 内部的 props 和 state 就会一直拥有其初始值,所以它永远都不需要重复执行,传入 [] 作为第二个参数更接近大家更熟悉的 componentDidMountcomponentWillUnmount 思维模式。

useContext

const contextValue = useContext(MyContext);

接收一个 context 对象( React.createContext 的返回值)并返回该 context 的当前值。

context 的当前值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop 决定。

useContext 需要配合 Context.Provider 使用,调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以 通过使用 memoization 来优化

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

二、额外的 Hook

useReducer : useState 的替代方案

即把对 state 的相关操作,全部放在一个作用域内

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

在某些场景下, useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为 你可以向子组件传递 dispatch 而不是回调函数

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] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}
第三参数:惰性初始化

你可以选择惰性地创建初始 state。为此,需要将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)

这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利:

function init(initialCount) {
  return {count: initialCount};
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  return (
    <>
      Count: {state.count}
      <button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>
        Reset
      </button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}
跳过 dispatch

如果 Reducer Hook 的返回值与当前 state 相同(使用 Object.is 比较算法 来比较),React 将跳过子组件的渲染及副作用的执行

useCallback

为什么使用 useCallback ?当 state 发生变化时,组件会重新渲染,并且组件内的函数、方法也会重新创建,这是没有必要的,所以需要 useCallback 进行优化,保持引用稳定。

格式:useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

如果不提供参数 deps ,那么使用 useCallback 毫无意义,当 state 发生变化时,组件会重新渲染,回调函数 fn 也会重新创建。所以使用 useCallback 提供 deps 是必要的,所有回调函数 fn 中引用的值都应该出现在依赖项数组 deps 中。

另外,如果提供的 deps[] ,那么回调函数 fn 只会在组件初始化的时候创建,内部依赖将始终为初始值。

示例:

把内联 回调函数依赖项数组 作为参数传入 useCallback ,返回一个 memoized 回调函数。该内联回调函数仅在某个依赖项改变时才会更新

const App = () => {
  const memoizedCallback = useCallback(
    () => {
      doSomething(a, b);
    },
    [a, b], // 所有回调函数中引用的值都应该出现在依赖项数组中。
  );
  return (
      <button onClick={memoizedCallback}></button>
  )
}
export default App;

推荐启用 eslint-plugin-react-hooks 中的 exhaustive-deps 规则。此规则会在添加错误依赖时发出警告并给出修复建议

useMemo 类似 vue computed

如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

返回一个 memoized 值。把 “创建”函数依赖项数组 作为参数传入 useMemo ,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。如果没有提供 依赖项数组useMemo 在每次渲染时都会计算新的值。

useRef 获取/操作 DOM

const refContainer = useRef(initialValue); 创建一个存储 DOM 对象的容器

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

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

本质上, useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。而 React.createRef() 创建的 ref,无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。

useRef()ref 属性更有用,它可以 很方便地保存任何可变值

function Timer() {
  const intervalRef = useRef();

  useEffect(() => {
    const id = setInterval(() => {
      // ...
    });
    intervalRef.current = id;
    return () => {
      clearInterval(intervalRef.current);
    };
  });

  // ...
}

注意:

当 ref 对象内容发生变化时, useRef不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用 回调 ref 来实现:

function MeasureExample() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  );
}

useImperativeHandle

格式: useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 可以让你在使用 ref 时暴露子组件中的方法给父组件。

useImperativeHandle 应当与 forwardRef 一起使用:

渲染 <FancyInput ref={inputRef} /> 的父组件可以调用 inputRef.current.focus()

import { forwardRef } from "react";

const FancyInput = forwardRef(function FancyInput(props, ref) {
  const inputRef = useRef();
  // 实现聚焦逻辑函数
  const focusHandler = () => {
    inputRef.current.focus();
  }
  // 暴露函数给父组件调用
  useImperativeHandle(ref, () => {
    return {
      focusHandler
    }
  });
  return <input ref={inputRef} ... />;
});

useLayoutEffect

尽可能使用 useEffect 以避免阻塞视觉更新

其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前, useLayoutEffect 内部的更新计划将被同步刷新。

注意:

  • useLayoutEffectcomponentDidMountcomponentDidUpdate 的调用阶段是一样的。但是,我们推荐你 一开始先用 useEffect ,只有当它出问题的时候再尝试使用 useLayoutEffect
  • 如果你使用服务端渲染,请记住,无论 useLayoutEffect 还是 useEffect 都无法在 Javascript 代码加载完成之前执行。这就是为什么在服务端渲染组件中引入 useLayoutEffect 代码时会触发 React 告警。解决这个问题,需要将代码逻辑移至 useEffect 中(如果首次渲染不需要这段逻辑的情况下),或是将该组件延迟到客户端渲染完成后再显示(如果直到 useLayoutEffect 执行之前 HTML 都显示错乱的情况下)
  • 若要从服务端渲染的 HTML 中排除依赖布局 effect 的组件,可以通过使用 showChild && <Child /> 进行条件渲染,并使用 useEffect(() => { setShowChild(true); }, []) 延迟展示组件。这样,在客户端渲染完成之前,UI 就不会像之前那样显示错乱了

useDebugValue

useDebugValue(value) : useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  // ...

  // 在开发者工具中的这个 Hook 旁边显示标签
  // e.g. "FriendStatus: Online"
  useDebugValue(isOnline ? 'Online' : 'Offline');

  return isOnline;
}

useDebugValue 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。

例如,一个返回 Date 值的自定义 Hook 可以通过格式化函数来避免不必要的 toDateString 函数调用:

useDebugValue(date, date => date.toDateString());

useDeferredValue

格式: const deferredValue = useDeferredValue(value);

useDeferredValue 接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后。如果当前渲染是一个紧急更新的结果,比如用户输入,React 将返回之前的值,然后在紧急渲染完成后渲染新的值。

该 hook 与使用防抖和节流去延迟更新的用户空间 hooks 类似。使用 useDeferredValue 的好处是,React 将在其他工作完成(而不是等待任意时间)后立即进行更新,并且像 startTransition 一样,延迟值可以暂停,而不会触发现有内容的意外降级。

useDeferredValue 仅延迟你传递给它的值。如果你想要在紧急更新期间防止子组件重新渲染,则还必须使用 React.memo 或 React.useMemo 记忆该子组件:

function Typeahead() {
  const query = useSearchQuery('');
  const deferredQuery = useDeferredValue(query);

  // Memoizing 告诉 React 仅当 deferredQuery 改变,
  // 而不是 query 改变的时候才重新渲染
  const suggestions = useMemo(() =>
    <SearchSuggestions query={deferredQuery} />,
    [deferredQuery]
  );

  return (
    <>
      <SearchInput query={query} />
      <Suspense fallback="Loading results...">
        {suggestions}
      </Suspense>
    </>
  );
}

记忆该子组件告诉 React 它仅当 deferredQuery 改变而不是 query 改变的时候才需要去重新渲染。这个限制不是 useDeferredValue 独有的,它和使用防抖或节流的 hooks 使用的相同模式。

useTransition

const [isPending, startTransition] = useTransition(); 返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数

startTransition 允许你通过标记更新将提供的回调函数作为一个过渡任务:

startTransition(() => {
  setCount(count + 1);
})

isPending 指示过渡任务何时活跃以显示一个等待状态:

function App() {
  const [isPending, startTransition] = useTransition();
  const [count, setCount] = useState(0);

  function handleClick() {
    startTransition(() => {
      setCount(c => c + 1);
    })
  }

  return (
    <div>
      {isPending && <Spinner />}
      <button onClick={handleClick}>{count}</button>
    </div>
  );
}

注意:

  • 过渡任务中触发的更新会让更紧急地更新先进行,比如点击。
  • 过渡任务中的更新将不会展示由于再次挂起而导致降级的内容。这个机制允许用户在 React 渲染更新的时候继续与当前内容进行交互。

useId

const id = useId();

useId 是一个用于生成横跨服务端和客户端的稳定的唯一 ID 的同时避免 hydration 不匹配的 hook。但 useId 不用于生成列表中的键。键应该从数据中生成。

function NameFields() {
  const id = useId();
  return (
    <div>
      <label htmlFor={id + '-firstName'}>First Name</label>
      <div>
        <input id={id + '-firstName'} type="text" />
      </div>
      <label htmlFor={id + '-lastName'}>Last Name</label>
      <div>
        <input id={id + '-lastName'} type="text" />
      </div>
    </div>
  );
}

注意:

useId 生成一个包含 : 的字符串 token。这有助于确保 token 是唯一的,但在 CSS 选择器querySelectorAll 等 API 中不受支持。

useId 支持 identifierPrefix 以防止在多个根应用的程序中发生冲突。 要进行配置,请参阅 hydrateRootReactDOMServer 的选项。

三、library-hooks

以下 hook 是为库作者提供的,用于将库深入集成到 React 模型中,通常不会在应用程序代码中使用。

usesyncexternalstore

useinsertioneffect

自定义 Hooks

自定义 Hook 就是一个普通函数,该函数名称以 use 开头,本质是一个调用其他钩子函数的钩子函数

使用规则:

  1. 只能在组件中或者其他自定义 Hook 函数中调用
  2. 只能在组件的顶层调用,不能嵌套在 iffor其他函数中
export default function useToggle() {
  // 可复用的逻辑代码
  const [value, setValue] = useState(true);
  const toggle = () => setValue(!value);

  // 哪些状态和回调函数需要在 其他组件或自定义 hook 中使用的就 return 出去
  return {
    value,
    toggle
  }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

眸中客

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

lee_heart

文章 0 评论 0

往事如风

文章 0 评论 0

春风十里

文章 0 评论 0

纸短情长

文章 0 评论 0

qq_pdEUFz

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文