Skip to content

严格模式使用 - React StrictMode完全指南

本文档详细介绍React StrictMode的使用方法、作用机制和最佳实践。

1. StrictMode概述

1.1 什么是StrictMode

tsx
import { StrictMode } from 'react';

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

StrictMode是React的开发模式工具:

  • 不渲染任何可见UI
  • 仅在开发模式生效
  • 为后代组件激活额外检查和警告
  • 帮助识别潜在问题

1.2 主要功能

typescript
const strictModeFeatures = {
  检测不安全生命周期: '识别使用过时生命周期方法的组件',
  警告字符串ref: '检测使用过时字符串ref的情况',
  警告findDOMNode: '检测使用已废弃findDOMNode',
  检测副作用: '检测意外的副作用',
  检测过时context: '检测使用过时的context API',
  检测不稳定状态: '检测可能导致问题的状态更新'
};

2. 双重渲染机制

2.1 为什么双重渲染

tsx
// StrictMode会调用以下函数两次:
// - 组件函数体
// - useState, useMemo, useReducer的初始化函数
// - class组件的constructor, render, shouldComponentUpdate

function Counter() {
  console.log('Render'); // 开发模式下打印两次
  
  const [count, setCount] = useState(() => {
    console.log('useState init'); // 打印两次
    return 0;
  });
  
  const doubled = useMemo(() => {
    console.log('useMemo'); // 打印两次
    return count * 2;
  }, [count]);
  
  return <div>{count}</div>;
}

2.2 目的

typescript
const doubleInvocationPurpose = {
  检测副作用: '帮助发现不纯的渲染逻辑',
  暴露问题: '双重调用会放大副作用产生的问题',
  未来兼容: '为React未来特性(如并发模式)做准备',
  
  示例: {
    不纯函数: `
      // ❌ 不纯: 有副作用
      function Component() {
        globalCounter++;  // 副作用
        return <div>{globalCounter}</div>;
      }
      
      // ✅ 纯函数: 无副作用
      function Component({ counter }) {
        return <div>{counter}</div>;
      }
    `
  }
};

3. 检测不安全的生命周期

3.1 过时的生命周期方法

tsx
// ❌ StrictMode会警告使用这些方法
class OldComponent extends React.Component {
  // 不安全: 可能在异步渲染中多次调用
  componentWillMount() {
    this.setState({ data: [] });
  }
  
  // 不安全: 可能在props未变化时也被调用
  componentWillReceiveProps(nextProps) {
    if (nextProps.id !== this.props.id) {
      this.fetchData(nextProps.id);
    }
  }
  
  // 不安全: 可能在渲染过程中被多次调用
  componentWillUpdate(nextProps, nextState) {
    if (nextState.data.length > this.state.data.length) {
      this.logDataChange();
    }
  }
  
  render() {
    return <div>{this.state.data}</div>;
  }
}

// ✅ 使用安全的替代方案
class ModernComponent extends React.Component {
  state = { data: [] };
  
  // 安全: 仅在mount时调用一次
  componentDidMount() {
    this.fetchData(this.props.id);
  }
  
  // 安全: 用于派生状态
  static getDerivedStateFromProps(props, state) {
    if (props.id !== state.prevId) {
      return {
        prevId: props.id,
        data: []
      };
    }
    return null;
  }
  
  // 安全: 在DOM更新后调用
  componentDidUpdate(prevProps, prevState) {
    if (prevProps.id !== this.props.id) {
      this.fetchData(this.props.id);
    }
    
    if (prevState.data.length < this.state.data.length) {
      this.logDataChange();
    }
  }
  
  render() {
    return <div>{this.state.data}</div>;
  }
}

// ✅ 更好: 使用函数组件和Hooks
function ModernFunctionComponent({ id }) {
  const [data, setData] = useState([]);
  
  useEffect(() => {
    fetchData(id).then(setData);
  }, [id]);
  
  useEffect(() => {
    if (data.length > 0) {
      logDataChange();
    }
  }, [data.length]);
  
  return <div>{data}</div>;
}

4. 检测副作用

4.1 渲染阶段的副作用

tsx
// ❌ 不纯的渲染
let globalCounter = 0;

function BadCounter() {
  globalCounter++;  // 副作用!StrictMode会导致不一致
  return <div>Count: {globalCounter}</div>;
}

// ✅ 纯渲染
function GoodCounter() {
  const [counter, setCounter] = useState(0);
  
  // 副作用放在useEffect中
  useEffect(() => {
    const interval = setInterval(() => {
      setCounter(c => c + 1);
    }, 1000);
    
    return () => clearInterval(interval);
  }, []);
  
  return <div>Count: {counter}</div>;
}

4.2 初始化函数的副作用

tsx
// ❌ 初始化函数有副作用
let cache = [];

function BadComponent() {
  const [data] = useState(() => {
    cache.push('item');  // 副作用!
    return cache;
  });
  
  return <div>{data.length}</div>;
}

// ✅ 纯的初始化函数
function GoodComponent() {
  const [data] = useState(() => {
    // 从localStorage读取(虽然是副作用,但是可接受的)
    const saved = localStorage.getItem('data');
    return saved ? JSON.parse(saved) : [];
  });
  
  // 副作用放在useEffect中
  useEffect(() => {
    localStorage.setItem('data', JSON.stringify(data));
  }, [data]);
  
  return <div>{data.length}</div>;
}

5. 检测过时的API

5.1 字符串ref

tsx
// ❌ StrictMode会警告
class OldRefComponent extends React.Component {
  componentDidMount() {
    this.refs.input.focus();  // 字符串ref
  }
  
  render() {
    return <input ref="input" />;
  }
}

// ✅ 使用回调ref
class CallbackRefComponent extends React.Component {
  inputRef = null;
  
  componentDidMount() {
    if (this.inputRef) {
      this.inputRef.focus();
    }
  }
  
  render() {
    return <input ref={node => this.inputRef = node} />;
  }
}

// ✅ 使用createRef
class ModernRefComponent extends React.Component {
  inputRef = React.createRef();
  
  componentDidMount() {
    this.inputRef.current?.focus();
  }
  
  render() {
    return <input ref={this.inputRef} />;
  }
}

// ✅ 函数组件使用useRef
function FunctionRefComponent() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    inputRef.current?.focus();
  }, []);
  
  return <input ref={inputRef} />;
}

5.2 findDOMNode

tsx
// ❌ StrictMode会警告
class OldDOMComponent extends React.Component {
  componentDidMount() {
    const node = ReactDOM.findDOMNode(this);  // 已废弃
    node.scrollIntoView();
  }
  
  render() {
    return <div>Content</div>;
  }
}

// ✅ 使用ref
class ModernDOMComponent extends React.Component {
  divRef = React.createRef();
  
  componentDidMount() {
    this.divRef.current?.scrollIntoView();
  }
  
  render() {
    return <div ref={this.divRef}>Content</div>;
  }
}

// ✅ 函数组件
function FunctionDOMComponent() {
  const divRef = useRef(null);
  
  useEffect(() => {
    divRef.current?.scrollIntoView();
  }, []);
  
  return <div ref={divRef}>Content</div>;
}

5.3 过时的Context API

tsx
// ❌ StrictMode会警告
class OldContextComponent extends React.Component {
  static childContextTypes = {
    theme: PropTypes.string
  };
  
  getChildContext() {
    return { theme: 'dark' };
  }
  
  render() {
    return this.props.children;
  }
}

// ✅ 使用新的Context API
const ThemeContext = React.createContext('light');

function ModernContextProvider({ children }) {
  const [theme, setTheme] = useState('dark');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function ModernContextConsumer() {
  const { theme } = useContext(ThemeContext);
  return <div>Theme: {theme}</div>;
}

6. StrictMode最佳实践

6.1 全局启用

tsx
// ✅ 在根组件启用
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'));
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

6.2 部分启用

tsx
// ✅ 仅对特定部分启用
function App() {
  return (
    <div>
      <Header />
      
      {/* 仅对新功能启用StrictMode */}
      <StrictMode>
        <NewFeature />
      </StrictMode>
      
      <Footer />
    </div>
  );
}

6.3 排除第三方库

tsx
// ✅ 排除不兼容的第三方组件
function App() {
  return (
    <StrictMode>
      <MyComponents />
      
      {/* 第三方库可能不兼容StrictMode */}
      <div>
        <ThirdPartyComponent />
      </div>
    </StrictMode>
  );
}

7. 常见问题和解决方案

7.1 useEffect执行两次

tsx
// ❌ 误解: 认为useEffect有bug
function Component() {
  useEffect(() => {
    console.log('Mounted'); // 打印两次
    fetchData();  // 请求两次
  }, []);
}

// ✅ 理解: StrictMode故意为之
// 解决: 确保副作用是幂等的
function Component() {
  useEffect(() => {
    let cancelled = false;
    
    fetchData().then(data => {
      if (!cancelled) {
        setData(data);
      }
    });
    
    return () => {
      cancelled = true;
    };
  }, []);
}

7.2 状态更新不一致

tsx
// ❌ 问题: 依赖外部可变状态
let externalState = 0;

function Component() {
  const [count, setCount] = useState(() => {
    return externalState++;  // 不一致!
  });
}

// ✅ 解决: 使用组件内部状态
function Component() {
  const [count, setCount] = useState(0);
  
  const increment = () => {
    setCount(c => c + 1);
  };
  
  return <button onClick={increment}>{count}</button>;
}

7.3 第三方库兼容性

tsx
// ❌ 第三方库可能不兼容StrictMode
import SomeLibrary from 'some-library';

function App() {
  return (
    <StrictMode>
      <SomeLibrary />  {/* 可能报warning */}
    </StrictMode>
  );
}

// ✅ 解决方案
// 1. 更新库到兼容版本
// 2. 联系库作者
// 3. 暂时排除该组件
function App() {
  return (
    <div>
      <StrictMode>
        <MyComponents />
      </StrictMode>
      
      <SomeLibrary />
    </div>
  );
}

8. 生产环境

tsx
// StrictMode在生产环境中自动禁用
// 不会影响性能或用户体验

const isDevelopment = process.env.NODE_ENV === 'development';

function App() {
  const app = <MainApp />;
  
  if (isDevelopment) {
    return <StrictMode>{app}</StrictMode>;
  }
  
  return app;
}

9. React 18增强

9.1 重用状态

tsx
// React 18的StrictMode会:
// 1. Mount组件
// 2. Unmount组件 (运行cleanup)
// 3. Mount组件 (使用之前的state)

function Component() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    console.log('Mount');
    
    return () => {
      console.log('Cleanup');
    };
  }, []);
  
  // 在StrictMode下:
  // Mount -> Cleanup -> Mount (state保留)
}

9.2 为未来特性准备

typescript
const react18StrictMode = {
  目的: '为即将到来的特性做准备',
  特性: [
    'Offscreen组件',
    '保留和恢复状态',
    '更好的并发支持'
  ],
  建议: '尽早适配StrictMode,为未来升级做准备'
};

10. 总结

StrictMode使用要点:

  1. 开发工具: 仅在开发模式生效
  2. 双重渲染: 帮助发现副作用
  3. 检测问题: 不安全生命周期、过时API
  4. 最佳实践: 全局启用、确保副作用纯净
  5. 兼容性: 注意第三方库
  6. 生产环境: 自动禁用,无性能影响
  7. React 18: 新增状态重用检查
  8. 未来准备: 为新特性做准备

启用StrictMode是提升代码质量的重要步骤,建议所有React项目都使用。