Appearance
严格模式检查项 - React StrictMode详细检查清单
本文档详细列举React StrictMode的所有检查项及对应的修复方案。
1. 不安全的生命周期检查
1.1 componentWillMount
tsx
// ❌ 检查项: 使用componentWillMount
class OldComponent extends React.Component {
componentWillMount() {
this.setState({ loading: true });
this.fetchData();
}
render() {
return <div>{this.state.data}</div>;
}
}
// ⚠️ Warning:
// componentWillMount has been renamed, and is not recommended for use.
// Use componentDidMount instead.
// ✅ 修复: 使用componentDidMount
class FixedComponent extends React.Component {
componentDidMount() {
this.setState({ loading: true });
this.fetchData();
}
render() {
return <div>{this.state.data}</div>;
}
}
// ✅ 更好: 使用函数组件
function ModernComponent() {
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
setLoading(true);
fetchData().then(data => {
setData(data);
setLoading(false);
});
}, []);
return <div>{data}</div>;
}1.2 componentWillReceiveProps
tsx
// ❌ 检查项: 使用componentWillReceiveProps
class OldComponent extends React.Component {
componentWillReceiveProps(nextProps) {
if (nextProps.userId !== this.props.userId) {
this.fetchUser(nextProps.userId);
}
}
render() {
return <div>{this.state.user}</div>;
}
}
// ⚠️ Warning:
// componentWillReceiveProps has been renamed, and is not recommended for use.
// ✅ 修复1: 使用getDerivedStateFromProps + componentDidUpdate
class FixedComponent1 extends React.Component {
static getDerivedStateFromProps(props, state) {
if (props.userId !== state.prevUserId) {
return {
prevUserId: props.userId,
user: null // 重置状态
};
}
return null;
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUser(this.props.userId);
}
}
render() {
return <div>{this.state.user}</div>;
}
}
// ✅ 修复2: 使用函数组件
function ModernComponent({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return <div>{user}</div>;
}1.3 componentWillUpdate
tsx
// ❌ 检查项: 使用componentWillUpdate
class OldComponent extends React.Component {
componentWillUpdate(nextProps, nextState) {
if (nextState.scrollTop !== this.state.scrollTop) {
this.saveScrollPosition(nextState.scrollTop);
}
}
render() {
return <div>{this.props.content}</div>;
}
}
// ⚠️ Warning:
// componentWillUpdate has been renamed, and is not recommended for use.
// ✅ 修复: 使用getSnapshotBeforeUpdate + componentDidUpdate
class FixedComponent extends React.Component {
getSnapshotBeforeUpdate(prevProps, prevState) {
if (prevState.scrollTop !== this.state.scrollTop) {
return this.state.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot !== null) {
this.saveScrollPosition(snapshot);
}
}
render() {
return <div>{this.props.content}</div>;
}
}
// ✅ 更好: 使用函数组件
function ModernComponent({ content }) {
const [scrollTop, setScrollTop] = useState(0);
useEffect(() => {
saveScrollPosition(scrollTop);
}, [scrollTop]);
return <div>{content}</div>;
}2. 过时API检查
2.1 字符串Ref
tsx
// ❌ 检查项: 使用字符串ref
class OldComponent extends React.Component {
componentDidMount() {
this.refs.input.focus();
}
render() {
return <input ref="input" />;
}
}
// ⚠️ Warning:
// A string ref, "input", has been found within a strict mode tree.
// String refs are a source of potential bugs and should be avoided.
// ✅ 修复1: 使用回调ref
class CallbackRefComponent extends React.Component {
inputRef = null;
setInputRef = (element) => {
this.inputRef = element;
};
componentDidMount() {
if (this.inputRef) {
this.inputRef.focus();
}
}
render() {
return <input ref={this.setInputRef} />;
}
}
// ✅ 修复2: 使用createRef
class CreateRefComponent extends React.Component {
inputRef = React.createRef();
componentDidMount() {
this.inputRef.current?.focus();
}
render() {
return <input ref={this.inputRef} />;
}
}
// ✅ 修复3: 使用useRef
function FunctionComponent() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
}2.2 findDOMNode
tsx
// ❌ 检查项: 使用findDOMNode
import ReactDOM from 'react-dom';
class OldComponent extends React.Component {
componentDidMount() {
const node = ReactDOM.findDOMNode(this);
if (node) {
node.scrollIntoView();
}
}
render() {
return <div>Content</div>;
}
}
// ⚠️ Warning:
// findDOMNode is deprecated in StrictMode.
// findDOMNode was passed an instance of Component which is inside StrictMode.
// ✅ 修复: 使用ref
class FixedComponent extends React.Component {
divRef = React.createRef();
componentDidMount() {
this.divRef.current?.scrollIntoView();
}
render() {
return <div ref={this.divRef}>Content</div>;
}
}
// ✅ 函数组件
function ModernComponent() {
const divRef = useRef(null);
useEffect(() => {
divRef.current?.scrollIntoView();
}, []);
return <div ref={divRef}>Content</div>;
}2.3 Legacy Context API
tsx
// ❌ 检查项: 使用旧的Context API
import PropTypes from 'prop-types';
class OldProvider extends React.Component {
static childContextTypes = {
theme: PropTypes.string
};
getChildContext() {
return { theme: this.props.theme };
}
render() {
return this.props.children;
}
}
class OldConsumer extends React.Component {
static contextTypes = {
theme: PropTypes.string
};
render() {
return <div>Theme: {this.context.theme}</div>;
}
}
// ⚠️ Warning:
// Legacy context API has been detected within a strict-mode tree.
// ✅ 修复: 使用新的Context API
const ThemeContext = React.createContext('light');
function ModernProvider({ theme, children }) {
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}
// 使用Context.Consumer
function ModernConsumer1() {
return (
<ThemeContext.Consumer>
{theme => <div>Theme: {theme}</div>}
</ThemeContext.Consumer>
);
}
// 使用useContext (推荐)
function ModernConsumer2() {
const theme = useContext(ThemeContext);
return <div>Theme: {theme}</div>;
}3. 副作用检查
3.1 渲染阶段副作用
tsx
// ❌ 检查项: 渲染函数中的副作用
let globalCounter = 0;
function BadComponent() {
globalCounter++; // 副作用!
console.log('Rendering'); // 副作用!
return <div>Count: {globalCounter}</div>;
}
// ⚠️ 行为:
// 在StrictMode下,globalCounter会增加两次
// console.log会打印两次
// ✅ 修复: 保持渲染函数纯净
function GoodComponent() {
const [counter, setCounter] = useState(0);
// 副作用放在useEffect中
useEffect(() => {
console.log('Component mounted');
const interval = setInterval(() => {
setCounter(c => c + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>Count: {counter}</div>;
}3.2 初始化函数副作用
tsx
// ❌ 检查项: useState/useMemo初始化函数中的副作用
let sharedCache = [];
function BadComponent() {
const [items] = useState(() => {
sharedCache.push('item'); // 副作用!
return sharedCache;
});
const computed = useMemo(() => {
console.log('Computing'); // 副作用!
return items.length * 2;
}, [items]);
return <div>{computed}</div>;
}
// ⚠️ 行为:
// sharedCache会被修改两次
// console.log会打印两次
// ✅ 修复: 保持初始化函数纯净
function GoodComponent() {
const [items] = useState(() => {
// 纯函数: 从localStorage读取
const saved = localStorage.getItem('items');
return saved ? JSON.parse(saved) : [];
});
const computed = useMemo(() => {
return items.length * 2; // 纯计算
}, [items]);
// 副作用放在useEffect中
useEffect(() => {
console.log('Computed value:', computed);
}, [computed]);
return <div>{computed}</div>;
}3.3 useEffect重复执行
tsx
// ❌ 问题: 未处理useEffect的重复执行
function BadComponent({ userId }) {
useEffect(() => {
// StrictMode会执行两次
fetchUser(userId); // 发送两次请求!
}, [userId]);
}
// ✅ 修复: 使用cleanup取消
function GoodComponent({ userId }) {
useEffect(() => {
let cancelled = false;
fetchUser(userId).then(user => {
if (!cancelled) {
setUser(user);
}
});
return () => {
cancelled = true;
};
}, [userId]);
}
// ✅ 更好: 使用AbortController
function BetterComponent({ userId }) {
useEffect(() => {
const abortController = new AbortController();
fetch(`/api/users/${userId}`, {
signal: abortController.signal
})
.then(res => res.json())
.then(setUser)
.catch(error => {
if (error.name !== 'AbortError') {
console.error(error);
}
});
return () => {
abortController.abort();
};
}, [userId]);
}4. React 18特定检查
4.1 状态保留检查
tsx
// React 18 StrictMode会检查组件是否能正确处理状态保留
function Component() {
const [count, setCount] = useState(0);
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => {
// ❌ 问题: 假设只会执行一次
if (!isInitialized) {
setIsInitialized(true);
// 初始化逻辑
}
}, [isInitialized]);
// StrictMode会:
// 1. Mount (isInitialized = true)
// 2. Unmount + Cleanup
// 3. Mount (isInitialized仍然是true)
// ✅ 修复: 使用ref跟踪是否真的mount过
const hasMounted = useRef(false);
useEffect(() => {
if (!hasMounted.current) {
hasMounted.current = true;
// 初始化逻辑
}
}, []);
}4.2 清理函数检查
tsx
// React 18 StrictMode会验证清理函数
function Component() {
useEffect(() => {
const subscription = subscribeToData();
// ❌ 问题: 没有正确清理
// return () => {}; // 空清理函数
// ✅ 正确清理
return () => {
subscription.unsubscribe();
};
}, []);
useEffect(() => {
const timer = setTimeout(() => {}, 1000);
// ❌ 问题: 忘记清理
// (没有return)
// ✅ 正确清理
return () => {
clearTimeout(timer);
};
}, []);
}5. 检查清单总结
typescript
const strictModeChecklist = {
不安全生命周期: {
检查项: [
'componentWillMount',
'componentWillReceiveProps',
'componentWillUpdate',
'UNSAFE_componentWillMount',
'UNSAFE_componentWillReceiveProps',
'UNSAFE_componentWillUpdate'
],
修复: [
'使用componentDidMount',
'使用getDerivedStateFromProps + componentDidUpdate',
'使用getSnapshotBeforeUpdate + componentDidUpdate',
'迁移到函数组件和Hooks'
]
},
过时API: {
检查项: [
'字符串ref (ref="name")',
'ReactDOM.findDOMNode()',
'Legacy Context API (childContextTypes, contextTypes)'
],
修复: [
'使用createRef或useRef',
'使用ref直接访问',
'使用新的Context API (createContext, useContext)'
]
},
副作用: {
检查项: [
'渲染函数中的副作用',
'useState/useMemo初始化函数中的副作用',
'useEffect重复执行未处理',
'组件卸载后的状态更新'
],
修复: [
'保持渲染函数纯净',
'保持初始化函数纯净',
'使用cleanup函数取消操作',
'检查组件是否已卸载'
]
},
React18特定: {
检查项: [
'状态保留处理',
'清理函数完整性',
'重用状态兼容性'
],
修复: [
'使用ref跟踪mount状态',
'确保清理函数正确执行',
'测试组件在mount/unmount/remount循环中的表现'
]
}
};6. 自动修复工具
6.1 React Codemod
bash
# 安装
npx react-codemod
# 修复不安全的生命周期
npx react-codemod rename-unsafe-lifecycles
# 修复过时的Context API
npx react-codemod React-PropTypes-to-prop-types6.2 ESLint规则
javascript
// .eslintrc.js
module.exports = {
plugins: ['react', 'react-hooks'],
rules: {
// 检测不安全的生命周期
'react/no-unsafe': 'error',
// 检测字符串ref
'react/no-string-refs': 'error',
// 检测findDOMNode
'react/no-find-dom-node': 'error',
// 检测过时的Context
'react/no-deprecated': 'error',
// Hooks规则
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn'
}
};7. 迁移策略
typescript
const migrationStrategy = {
阶段1_准备: [
'1. 在根组件启用StrictMode',
'2. 记录所有warning',
'3. 创建修复任务列表'
],
阶段2_修复: [
'1. 优先修复关键路径',
'2. 批量修复相同类型问题',
'3. 逐个模块修复',
'4. 为每个修复编写测试'
],
阶段3_验证: [
'1. 运行全部测试',
'2. 手动测试关键功能',
'3. 监控生产环境',
'4. 收集反馈'
],
阶段4_维护: [
'1. 保持StrictMode启用',
'2. 新代码遵循规范',
'3. 定期审查',
'4. 持续改进'
]
};8. 总结
StrictMode检查项完整清单:
- 不安全生命周期: componentWillMount/ReceiveProps/Update
- 过时API: 字符串ref、findDOMNode、Legacy Context
- 副作用: 渲染函数、初始化函数、useEffect重复
- React 18: 状态保留、清理函数
修复优先级:
- 高: 不安全生命周期、findDOMNode
- 中: 字符串ref、Legacy Context
- 低: 优化副作用处理
建议:
- 所有新项目从一开始就启用StrictMode
- 现有项目逐步迁移
- 使用自动化工具辅助
- 建立code review流程确保新代码符合规范
StrictMode是提升代码质量、为未来特性做准备的重要工具。