1. 首页
  2. React

React 19 内置Hooks

数据驱动更新 State Hook

useState

useState 是 React 中的一个 Hook,用于在函数组件中添加状态。它接受一个初始状态作为参数,并返回一个包含当前状态值和更新状态值的数组。

参数说明:

  • 初始状态:可以是任何 JavaScript 数据类型,用来初始化状态的值。

返回说明:

  • 数组:包含当前状态值和更新状态值的数组,第一个元素是当前状态值,第二个元素是更新状态值的函数。

应用场景:

  • 在函数组件中添加和管理状态。

  • 替代类组件中的 this.state 和 this.setState。

用法举例:

import React, { useState } from 'react';

const ExampleComponent: React.FC = () => {
  const [count, setCount] = useState<number>(0);

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default ExampleComponent;

在上面的例子中,我们使用 useState 创建了一个名为 count 的状态变量,并初始化为 0。我们还定义了一个 increment 函数,用于增加 count 的值。最后,我们在 JSX 中展示了当前 count 的值,并在按钮点击事件中调用 increment 函数来更新 count 的值。

useReducer

useReducer 是 React 中的另一个 Hook,用于管理复杂的状态逻辑。它接受一个 reducer 函数和初始状态作为参数,并返回当前状态值和 dispatch 函数。

参数说明:

  • reducer 函数:接受两个参数,当前状态和 action,返回新的状态。

  • 初始状态:可以是任何 JavaScript 数据类型,用来初始化状态的值。

返回说明:

  • 数组:包含当前状态值和 dispatch 函数的数组,第一个元素是当前状态值,第二个元素是派发 action 的函数。

应用场景:

  • 状态逻辑较复杂,需要根据不同 action 进行状态更新的情况。

  • 替代 useState,在某些情况下更方便和易于维护。

用法举例:

import React, { useReducer } from 'react';

type State = {
  count: number;
};

type Action = {
  type: 'increment' | 'decrement';
};

const initialState: State = {
  count: 0,
};

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
};

const ExampleComponent: React.FC = () => {
  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>
  );
};

export default ExampleComponent;

在上面的例子中,我们使用 useReducer 创建了一个名为 count 的状态变量,并初始化为 0。我们定义了一个 reducer 函数来处理不同的 action 类型并更新状态。然后,我们使用 dispatch 函数来派发不同的 action,从而更新状态。最后,在 JSX 中展示当前 count 的值,并在按钮点击事件中调用对应的 dispatch 函数来更新 count 的值。

状态传递 Context Hook

useContext

useContext 是 React 中的一个 Hook,用于在函数组件中访问 Context。它接受一个 Context 对象作为参数,并返回该 Context 的当前值。

参数说明:

  • Context 对象:通过 React.createContext 创建的 Context 对象。

返回说明:

  • 任意类型:返回该 Context 的当前值。

应用场景:

  • 在函数组件中访问全局的状态或其他共享数据。

  • 避免在组件树中层层传递 props。

用法举例:

import React, { createContext, useContext } from 'react';

type Theme = 'light' | 'dark';

const ThemeContext = createContext<Theme>('light');

const App: React.FC = () => {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
};

const Toolbar: React.FC = () => {
  const theme = useContext(ThemeContext);

  return (
    <div>
      <p>Current Theme: {theme}</p>
    </div>
  );
};

export default App;

在上面的例子中,我们创建了一个 ThemeContext,其默认值为 'light'。在 App 组件中,我们使用 ThemeContext.Provider 来提供当前主题值为 'dark',并渲染了 Toolbar 组件。在 Toolbar 组件中,我们使用 useContext(ThemeContext) 来获取当前主题值,并在 JSX 中展示出来。这样就实现了在函数组件中访问全局的主题值而不需要通过 props 层层传递。

元素获取 Ref Hook

useRef

useRef 是 React 中的一个 Hook,主要用于访问和存储对 DOM 元素的引用或保存不需要触发重新渲染的可变值。

参数说明:

  • initialValue: 可选,初始值。可以是任何类型,通常用于存储一个对象或 DOM 引用。

返回说明:

  • 返回一个可变的 ref 对象,ref 对象的 .current 属性可以用来存储任何值。

应用场景:

  • 访问 DOM 元素: 通过 useRef 可以直接访问组件中的 DOM 元素,常用于获取焦点、选择文本等。

  • 存储可变值: 在函数组件中,useRef 可以用来存储不需要重新渲染的可变值,例如计时器 ID、先前的状态等。

  • 保持引用: useRef 可以保持对某个值的引用,而不会在组件重新渲染时重置该值。

用法举例:

import React, { useRef } from 'react';

const ExampleComponent: React.FC = () => {
  const inputRef = useRef<HTMLInputElement>(null);

  const focusInput = () => {
    inputRef.current?.focus(); // 使用可选链安全地访问 current
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus the input</button>
    </div>
  );
};

export default ExampleComponent;

在上面的例子中,我们使用 useRef 创建了一个 inputRef,它将用于引用输入框元素。我们通过 ref 属性将 inputRef 关联到输入框上。当用户点击按钮时,focusInput 函数会调用 inputRef.current?.focus(),使输入框获得焦点。

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

const TimerComponent: React.FC = () => {
  const timerRef = useRef<NodeJS.Timeout | null>(null);
  const countRef = useRef(0);

  useEffect(() => {
    timerRef.current = setInterval(() => {
      countRef.current += 1;
      console.log(countRef.current);
    }, 1000);

    // 清理定时器
    return () => {
      if (timerRef.current) {
        clearInterval(timerRef.current);
      }
    };
  }, []);

  return <div>Check the console for count updates every second.</div>;
};

export default TimerComponent;

在这个例子中,我们使用 useRef 来存储定时器的引用和计数器的值。countRef 用于记录计数器的当前值,而不会导致组件重新渲染。定时器每秒增加计数器的值并输出到控制台。组件卸载时,定时器会被清理。

useImperativeHandle

useImperativeHandle 是 React 中的一个 Hook,通常与 forwardRef 一起使用,用于自定义组件暴露给父组件的实例值。它允许你在函数组件中控制通过 ref 传递给父组件的值。

参数说明:

  • ref: 传递给 forwardRef 的 ref 对象。

  • createHandle: 一个函数,返回一个对象,该对象包含要暴露给父组件的实例值。

返回说明:

  • void: 该 Hook 不返回任何值。

应用场景:

  • 自定义组件的实例方法: 当你需要在父组件中调用子组件的方法时,可以使用 useImperativeHandle 来暴露这些方法。

  • 控制子组件的行为: 允许父组件通过 ref 直接控制子组件的某些行为。

用法举例:

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

type ChildRef = {
  focus: () => void;
};

const ChildComponent = forwardRef<ChildRef, {}>((_, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current?.focus(); // 暴露 focus 方法
    },
  }));

  return <input ref={inputRef} type="text" />;
});

const ParentComponent: React.FC = () => {
  const childRef = useRef<ChildRef>(null);

  const handleFocus = () => {
    childRef.current?.focus(); // 调用子组件的 focus 方法
  };

  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={handleFocus}>Focus the input in Child</button>
    </div>
  );
};

export default ParentComponent;

在上面的例子中,我们创建了一个 ChildComponent,它使用 forwardRef 来接收父组件传递的 ref。在 ChildComponent 中,我们使用 useImperativeHandle 来暴露一个 focus 方法,该方法可以使输入框获得焦点。

ParentComponent 中,我们创建了一个 childRef,并将其传递给 ChildComponent。当用户点击按钮时,handleFocus 函数会调用 childRef.current?.focus(),从而触发子组件的 focus 方法,使输入框获得焦点。

总结:useImperativeHandle 使得父组件能够控制子组件的行为,提供了一种灵活的方式来管理组件间的交互。

副作用 Effect Hook

useEffect

useEffect 是 React 中的一个 Hook,用于在函数组件中执行副作用操作。它接受一个函数和一个依赖数组作为参数,并在每次渲染后执行该函数。

参数说明:

  • effect: 一个函数,用于执行副作用操作。

  • deps: 一个数组,包含影响副作用操作的依赖项。当依赖项发生变化时,effect 函数会被重新执行。

返回说明:

  • void: 该 Hook 不返回任何值。

应用场景:

  • 数据获取: 在组件挂载后获取数据。

  • 订阅事件: 在组件挂载后订阅事件,组件卸载前取消订阅。

  • 手动操作 DOM: 在组件挂载后或更新后手动操作 DOM 元素。

  • 清理副作用: 在组件卸载前清理副作用。

用法举例:

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

const ExampleComponent: React.FC = () => {
  const [data, setData] = useState<string>('');

  // 模拟数据获取
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('https://api.example.com/data');
      const result = await response.json();
      setData(result.data);
    };

    fetchData();

    return () => {
      // 在组件卸载前清理副作用
      console.log('Cleaning up');
    };
  }, []); // 空依赖数组表示只在组件挂载和卸载时执行

  return (
    <div>
      <p>Data: {data}</p>
    </div>
  );
};

export default ExampleComponent;

在这个示例中,我们使用 useEffect 在组件挂载后获取数据,并在组件卸载前清理副作用。在 useEffect 的回调函数中,我们定义了一个 fetchData 函数来获取数据,并在组件挂载后调用它。同时,我们返回一个清理函数,用于在组件卸载前执行清理操作。在依赖数组中传入空数组表示 useEffect 只在组件挂载和卸载时执行。

这种方式可以帮助我们管理组件中的副作用操作,确保在适当的时机执行相应的操作,并在组件卸载时清理副作用,避免内存泄漏和其他问题。

useLayoutEffect

useLayoutEffect 是 React 中的一个 Hook,与 useEffect 类似,用于在函数组件中执行副作用操作。不同之处在于 useLayoutEffect 会在 DOM 变更之后同步触发,而 useEffect 则是在浏览器绘制完成后异步触发。

参数说明:

  • effect: 一个函数,用于执行副作用操作。

  • deps: 一个数组,包含影响副作用操作的依赖项。当依赖项发生变化时,effect 函数会被重新执行。

返回说明:

  • void: 该 Hook 不返回任何值。

应用场景:

  • DOM 操作: 在 DOM 变更之后立即执行操作,确保操作不会影响布局。

  • 测量元素尺寸: 在 DOM 变更后立即测量元素的尺寸或位置。

  • 动画控制: 在 DOM 变更后立即控制动画效果。

  • 同步更新布局: 需要在 DOM 变更后立即更新布局的情况。

用法举例:

import React, { useLayoutEffect, useState, useRef } from 'react';

const ExampleComponent: React.FC = () => {
  const [width, setWidth] = useState<number>(0);
  const elementRef = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    const handleResize = () => {
      if (elementRef.current) {
        const { width } = elementRef.current.getBoundingClientRect();
        setWidth(width);
      }
    };

    window.addEventListener('resize', handleResize);
    handleResize(); // 立即执行一次

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // 空依赖数组表示只在组件挂载和卸载时执行

  return (
    <div ref={elementRef}>
      <p>Width: {width}px</p>
    </div>
  );
};

export default ExampleComponent;

在这个示例中,我们使用 useLayoutEffect 来在 DOM 变更之后立即测量元素的宽度,并在窗口大小变化时实时更新宽度。在 useLayoutEffect 的回调函数中,我们定义了一个 handleResize 函数,用于获取元素的宽度并更新状态。然后我们通过 window.addEventListener 监听窗口大小变化事件,并在组件挂载时立即执行一次 handleResize。在依赖数组中传入空数组表示 useLayoutEffect 只在组件挂载和卸载时执行。

通过使用 useLayoutEffect ,我们可以在 DOM 变更后立即执行操作,确保操作不会影响布局,适用于需要同步更新布局或执行 DOM 操作的场景。

useInsertionEffect

useInsertionEffect 是 React 18 中引入的一个 Hook,主要用于在 DOM 更新之前插入样式或执行其他副作用。它的执行时机比 useLayoutEffect 更早,适用于需要在浏览器绘制之前进行样式插入的场景。

参数说明:

  • effect: 一个函数,用于执行副作用操作。

  • deps: 一个数组,包含影响副作用操作的依赖项。当依赖项发生变化时,effect 函数会被重新执行。

返回说明:

  • void: 该 Hook 不返回任何值。

应用场景:

  • 动态样式插入: 在组件渲染之前插入动态生成的样式,确保样式在浏览器绘制之前生效。

  • CSS-in-JS 库: 在使用 CSS-in-JS 库时,确保样式在 DOM 更新之前插入。

  • 避免闪烁: 在需要避免样式闪烁的情况下,使用 useInsertionEffect 可以确保样式在渲染之前就已经应用。

用法举例:

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

const ExampleComponent: React.FC = () => {
  const [color, setColor] = useState<string>('blue');

  useInsertionEffect(() => {
    const style = document.createElement('style');
    style.textContent = `
      .dynamic {
        color: ${color};
      }
    `;
    document.head.appendChild(style);

    return () => {
      document.head.removeChild(style); // 清理样式
    };
  }, [color]); // 当 color 变化时重新插入样式

  return (
    <div>
      <p className="dynamic">This text will change color.</p>
      <button onClick={() => setColor(color === 'blue' ? 'red' : 'blue')}>
        Toggle Color
      </button>
    </div>
  );
};

export default ExampleComponent;

在这个示例中,我们使用 useInsertionEffect 来动态插入样式。每当 color 状态变化时,我们都会创建一个新的 <style> 元素,并将其插入到文档的 <head> 中。这样,文本的颜色会根据按钮的点击而变化。

通过使用 useInsertionEffect,我们确保样式在浏览器绘制之前就已经插入,从而避免了可能的闪烁或样式不一致的问题。清理函数会在组件卸载或依赖项变化时移除插入的样式,确保不会造成内存泄漏。

useInsertionEffectuseEffect 的区别

  1. 执行时机:

    • useInsertionEffect 在 DOM 变更之前同步执行,这意味着它会在浏览器绘制之前运行,适合需要插入样式或进行布局计算的场景。

    • useEffect 在 DOM 变更后异步执行,适合大多数副作用操作,如数据获取、事件监听等。

  2. 适用场景:

    • useInsertionEffect 主要用于需要确保样式或其他操作在用户看到变化之前完成的场景。

    • useEffect 则更为通用,可以处理各种副作用,且通常是开发者最常用的选择。

总结

虽然在某些情况下 useInsertionEffectuseEffect 可以实现相同的功能,但它们的执行时机和适用场景有所不同。useInsertionEffect 是用于特定场景的优化工具,通常在处理样式和布局时使用,而 useEffect 则是处理一般副作用的主要手段。选择使用哪一个 Hook 取决于你的具体需求。

性能 Hook

useMemo

useMemo 是 React 中的一个 Hook,用于对计算昂贵的值进行缓存,避免在每次渲染时重新计算。它接受一个函数和依赖数组作为参数,并返回计算结果。

参数说明:

  • factory: 一个函数,用于计算需要缓存的值。

  • deps: 一个数组,包含影响计算结果的依赖项。当依赖项发生变化时,将重新计算值。

返回说明:

  • T: 计算结果的类型,根据传入的 factory 函数的返回类型确定。

应用场景:

  • 性能优化: 缓存昂贵的计算结果,避免在每次渲染时重新计算。

  • 避免不必要的重渲染: 通过缓存值,可以避免不必要的组件重渲染。

  • 优化组件性能: 在计算量大的场景下,使用 useMemo 可以提高组件的性能。

用法举例:

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

const ExampleComponent: React.FC = () => {
  const [count, setCount] = useState<number>(0);

  // 计算平方值
  const squaredValue = useMemo(() => {
    console.log('Calculating squared value');
    return count ** 2;
  }, [count]); // 当 count 变化时重新计算

  return (
    <div>
      <p>Count: {count}</p>
      <p>Squared Value: {squaredValue}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
    </div>
  );
};

export default ExampleComponent;

在这个示例中,我们使用 useMemo 缓存了计算的平方值。每当 count 状态变化时,squaredValue 会重新计算,但在同一个 count 值下,不会重复计算平方值。这可以减少不必要的计算,提高性能。

通过使用 useMemo,我们可以避免在每次渲染时都重新计算昂贵的值,优化组件性能,并确保只在必要时才进行计算。

useCallback

useCallback 是 React 中的一个 Hook,用于缓存回调函数,避免在每次渲染时重新创建新的回调函数。它接受一个回调函数和依赖数组作为参数,并返回一个 memoized(记忆化)版本的回调函数。

参数说明:

  • callback: 一个回调函数,需要被缓存。

  • deps: 一个数组,包含影响回调函数的依赖项。当依赖项发生变化时,将重新创建新的回调函数。

返回说明:

  • T: 缓存的回调函数的类型,根据传入的 callback 函数的类型确定。

应用场景:

  • 避免不必要的子组件重渲染: 缓存回调函数可以避免子组件在每次渲染时重新创建新的回调函数,提高性能。

  • 传递给子组件的回调函数: 在将回调函数传递给子组件时,使用 useCallback 可以确保子组件不会因为父组件的重新渲染而触发不必要的更新。

  • 优化性能: 在性能敏感的场景下,使用 useCallback 可以优化组件的性能,避免不必要的计算。

用法举例:

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

const ExampleComponent: React.FC = () => {
  const [text, setText] = useState<string>('');
  const [count, setCount] = useState<number>(0);

  const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
    setText(event.target.value);
  }, []);

  const handleIncrement = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <input type="text" value={text} onChange={handleChange} />
      <p>Text: {text}</p>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment Count</button>
    </div>
  );
};

export default ExampleComponent;

在这个示例中,我们使用 useCallback 缓存了处理输入框变化和增加计数的回调函数 handleChangehandleIncrement。这样,在每次父组件重新渲染时,这两个回调函数不会被重新创建,从而避免不必要的性能消耗。

通过使用 useCallback,我们可以确保只在依赖项发生变化时重新创建回调函数,提高性能并避免不必要的更新。这对于传递给子组件的回调函数或在性能敏感的场景下非常有用。感谢您的指正,希望这个例子更符合您的要求。

useTransition

useTransition 是 React 18 中引入的一个 Hook,用于管理 UI 中的过渡状态,特别是在处理长时间运行的状态更新时。它允许你将某些更新标记为“过渡”状态,这样 React 可以优先处理更重要的更新,比如用户输入,同时延迟处理过渡更新。

参数说明:

  • useTransition 不需要任何参数

返回说明:

  • useTransition 返回一个数组,包含两个元素

    1. isPending(boolean),告诉你是否存在待处理的 transition。

    2. startTransition(function) 函数,你可以使用此方法将状态更新标记为 transition。

应用场景:

  • 优化用户体验: 在数据加载或其他过渡时显示加载指示器,提高用户体验。

  • 控制交互: 在需要进行一些耗时操作时,可以使用 useTransition 控制界面的交互,避免用户误操作。

用法举例:

想象一个场景,页面上有多个tab,其中一个请求耗时较长,我们快速点击tab切换内容,但总是会在请求耗时的tab上卡顿一下,代码如下:

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

const TabContainer = () => {
  const [tab, setTab] = useState('about');

	// 核心:切换选项卡
  function selectTab(nextTab) {
    setTab(nextTab);
  }

  return (
    <div>
      <div>
        <TabButton isActive={tab === 'about'} onClick={() => selectTab('about')}>About</TabButton>
        <TabButton isActive={tab === 'posts'} onClick={() => selectTab('posts')}>Posts (slow)</TabButton>
        <TabButton isActive={tab === 'contact'} onClick={() => selectTab('contact')}>Contact</TabButton>
      </div>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </div>
  );
};

const PostsTab = memo(() => {
  let items = [];
  for (let i = 0; i < 500; i++) {
    items.push(<SlowPost key={i} index={i} />);
  }
  return <ul>{items}</ul>;
});

const SlowPost = ({ index }) => {
  let startTime = performance.now();
  while (performance.now() - startTime < 10) { }
  return <li>Post on weijunext.com #{index + 1}</li>;
};

// 省略非关键代码
// ……

export default TabContainer;

如果我们想用useTransition保持UI的响应,只需要用startTransition包裹切换选项卡的set

const [isPending, startTransition] = React.useTransition();

function selectTab(nextTab) {
  startTransition(() => {
    setTab(nextTab);      
  });
}

这样我们快速切换tab,无论点到哪一个tab都不会卡顿。

注意事项:

在React18中,不支持 async 和 await,但是从React19开始,已经支持 async 和 await 了

在 React18 中的用法

// 错误做法
startTransition(() => {
  // ❌ 在调用 startTransition 后更新状态
  setTimeout(() => {
    setPage('/about');
  }, 1000);
});

// 正确做法
setTimeout(() => {
  startTransition(() => {
    // ✅ 在调用 startTransition 中更新状态
    setPage('/about');
  });
}, 1000);


// 错误做法
startTransition(async () => {
  await someAsyncFunction();
  // ❌ 在调用 startTransition 后更新状态
  setPage('/about');
});

// 正确做法
await someAsyncFunction();
startTransition(() => {
  // ✅ 在调用 startTransition 中更新状态
  setPage('/about');
});

在 React19 中的用法

import {useState, useTransition} from 'react';
import {updateQuantity} from './api';

function CheckoutForm() {
  const [isPending, startTransition] = useTransition();
  const [quantity, setQuantity] = useState(1);
  // 支持 await async
  function onSubmit(newQuantity) {
    startTransition(async function () {
      const savedQuantity = await updateQuantity(newQuantity);
      startTransition(() => {
        setQuantity(savedQuantity);
      });
    });
  }
  // ……
}

原理剖析:

useTransition 的核心原理是将一部分状态更新处理为低优先级任务,这样可以将关键的高优先级任务先执行,而低优先级的过渡更新则会稍微延迟处理。这在渲染大量数据、进行复杂运算或处理长时间任务时特别有效。React 通过调度机制来管理优先级:

  • 高优先级更新:直接影响用户体验的任务,比如表单输入、按钮点击等。

  • 低优先级更新:相对不影响交互的过渡性任务,比如大量数据渲染、动画等,这些任务可以延迟执行。

+-----------------------+
|         App           |
|                       |
|  +--------------+     |
|  |    Input     |     |
|  +--------------+     |
|                       |
|  +--------------+     |
|  |   Display    |     |
|  +--------------+     |
+-----------------------+

用户输入
    |
    v
[高优先级更新] ---> [调度器] ---> [React 更新组件]
    |
    +---> [低优先级过渡更新] --> [调度器] --> [等待处理]

useDeferredValue

useDeferredValue 是 React 18 中引入的一个 Hook,用于延迟获取某个值,以提高性能。它可以将某个值的获取推迟到下一个渲染周期,从而避免在当前渲染周期内进行不必要的计算。

参数说明:

  • value: 要延迟获取的值。

返回说明:

  • 返回一个代表延迟获取的值的对象。

应用场景:

  • 性能优化: 在某些情况下,获取某个值可能会引起性能问题,通过延迟获取可以避免不必要的计算,提高性能。

  • 优化渲染: 在某些值不是立即需要的情况下,可以延迟获取以避免不必要的渲染。

用法举例:

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

const ExampleComponent: React.FC = () => {
  const [count, setCount] = useState<number>(0);
  const deferredCount = useDeferredValue(count);

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

  return (
    <div>
      <p>Deferred Count: {deferredCount}</p>
      <p>Actual Count: {count}</p>
      <button onClick={incrementCount}>Increment Count</button>
    </div>
  );
};

export default ExampleComponent;

在这个示例中,我们使用 useDeferredValuecount 的值延迟获取。在每次用户点击增加按钮时,count 的值会增加,但 deferredCount 的值会在下一个渲染周期才会更新。这样可以避免在当前渲染周期内进行不必要的计算,提高性能。

通过使用 useDeferredValue,我们可以延迟获取某个值,从而优化性能并减少不必要的渲染。这对于一些不是立即需要的值的场景非常有用。希望这个例子能够帮助您理解 useDeferredValue 的用法和应用场景。

其他 Hook

useSyncExternalStore

useSyncExternalStore 是 React 18 中引入的一个 Hook,用于从外部存储(如 Redux、MobX 或其他状态管理库)中同步获取数据。它提供了一种安全的方式来订阅外部存储的变化,并确保组件在外部存储更新时能够正确地重新渲染。

参数说明:

useSyncExternalStore 接受三个参数:

  1. subscribe: 一个函数,用于订阅外部存储的变化。它应该返回一个取消订阅的函数。

  2. getSnapshot: 一个函数,用于获取当前的快照值(即外部存储的当前状态)。

  3. getServerSnapshot(可选): 在服务器端渲染时使用的函数,返回服务器快照的值。

返回说明:

  • 返回外部存储的当前快照值。

应用场景:

  1. 与外部状态管理库集成: 在使用 Redux、MobX 等状态管理库时,可以使用 useSyncExternalStore 来同步外部状态。

  2. 确保一致性: 确保在外部存储更新时,组件能够正确地重新渲染,避免不一致的状态。

用法举例:

一、订阅浏览器Api 实现自定义hook(useStorage)

我们实现一个useStorage Hook,用于订阅 localStorage 数据。这样做的好处是,我们可以确保组件在 localStorage 数据发生变化时,自动更新同步。

实现代码

我们将创建一个 useStorage Hook,能够存储数据到 localStorage,并在不同浏览器标签页之间同步这些状态。此 Hook 接收一个键值参数用于存储数据的键名,还可以接收一个默认值用于在无数据时的初始化。

在 hooks/useStorage.ts 中定义 useStorage Hook:

import { useSyncExternalStore } from "react"

/**
 * 
 * @param key 存储到localStorage 的key
 * @param defaultValue 默认值
 */
export const useStorage = (key: any, defaultValue?: any) => {
    const subscribe = (callback: () => void) => {
        window.addEventListener('storage', (e) => {
            console.log('触发了', e)
            callback()
        })
        return () => window.removeEventListener('storage', callback)
    }
    //从localStorage中获取数据 如果读不到返回默认值
    const getSnapshot = () => {
        return (localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key)!) : null) || defaultValue
    }
    //修改数据
    const setStorage = (value: any) => {
        localStorage.setItem(key, JSON.stringify(value))
        window.dispatchEvent(new StorageEvent('storage')) //手动触发storage事件
    }
    //返回数据
    const res = useSyncExternalStore(subscribe, getSnapshot)

    return [res, setStorage]
}

在 App.tsx 中,我们可以直接使用 useStorage,来实现一个简单的计数器。值会存储在 localStorage 中,并且在刷新或其他标签页修改数据时自动更新。

import { useStorage } from "./hooks/useStorage"
const App = () => {
  const [val, setVal] = useStorage('data', 1)
  return (<>
    <h3>{val}</h3>
    <button onClick={() => setVal(val + 1)}>设置val</button>
  </>)
}

export default App

效果演示

  • 值的持久化:点击按钮增加 val,页面刷新后依然会保留最新值。

  • 跨标签页同步:在多个标签页打开该应用时,任意一个标签页修改 val,其他标签页会实时更新,保持同步状态。

二、订阅history实现路由跳转

实现一个简易的useHistory Hook,获取浏览器url信息 + 参数

import { useSyncExternalStore } from "react"
export const useHistory = () => {
    const subscribe = (callback: () => void) => {
        window.addEventListener('popstate', callback)
        window.addEventListener('hashchange', callback)
        return () => {
            window.removeEventListener('popstate', callback)
            window.removeEventListener('hashchange', callback)
        }
    }
    const getSnapshot = () => {
        return window.location.href
    }
    const push = (path: string) => {
        window.history.pushState(null, '', path)
        window.dispatchEvent(new PopStateEvent('popstate'))
    }
    const replace = (path: string) => {
        window.history.replaceState(null, '', path)
        window.dispatchEvent(new PopStateEvent('popstate'))
    }
    const res = useSyncExternalStore(subscribe, getSnapshot)
    return [res, push, replace] as const
}

使用 useHistory Hook

让我们在组件中使用这个 useHistory Hook,实现基本的前进、后退操作以及程序化导航。

import { useHistory } from "./hooks/useHistory"
const App = () => {
  const [history, push, replace] = useHistory()
  return (<>
    <div>当前url:{history}</div>
    <button onClick={() => { push('/aaa') }}>跳转</button>
    <button onClick={() => { replace('/bbb') }}>替换</button>
  </>)
}

export default App

效果演示

  • history:这是 useHistory 返回的当前路径值。每次 URL 变化时,useSyncExternalStore 会自动触发更新,使 history 始终保持最新路径。

  • push 和 replace:点击“跳转”按钮调用 push("/aaa"),会将 /aaa 推入历史记录;点击“替换”按钮调用 replace("/bbb"),则会将当前路径替换为 /bbb。

注意事项

如果 getSnapshot 返回值不同于上一次,React 会重新渲染组件。这就是为什么,如果总是返回一个不同的值,会进入到一个无限循环,并产生这个报错。

Uncaught (in promise) Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.

function getSnapshot() {
  return myStore.todos; //object
}

这种写法每次返回了对象的引用,即使这个对象没有改变,React 也会重新渲染组件。

如果你的 store 数据是可变的,getSnapshot 函数应当返回一个它的不可变快照。这意味着 确实 需要创建新对象,但不是每次调用都如此。而是应当保存最后一次计算得到的快照,并且在 store 中的数据不变的情况下,返回与上一次相同的快照。如何决定可变数据发生了改变则取决于你的可变 store。

function getSnapshot() {
  if (myStore.todos !== lastTodos) {
    // 只有在 todos 真的发生变化时,才更新快照
    lastSnapshot = { todos: myStore.todos.slice() };
    lastTodos = myStore.todos;
  }
  return lastSnapshot;
}

useDebugValue

useDebugValue 是 React 18 中引入的一个 Hook,用于为自定义 Hook 提供调试信息。它可以帮助开发者在开发过程中更好地理解自定义 Hook 的工作原理和状态。

参数说明:

  • value: 要为其提供调试信息的值。

  • format: 一个函数,用于格式化 value 提供的调试信息。

返回说明:

  • 无返回值。

应用场景:

  • 自定义 Hook 调试: 在自定义 Hook 中使用 useDebugValue 提供更多有关 Hook 内部状态的调试信息。

  • 开发调试工具: 可以结合开发者工具使用,帮助开发者更好地理解 Hook 的工作原理。

用法举例:

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

const useCounter = (initialCount: number) => {
  const [count, setCount] = useState(initialCount);

  useDebugValue(count, (count) => `Current count: ${count}`);

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

  return { count, increment };
};

const ExampleComponent: React.FC = () => {
  const { count, increment } = useCounter(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment Count</button>
    </div>
  );
};

export default ExampleComponent;

在这个示例中,我们定义了一个自定义 Hook useCounter,其中使用 useDebugValue 提供了关于 count 值的调试信息。在调试工具中,开发者可以看到当前的计数值,帮助他们更好地理解 Hook 的工作原理。

通过使用 useDebugValue,我们可以为自定义 Hook 提供更多的调试信息,帮助开发者更好地理解 Hook 的状态和工作原理。这对于开发过程中的调试和问题排查非常有帮助。希望这个例子能够帮助您理解 useDebugValue 的用法和应用场景。

useId

useId 是 React 18 中引入的一个 Hook,主要用于生成唯一的 ID,通常用于支持无障碍(accessibility)特性和确保组件在多个实例中正确关联。它可以帮助你在渲染过程中生成稳定的 ID,避免了手动生成 ID 的复杂性和潜在的冲突。

参数说明:

useId 的基本用法非常简单。它不需要任何参数,返回一个字符串形式的唯一 ID。这个 ID 在组件的生命周期内是稳定的,即使组件重新渲染也不会改变。

返回说明:

  • useId 返回一个字符串,代表唯一的 ID。

使用示例:

以下是一个使用 useId 的简单示例,展示如何在表单中使用该 ID 来关联标签和输入字段:

import React, { useId } from 'react';

function CustomInput() {
  const id = useId(); // 使用 useId 生成唯一 ID

  return (
    <div>
      <label htmlFor={id}>Enter your name:</label>
      <input id={id} type="text" />
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Form with Unique IDs</h1>
      <CustomInput />
      <CustomInput /> {/* 这里会生成不同的 ID */}
    </div>
  );
}

export default App;

注意事项:

  • SSR 兼容性:useId 在服务器端渲染(SSR)和客户端渲染(CSR)中都能保持一致性。它会确保在服务器和客户端生成相同的 ID,从而避免 ID 不一致的问题。

  • 多实例:在多个实例的情况下,useId 会为每个实例生成不同的 ID,这对于组件复用非常有用。

  • 不适用于动态生成的 ID:如果你需要根据某些动态数据生成 ID,建议使用其他方法,因为 useId 的目的是生成稳定的、可重用的 ID。

useId 是一个非常实用的工具,特别是当你需要在组件中处理无障碍特性时,可以确保组件的可访问性和用户体验。

useOptimistic

useOptimistic 是一个 React Hook,它允许你在进行异步操作时显示不同 state。它接受 state 作为参数,并返回该 state 的副本,在异步操作(如网络请求)期间可以不同。你需要提供一个函数,该函数接受当前 state 和操作的输入,并返回在操作挂起期间要使用的乐观状态。

这个状态被称为“乐观”状态是因为通常用于立即向用户呈现执行操作的结果,即使实际上操作需要一些时间来完成。

参数说明:

  • state:初始时和没有挂起操作时要返回的值。

  • updateFn(currentState, optimisticValue):一个函数,接受当前 state 和传递给 addOptimistic 的乐观值,并返回结果乐观状态。它必须是一个纯函数。updateFn 接受两个参数:currentStateoptimisticValue。返回值将是 currentStateoptimisticValue 的合并值。

返回说明:

  • optimisticState:结果乐观状态。除非有操作挂起,否则它等于 state,在这种情况下,它等于 updateFn 返回的值。

  • addOptimistic:触发乐观更新时调用的 dispatch 函数。它接受一个可以是任何类型的参数 optimisticValue,并以 stateoptimisticValue 作为参数来调用 updateFn

应用场景:

  • useOptimistic Hook 提供了一种在后台操作(如网络请求)完成之前乐观地更新用户界面的方式。在表单的上下文中,这种技术有助于使应用程序在感觉上响应地更加快速。当用户提交表单时,界面立即更新为预期的结果,而不是等待服务器的响应来反映更改。

    例如,当用户在表单中输入消息并点击“发送”按钮时,useOptimistic Hook 允许消息立即出现在列表中,并带有“发送中……”标签,即使消息实际上还没有发送到服务器。这种“乐观”方法给人一种快速和响应灵敏的印象。然后,表单在后台尝试真正发送消息。一旦服务器确认消息已收到,“发送中……”标签就会被移除。

用法举例:

app.js

import { useOptimistic, useState, useRef } from "react";
import { deliverMessage } from "./actions.js";

function Thread({ messages, sendMessage }) {
  const formRef = useRef();
  async function formAction(formData) {
    addOptimisticMessage(formData.get("message"));
    formRef.current.reset();
    await sendMessage(formData);
  }
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [
      ...state,
      {
        text: newMessage,
        sending: true
      }
    ]
  );

  return (
    <>
      {optimisticMessages.map((message, index) => (
        <div key={index}>
          {message.text}
          {!!message.sending && <small>(发送中……)</small>}
        </div>
      ))}
      <form action={formAction} ref={formRef}>
        <input type="text" name="message" placeholder="你好!" />
        <button type="submit">发送</button>
      </form>
    </>
  );
}

export default function App() {
  const [messages, setMessages] = useState([
    { text: "你好,在这儿!", sending: false, key: 1 }
  ]);
  async function sendMessage(formData) {
    const sentMessage = await deliverMessage(formData.get("message"));
    setMessages((messages) => [...messages, { text: sentMessage }]);
  }
  return <Thread messages={messages} sendMessage={sendMessage} />;
}

actions.js

export async function deliverMessage(message) {
  await new Promise((res) => setTimeout(res, 1000));
  return message;
}

useActionState

useActionState 是一个可以根据某个表单动作的结果更新 state 的 Hook。

useFormStatus

useFormStatus 是一个提供上次表单提交状态信息的 Hook。


TOP