StrictMode

StrictMode 帮你检测应用中的潜在问题,但不渲染可见 UI。

核心概述

⚠️ 痛点: React 应用中的隐藏问题难以发现

  • • 不安全的生命周期方法(如 componentWillMount)
  • • 过时的 API 使用(如 ReactDOM.render)
  • • 意外的副作用(如多次渲染时的问题)
  • • 过时的 Context API 使用

✅ 解决方案: StrictMode 开发时检测工具

  • 不安全的生命周期检测: 识别过时的生命周期方法
  • 过时 API 警告: 提醒使用新 API
  • 双重渲染: 通过多次执行发现副作用
  • 仅开发模式: 不影响生产构建

💡 心智模型: 代码质量扫描仪

将 StrictMode 想象成"代码质量扫描仪":

  • 静态分析: 检测不安全的生命周期和过时 API
  • 动态测试: 通过双重执行发现副作用
  • 开发专用: 只在开发环境启用,生产环境自动移除
  • 零运行时成本: 不影响生产性能

技术规格

类型签名

<StrictMode>
  {/* 子组件 */}
</StrictMode>

// 或使用 Fragment 简写
<React.StrictMode>
  {/* 子组件 */}
</React.StrictMode>

检查清单

检查项说明
不安全的生命周期componentWillMount, componentWillReceiveProps, componentWillUpdate
过时的 APIReactDOM.render, findDOMNode, 旧版 Context API
意外的副作用通过双重渲染 constructor, render, setState 等发现
过时的 Context API使用 contextType 和新的 Context API

实战演练

示例 1: 启用 StrictMode

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root')!);

root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

// 或使用 React.StrictMode
import React from 'react';

root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

示例 2: 检测不安全的生命周期

// ❌ 使用不安全的生命周期
class OldComponent extends React.Component {
  componentWillMount() {
    // 不安全的生命周期方法
    console.log('Component will mount');
  }

  render() {
    return <div>Old Component</div>;
  }
}

// 在 StrictMode 下会显示警告:
// Warning: componentWillMount has been renamed, and is not recommended for use.

// ✅ 使用安全的生命周期
class NewComponent extends React.Component {
  componentDidMount() {
    // 安全的生命周期方法
    console.log('Component did mount');
  }

  render() {
    return <div>New Component</div>;
  }
}

示例 3: 检测意外的副作用

// ❌ 有副作用的组件
function ProblematicComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    // 直接在 effect 中获取数据
    fetch('/api/data')
      .then(res => res.json())
      .then(data => setData(data));
  }, []); // 依赖数组为空,但可能有问题

  return <div>{JSON.stringify(data)}</div>;
}

// StrictMode 会:
// 1. 卸载并重新挂载组件
// 2. 发现 fetch 被调用两次
// 3. 提醒你可能有副作用问题

// ✅ 修复: 使用清理函数
function FixedComponent() {
  const [data, setData] = useState(null);
  const [ignore, setIgnore] = useState(false);

  useEffect(() => {
    let aborted = false;

    const fetchData = async () => {
      const response = await fetch('/api/data');
      const json = await response.json();

      if (!aborted) {
        setData(json);
      }
    };

    fetchData();

    // 清理函数
    return () => {
      aborted = true;
    };
  }, []);

  return <div>{JSON.stringify(data)}</div>;
}

避坑指南

❌ 错误 1: 在生产环境启用 StrictMode

// ❌ 错误: 手动条件判断很麻烦
const isDev = process.env.NODE_ENV === 'development';

function App() {
  return isDev ? (
    <StrictMode>
      <MainApp />
    </StrictMode>
  ) : (
    <MainApp />
  );
}

// 问题:
// 1. 代码冗余
// 2. 容易忘记切换
// 3. 维护成本高

✅ 正确 1: 始终启用 StrictMode

// ✅ 正确: 直接使用,React 会自动处理
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

// 优点:
// 1. 代码简洁
// 2. 生产环境自动移除 (打包工具优化)
// 3. 零运行时开销

// Next.js 示例 (pages/_app.js)
export default function MyApp({ Component, pageProps }) {
  return (
    <StrictMode>
      <Component {...pageProps} />
    </StrictMode>
  );
}

❌ 错误 2: 忽视 StrictMode 警告

// ❌ 错误: 看到警告但不修复
class MyComponent extends React.Component {
  componentWillMount() {
    // 警告: componentWillMount 已过时
    // 开发者: "能跑就行,忽略警告"
  }

  render() {
    return <div>...</div>;
  }
}

// 问题:
// 1. 未来版本会移除这些 API
// 2. 积累技术债务
// 3. 升级 React 时会出错

✅ 正确 2: 及时修复所有警告

// ✅ 正确: 逐步修复所有警告
// 优先级:
// 1. 高优先级: 不安全的生命周期、过时的 API
// 2. 中优先级: 副作用警告
// 3. 低优先级: 其他建议

class FixedComponent extends React.Component {
  componentDidMount() {
    // ✅ 使用安全的生命周期
  }

  render() {
    return <div>...</div>;
  }
}

// 优点:
// 1. 代码面向未来
// 2. 平滑升级 React 版本
// 3. 最佳实践

❌ 错误 3: 依赖双重渲染行为

// ❌ 错误: 依赖组件只渲染一次
function BrokenComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 假设这个 effect 只运行一次
    console.log('This should run once');

    fetch('/api/increment')
      .then(res => res.json())
      .then(data => setCount(data.count));
  }, []);

  return <div>Count: {count}</div>;
}

// 问题:
// StrictMode 会双重渲染 effect
// 导致 fetch 被调用两次
// 数据可能不一致

✅ 正确 3: 编写幂等的代码

// ✅ 正确: effect 应该是幂等的
function FixedComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    let aborted = false;

    const incrementCount = async () => {
      const response = await fetch('/api/increment');
      const data = await response.json();

      if (!aborted) {
        setCount(data.count);
      }
    };

    incrementCount();

    // 清理函数防止重复
    return () => {
      aborted = true;
    };
  }, []);

  return <div>Count: {count}</div>;
}

// 优点:
// 1. 多次调用也是安全的
// 2. 避免竞态条件
// 3. 符合 React 最佳实践

最佳实践

1. 全局启用 StrictMode

// 推荐方式: 在应用根组件启用
// Next.js (pages/_app.js)
export default function MyApp({ Component, pageProps }) {
  return (
    <StrictMode>
      <Component {...pageProps} />
    </StrictMode>
  );
}

// Next.js (app/layout.js)
export default function RootLayout({ children }) {
  return (
    <html lang="zh-CN">
      <body>
        <StrictMode>{children}</StrictMode>
      </body>
    </html>
  );
}

// Create React App (index.js)
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

2. 局部启用 StrictMode

// 如果只想检查特定组件
function App() {
  return (
    <Fragment>
      <Header />
      <StrictMode>
        {/* 只检查这部分 */}
        <ProblematicComponent />
      </StrictMode>
      <Footer />
    </Fragment>
  );
}

// 使用场景:
// 1. 逐步迁移旧代码
// 2. 性能敏感的应用 (局部检查)
// 3. 测试特定功能模块

3. 配合 ESLint 使用

// .eslintrc.json
{
  "extends": [
    "react-app",
    "plugin:react-hooks/recommended"
  ],
  "rules": {
    "react/no-deprecated": "error",
    "react/no-string-refs": "error",
    "react/no-unsafe": "warn",
    "react-hooks/exhaustive-deps": "warn"
  }
}

// 优点:
// 1. 编码时就发现问题
// 2. 配合 StrictMode 双重保障
// 3. 强制执行最佳实践

4. 处理双重渲染

// StrictMode 在开发模式下会双重渲染:
// 1. 第一次渲染 (检测副作用)
// 2. 卸载组件
// 3. 第二次渲染 (验证幂等性)

// 如何验证你的组件是否正确:

// ✅ 构造函数/渲染应该是纯净的
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    // ✅ 不要在这里获取数据
    this.state = { count: 0 };
  }

  render() {
    // ✅ 不要在这里产生副作用
    return <div>{this.state.count}</div>;
  }

  componentDidMount() {
    // ✅ 在这里获取数据
  }
}

// ✅ Effect 应该有清理函数
useEffect(() => {
  const subscription = props.source.subscribe();

  return () => {
    // 清理订阅
    subscription.unsubscribe();
  };
}, [props.source]);

相关链接

这篇文章有帮助吗?