Portal.tsx 1.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. 'use client';
  2. /*React v19 团队现在更倾向于:
  3. 如果一个值能在 render 阶段计算出来,就不要放进 state。
  4. 这就是为什么如果你在useEffect/useLayoutEffect中setState会eslint不通过的原因。
  5. 以Portal组件为例,在react v19之前Portal的标准写法是:
  6. ```react
  7. 'use client';
  8. import { useEffect, useState } from 'react';
  9. import { createPortal } from 'react-dom';
  10. export default function Portal({ children }: { children: React.ReactNode }) {
  11. const [mounted, setMounted] = useState(false);
  12. // useEffect 只会在客户端执行,服务端渲染时不会运行(Next.js 在服务器端会跳过所有 useEffect / useLayoutEffect)。
  13. useEffect(() => {
  14. setMounted(true); // 组件挂载后,把标志位置为 true
  15. }, []); // 空依赖数组,仅执行一次
  16. if (!mounted) return null; // SSR 时不渲染,避免 hydration 错误
  17. return createPortal(children, document.body);
  18. }
  19. ```
  20. 但是在react v19 中 认为mounted是一个「派生状态(derived state)」,
  21. 完全可以在 render 时计算出来(使用document 来判断),不需要专门声明一个state在副作用里更改状态(引起二次渲染),所以eslint报错。
  22. */
  23. import {useRef, useEffect} from 'react';
  24. import { createPortal } from 'react-dom';
  25. export default function Portal({
  26. children,
  27. }: {
  28. children: React.ReactNode;
  29. }) {
  30. const mountedRef = useRef(false);
  31. useEffect(() => {
  32. mountedRef.current = true;
  33. // 无需任何清理 (cleanup) 函数
  34. }, []); // 依赖数组完全为空
  35. // 服务端和客户端首次渲染时都返回 null
  36. if (!mountedRef.current) {
  37. return null;
  38. }
  39. const portalRoot = document.getElementById('modal-root');
  40. if (!portalRoot) {
  41. return null;
  42. }
  43. return createPortal(children, portalRoot);
  44. }