Skip to content

严格模式检查项 - 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-types

6.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检查项完整清单:

  1. 不安全生命周期: componentWillMount/ReceiveProps/Update
  2. 过时API: 字符串ref、findDOMNode、Legacy Context
  3. 副作用: 渲染函数、初始化函数、useEffect重复
  4. React 18: 状态保留、清理函数

修复优先级:

  1. 高: 不安全生命周期、findDOMNode
  2. 中: 字符串ref、Legacy Context
  3. 低: 优化副作用处理

建议:

  • 所有新项目从一开始就启用StrictMode
  • 现有项目逐步迁移
  • 使用自动化工具辅助
  • 建立code review流程确保新代码符合规范

StrictMode是提升代码质量、为未来特性做准备的重要工具。