Appearance
严格模式使用 - 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使用要点:
- 开发工具: 仅在开发模式生效
- 双重渲染: 帮助发现副作用
- 检测问题: 不安全生命周期、过时API
- 最佳实践: 全局启用、确保副作用纯净
- 兼容性: 注意第三方库
- 生产环境: 自动禁用,无性能影响
- React 18: 新增状态重用检查
- 未来准备: 为新特性做准备
启用StrictMode是提升代码质量的重要步骤,建议所有React项目都使用。