使用 React 时最常见的错误

2025-05-24

使用 React 时最常见的错误

该文章最初发布在我的个人博客上。

在 Stack Overflow 上回答 React 相关问题时,我注意到大家在使用这个库时遇到的问题主要分为几类。我决定写一些最常见的问题,并展示如何处理它们,希望能对 React 新手,以及所有正在努力理解其基本概念的人有所帮助。本文交替介绍了使用基于类的组件和使用 hooks 的函数式组件的陷阱。 

1. 直接修改状态

React 中的状态被视为不可变的,因此不应直接更改。应该使用钩子setState 中的特殊方法和 setter 函数useState 。考虑以下示例,您希望checked根据复选框的状态更新数组中特定对象的字段。

    const updateFeaturesList = (e, idx) => {
      listFeatures[idx].checked = e.target.checked;
      setListFeatures(listFeatures);
    };
Enter fullscreen mode Exit fullscreen mode

这段代码的问题在于,由于状态更新使用的是相同的对象引用,因此不会触发重新渲染,因此状态的更改不会反映在 UI 中。不直接修改状态的另一个重要原因是,由于状态的异步特性,后续的状态更新可能会覆盖直接对状态进行的更新,从而导致一些难以规避的错误。在这种情况下,正确的做法是使用 useState 的 setter 方法。

    const updateFeaturesList = (e, idx) => {
      const { checked } = e.target;
      setListFeatures(features => {
        return features.map((feature, index) => {
          if (idx === index) {
            feature = { ...feature, checked };
          }
          return feature;
        });
      });
    };
Enter fullscreen mode Exit fullscreen mode

通过使用map对象扩展,我们还确保不会改变原始状态项目。

2. 初始状态设置错误的值类型

将初始状态值设置为null 空字符串,然后在 render 中像访问对象一样访问该值的属性,这是一个相当常见的错误。同样,不为嵌套对象提供默认值,然后在 render 或其他组件方法中尝试访问它们也会导致同样的错误。

 

    class UserProfile extends Component {
      constructor(props) {
        super(props);

        this.state = {
          user: null
        };
      }

      componentDidMount() {
        fetch("/api/profile").then(data => {
          this.setState({ user: data });
        });
      }

      render() {
        return (
          <div>
            <p>User name:</p>
            <p>{this.state.user.name}</p> // Cannnot read property 'name' of null
          </div>
        );
      }
    }
Enter fullscreen mode Exit fullscreen mode

将初始状态的值设置为空数组,然后尝试访问其中的第 n 个元素时,也会出现类似的错误。当通过 API 调用获取数据时,组件将使用提供的初始状态进行渲染,尝试访问元素的属性nullundefined导致错误。因此,确保初始状态与更新后的状态紧密相关非常重要。在我们的例子中,正确的状态初始化如下:

    class UserProfile extends Component {
      constructor(props) {
        super(props);

        this.state = {
          user: {
            name: ""
            // Define other fields as well
          }
        };
      }

      componentDidMount() {
        fetch("/api/profile").then(data => {
          this.setState({ user: data });
        });
      }

      render() {
        return (
          <div>
            <p>User name:</p>
            <p>{this.state.user.name}</p> // Renders without errors
          </div>
        );
      }
    }
Enter fullscreen mode Exit fullscreen mode

从用户体验 (UX) 的角度来看,最好在获取数据之前显示某种加载器。 

3. 忘记setState 异步

另一个常见的错误是尝试在设置状态值后立即访问它。

    handleChange = count => {
      this.setState({ count });
      this.props.callback(this.state.count); // Old state value
    };
Enter fullscreen mode Exit fullscreen mode

设置新值不会立即生效,通常会在下一个可用渲染时完成,或者可以批量处理以优化性能。因此,在设置状态值后访问状态值可能无法反映最新更新。此问题可以通过使用 的可选第二个参数来解决setState,该参数是一个回调函数,在状态更新为最新值后调用。

 

    handleChange = count => {
      this.setState({ count }, () => {
        this.props.callback(this.state.count); // Updated state value
      });
    };
Enter fullscreen mode Exit fullscreen mode

不过,使用 hooks 就完全不同了,因为 from 的 setter 函数useState 没有像 那样的第二个回调参数setState。在这种情况下,官方推荐的方式是使用useEffecthook。

    const [count, setCount] = useState(0)

    useEffect(() => {
      callback(count); // Will be called when the value of count changes
    }, [count, callback]);

    const handleChange = value => {
      setCount(value)
    };
Enter fullscreen mode Exit fullscreen mode

需要注意的是,它setState并非异步的,因为它返回一个 Promise。因此,直接async/await使用它或使用then是行不通的(这也是一个常见错误)。 

4. 错误地依赖当前状态值来计算下一个状态

该问题与上面讨论的问题相关,因为它也与状态更新异步有关。

    handleChange = count => {
      this.setState({ count: this.state.count + 1 }); // Relying on current value of the state to update it
    };
Enter fullscreen mode Exit fullscreen mode

这种方法的问题在于,在设置新状态时,count 的值可能无法正确更新,从而导致新状态值设置不正确。正确的方法是使用 的函数形式setState

    increment = () => {
      this.setState(state => ({ count: state.count + 1 })); // The latest state value is used
    };
Enter fullscreen mode Exit fullscreen mode

的函数形式setState有第二个参数 - props在应用更新时,可以以与状态类似的方式使用。  

同样的逻辑适用于useState钩子,其中 setter 接受一个函数作为参数。

    const increment = () => {
     setCount(currentCount => currentCount + 1)
    };
Enter fullscreen mode Exit fullscreen mode

5. 省略依赖数组 useEffect

这是一个不太常见的错误,但还是会发生。尽管省略 for 的依赖数组完全合理useEffect,但当它的回调修改状态时,这样做可能会导致无限循环。

6. 将对象或其他非原始类型的值传递给 useEffect的依赖数组

与上述情况类似,但更微妙的错误是跟踪 effect hook 依赖数组中的对象、数组或其他非原始值。请考虑以下代码。

 

    const features = ["feature1", "feature2"];

    useEffect(() => {
      // Callback 
    }, [features]);
Enter fullscreen mode Exit fullscreen mode

在这里,当我们将数组作为依赖项传递时,React 将仅存储对它的引用,并将其与数组的前一个引用进行比较。但是,由于它是在组件内部声明的,因此features每次渲染时都会重新创建数组,这意味着它的引用每次都是一个新的,因此不等于 跟踪的引用useEffect。最终,即使数组没有更改,回调函数也会在每次渲染时运行。对于原始值(例如字符串和数字)来说,这不是问题,因为在 JavaScript 中它们是按值而不是按引用进行比较的。

有几种方法可以解决这个问题。第一种方法是将变量声明移到组件外部,这样就不会在每次渲染时重新创建它。但是,在某些情况下这是不可能的,例如,如果我们跟踪 props 或被跟踪的依赖项是组件状态的一部分。另一种选择是使用自定义深度比较钩子来正确跟踪依赖项引用。一个更简单的解决方案是将值包装到useMemo钩子中,这样在重新渲染期间就可以保留引用。

    const features = useMemo(() => ["feature1", "feature2"], []);

    useEffect(() => {
      // Callback 
    }, [features]);
Enter fullscreen mode Exit fullscreen mode

希望此列表能够帮助您避免最常见的 React 问题并提高对主要陷阱的理解。 

对这篇文章有任何问题/评论或其他反馈吗?请在评论区或 Twitter上告诉我。

文章来源:https://dev.to/clarity89/the-most-common-mistakes-when-using-react-45h2
PREV
Netflix 简介动画 - 纯 CSS
NEXT
接下来我应该做什么?快速项目创意