React useEffect 在服务端渲染中的执行行为

React useEffect 在服务端渲染中的执行行为

本文档详细解释了 React 中 useEffect 钩子在服务端渲染 (SSR) 环境中的执行行为、原理以及最佳实践。

1. useEffect 在 SSR 中的执行行为

useEffect 钩子在服务端渲染和客户端渲染中的行为是不同的:

服务端渲染时

  • useEffect 钩子不会执行
  • 服务端渲染只执行组件的渲染函数,生成 HTML
  • 所有的副作用(包括 useEffect 中的代码)都会被跳过

客户端水合时

  • 水合完成后,useEffect 钩子会执行
  • 这是 React 将事件监听器附加到 DOM 并接管应用的过程

后续客户端渲染时

  • 每次组件重新渲染后,useEffect 都会根据其依赖项决定是否执行

2. 代码示例

javascript 复制代码
import { useState, useEffect } from 'react';

function ExampleComponent() {
  const [count, setCount] = useState(0);
  const [serverRendered, setServerRendered] = useState(true);
  
  // 这个 useEffect 在服务端不会执行,只在客户端水合后执行一次
  useEffect(() => {
    console.log('这个 useEffect 只在客户端执行');
    setServerRendered(false);
  }, []);
  
  return (
    <div>
      <p>计数: {count}</p>
      <p>是否服务端渲染: {serverRendered ? '是' : '否'}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}

3. 为什么 useEffect 在服务端不执行?

React 设计 useEffect 不执行的原因有几个:

  1. 避免副作用:服务端渲染的目的是生成静态 HTML,不应该有副作用(如 DOM 操作、API 调用等)
  2. 避免不一致:如果服务端执行了副作用,可能会导致服务端和客户端渲染结果不一致
  3. 性能考虑:服务端渲染应该尽可能快,执行副作用会降低性能

4. 如何区分服务端和客户端代码

由于 useEffect 只在客户端执行,它常被用来处理仅客户端的逻辑:

javascript 复制代码
import { useState, useEffect } from 'react';

function ClientOnlyComponent() {
  const [isClient, setIsClient] = useState(false);
  
  useEffect(() => {
    // 这段代码只在客户端执行
    setIsClient(true);
    
    // 可以安全地使用浏览器 API
    document.title = '客户端渲染的页面';
  }, []);
  
  // 在服务端渲染一个占位符
  if (!isClient) {
    return <div>加载中...</div>;
  }
  
  // 在客户端渲染实际内容
  return (
    <div>
      <h1>这是客户端渲染的内容</h1>
      <p>窗口宽度: {window.innerWidth}px</p>
    </div>
  );
}

5. 替代方案

对于需要在服务端和客户端都执行的代码,可以使用其他方法:

直接在组件函数体中

组件函数体中的代码在服务端和客户端都会执行:

javascript 复制代码
function Component() {
  // 这段代码在服务端和客户端都会执行
  const serverTime = new Date().toISOString();
  
  return <div>服务器时间: {serverTime}</div>;
}

使用 useLayoutEffect 的替代方案

创建一个同构的 useLayoutEffect 钩子:

javascript 复制代码
import { useEffect, useLayoutEffect } from 'react';

// 创建一个在服务端使用 useEffect,在客户端使用 useLayoutEffect 的钩子
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;

function Component() {
  useIsomorphicLayoutEffect(() => {
    // 这段代码在服务端使用 useEffect(不执行),在客户端使用 useLayoutEffect(执行)
  }, []);
  
  return <div>内容</div>;
}
详细解释

useLayoutEffect 是 React 提供的另一个副作用钩子,它与 useEffect 的主要区别在于执行时机:

  1. useEffect 执行时机:在浏览器绘制 DOM 更新后异步执行
  2. useLayoutEffect 执行时机:在 React 完成 DOM 更新后,但在浏览器绘制之前同步执行

在服务端渲染环境中,useLayoutEffectuseEffect 都不会执行,但 React 会发出警告,因为 useLayoutEffect 设计用于在浏览器环境中同步执行。

为了解决这个问题,我们可以创建一个同构的 useLayoutEffect 钩子,它在服务端使用 useEffect(不执行),在客户端使用 useLayoutEffect(执行)。

工作原理
  1. 环境检测typeof window !== 'undefined' 检查代码是否在浏览器环境中运行
  2. 条件选择
    • 在浏览器中:使用 useLayoutEffect,在 DOM 更新后同步执行
    • 在服务端:使用 useEffect,在服务端渲染时不会执行
使用场景

这种模式特别适用于需要在客户端立即执行 DOM 操作的场景,例如:

  1. 测量 DOM 元素:需要立即获取元素尺寸
  2. 动画:需要在浏览器绘制前应用动画
  3. 焦点管理:需要在 DOM 更新后立即设置焦点
完整示例
javascript 复制代码
import { useState, useEffect, useLayoutEffect } from 'react';

// 创建同构的 useLayoutEffect
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;

function AnimatedComponent() {
  const [width, setWidth] = useState(0);
  const [height, setHeight] = useState(0);
  const [isClient, setIsClient] = useState(false);
  
  // 使用同构的 useLayoutEffect
  useIsomorphicLayoutEffect(() => {
    // 这段代码在服务端不会执行
    // 在客户端,它会在 DOM 更新后、浏览器绘制前执行
    const element = document.getElementById('animated-element');
    if (element) {
      setWidth(element.offsetWidth);
      setHeight(element.offsetHeight);
    }
    
    // 标记组件已在客户端渲染
    setIsClient(true);
  }, []);
  
  // 在服务端渲染一个占位符
  if (!isClient) {
    return <div id="animated-element" style={{ width: '100px', height: '100px' }}>加载中...</div>;
  }
  
  // 在客户端渲染实际内容
  return (
    <div>
      <div 
        id="animated-element" 
        style={{ 
          width: `${width}px`, 
          height: `${height}px`,
          transition: 'all 0.3s ease',
          backgroundColor: '#f0f0f0'
        }}
      >
        元素尺寸: {width}x{height}
      </div>
    </div>
  );
}
注意事项
  1. 避免水合不匹配:确保服务端和客户端渲染的内容一致,避免水合错误
  2. 性能考虑useLayoutEffect 是同步执行的,可能会阻塞渲染,应谨慎使用
  3. 条件渲染 :使用 isClient 状态来区分服务端和客户端渲染,避免使用浏览器 API 导致错误
在库中实现

如果你正在开发一个 React 库,可以在库中实现这个模式:

javascript 复制代码
// my-react-library.js
import { useEffect, useLayoutEffect } from 'react';

// 导出同构的 useLayoutEffect
export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;

// 使用这个钩子的组件
export function MyComponent() {
  useIsomorphicLayoutEffect(() => {
    // 客户端代码
  }, []);
  
  return <div>内容</div>;
}

这样,库的使用者就不需要关心服务端渲染的兼容性问题,库会自动处理这些差异。

6. 常见问题和解决方案

1. 浏览器 API 不可用

在服务端渲染时,浏览器 API(如 windowdocument)不可用,这会导致错误。

解决方案

javascript 复制代码
function Component() {
  const [windowWidth, setWindowWidth] = useState(0);
  
  useEffect(() => {
    // 只在客户端执行
    setWindowWidth(window.innerWidth);
    
    const handleResize = () => {
      setWindowWidth(window.innerWidth);
    };
    
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  
  return (
    <div>
      <p>窗口宽度: {windowWidth || '...'}px</p>
    </div>
  );
}

2. 数据获取

在服务端渲染应用中,数据获取通常需要在服务端完成,然后传递给客户端。

解决方案

javascript 复制代码
// 服务端
async function renderApp(req, res) {
  // 在服务端获取数据
  const data = await fetchData();
  
  const appHtml = renderToString(
    <App initialData={data} />
  );
  
  res.send(`
    <!DOCTYPE html>
    <html>
      <head><title>React SSR 应用</title></head>
      <body>
        <div id="root">${appHtml}</div>
        <script>
          window.__INITIAL_DATA__ = ${JSON.stringify(data)};
        </script>
        <script src="/client.js"></script>
      </body>
    </html>
  `);
}

// 客户端
function App({ initialData }) {
  const [data, setData] = useState(initialData);
  
  useEffect(() => {
    // 只在客户端执行,用于后续数据更新
    if (!initialData) {
      fetchData().then(result => {
        setData(result);
      });
    }
  }, [initialData]);
  
  return (
    <div>
      {data ? (
        <ul>
          {data.map(item => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ul>
      ) : (
        <p>加载中...</p>
      )}
    </div>
  );
}

7. 最佳实践

  1. 使用 useEffect 处理仅客户端的逻辑:如 DOM 操作、事件监听、浏览器 API 调用等
  2. 使用条件渲染处理服务端和客户端的差异 :如使用 typeof window !== 'undefined' 检查
  3. 避免在服务端和客户端产生不同的渲染结果:这会导致水合不匹配
  4. 使用 useLayoutEffect 的替代方案 :创建一个同构的 useLayoutEffect 钩子
  5. 在服务端获取数据,在客户端更新数据:避免在客户端重复获取初始数据

结论

理解 useEffect 在服务端渲染环境中的执行行为对于构建高质量的 SSR 应用至关重要。通过正确处理服务端和客户端的差异,可以创建流畅、高效的 SSR 应用,提供出色的用户体验。

相关推荐
小满zs14 分钟前
React-router v7 第四章(路由传参)
前端·react.js
键指江湖1 小时前
React 在组件间共享状态
前端·javascript·react.js
徐小夕2 小时前
花了2个月时间研究了市面上的4款开源表格组件,崩溃了,决定自己写一款
前端·javascript·react.js
聪明的墨菲特i4 小时前
React与Vue:哪个框架更适合入门?
开发语言·前端·javascript·vue.js·react.js
JiangJiang6 小时前
🚀 Vue 人看 useMemo:别再滥用它做性能优化
前端·react.js·面试
Dragon Wu6 小时前
前端 React 弹窗式 滑动验证码实现
前端·javascript·react.js·typescript·前端框架·reactjs
AI程序员罗尼8 小时前
React SSR 水合(Hydration)详解
react.js
AI程序员罗尼9 小时前
React 服务端渲染 (SSR) 详解
react.js
Rachel_wang10 小时前
如何安装并启动 electron-prokit(react+ts) 项目
react.js·electron