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 应用,提供出色的用户体验。

相关推荐
Dragon Wu8 分钟前
React state在setInterval里未获取最新值的问题
前端·javascript·react.js·前端框架
YU大宗师11 分钟前
React面试题
前端·javascript·react.js
木兮xg12 分钟前
react基础篇
前端·react.js·前端框架
三思而后行,慎承诺2 小时前
Reactnative实现远程热更新的原理是什么
javascript·react native·react.js
知识分享小能手2 小时前
React学习教程,从入门到精通,React 组件生命周期详解(适用于 React 16.3+,推荐函数组件 + Hooks)(17)
前端·javascript·vue.js·学习·react.js·前端框架·vue3
夏天19955 小时前
React:聊一聊状态管理
前端·javascript·react.js
LFly_ice6 小时前
学习React-11-useDeferredValue
前端·学习·react.js
LFly_ice11 小时前
学习React-10-useTransition
前端·学习·react.js
知识分享小能手11 小时前
React学习教程,从入门到精通,React 构造函数(Constructor)完整语法知识点与案例详解(16)
前端·javascript·学习·react.js·架构·前端框架·vue