在Stack Overflow上回答与React框架相关的问题时,我注意到人们对于这个框架有几类主要的问题。我决定将一些最常见的问题和如何处理这些问题的解决方法写出来,以期对那些还不熟悉React框架的人,或那些正在努力掌握其基本概念的人有所帮助。在本文中,对于使用基于类组件和使用钩子(hook)的函数组件遇到的问题,都会交叉谈到。
直接修改状态
React中的状态被认为是不可变的,因此不应该直接修改。如果要修改状态值,应该使用一个特殊的setState方法和useState钩子中的setter函数。考虑下面的例子,在这个例子中,你希望根据复选框(checkbox)的状态更新数组中特定对象的checked字段。
const updateFeaturesList = (e, idx) => {
listFeatures[idx].checked = e.target.checked;
setListFeatures(listFeatures);
};
这段代码的问题在于,对状态的更改不会反映到UI中,因为状态更新使用了相同的对象引用,因此不会触发重新渲染(re-render)动作。不能直接改变状态的另一个重要原因是,由于它的异步特性,后面的状态更新可能会覆盖直接对状态所做的更新,从而导致一些无法查清的错误。在这种情况下,正确的方法是使用useState的setter方法。
const updateFeaturesList = (e, idx) => {
const { checked } = e.target;
setListFeatures(features => {
return features.map((feature, index) => {
if (id === index) {
feature = { ...feature, checked };
}
return feature;
});
});
};
通过使用map和object spread(对象展开),我们还能确保不会更改原始状态项。
在初始状态上设置错误的值类型
将初始状态值设置为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 (
User name:
{this.state.user.name}
// Cannnot read property 'name' of null
);
}
}
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 (
User name:
{this.state.user.name}
// Renders without errors
);
}
}
从用户体验的角度来看,最好展示某种loader的结果给用户,直到数据被正确地获取到。
忘记setState是异步的
另一个常见的错误是试图在设置状态值之后立即访问它。
handleChange = count => {
this.setState({ count });
this.props.callback(this.state.count); // Old state value
};
设置新值不会立即生效,通常它会在下一个可用的渲染上完成,或者可以进行批量处理以优化性能。因此,在设置状态值之后立即访问该值可能不会得到最新的更新结果。这个问题可以通过使用setState的可选的第二个参数来解决,这个参数是一个回调函数,它在状态值被最新的值更新完成后会被调用。
handleChange = count => {
this.setState({ count }, () => {
this.props.callback(this.state.count); // Updated state value
});
};
不过,这与钩子(hook)的做法有很大不同,因为useState钩子的setter函数没有第二个类似于setState的回调参数。在这种情况下,官方推荐的做法是使用useEffect钩子。
const [count, setCount] = useState(0)
useEffect(() => {
callback(count); // Will be called when the value of count changes
}, [count, callback]);
const handleChange = value => {
setCount(value)
};
应该注意的是,setState方法严格来说并不是异步的,只不过它返回的是一个预期(promise)。因此,对它进行async/await操作或使用then将不起作用(这是另一个常见的错误)。
错误地依赖当前状态值来计算下一个状态
这个问题与上面讨论的问题有关,因为它还是和异步状态更新相关。见下例:
handleChange = count => {
this.setState({ count: this.state.count + 1 }); // Relying on current value of the state to update it
};
上面代码中的这种更新方式存在的问题是:在设置新状态时,count的值可能没有正确更新,这将导致新状态值的设置不正确。正确的方法是使用setState的函数形式。
increment = () => {
this.setState(state => ({ count: state.count + 1 })); // The latest state value is used
};
setState的函数形式在更新被执行时有第二个参数 - props,可以以和state参数类似的方式使用。
同样的逻辑也适用于useState钩子,其中setter接受函数作为参数。
const increment = () => {
setCount(currentCount => currentCount + 1)
};
忽略useEffect的dependency数组
这是一个不太常见的错误,但仍然时有发生。即使有完全有效的情况可以忽略useEffect的dependency数组,但在其回调函数更新状态时这样做可能会导致无限循环。
将非基元类型的对象或其它值传递给useEffect的dependency数组
与上面的情况类似,但更微妙的错误是跟踪对象、数组或effect钩子的dependency数组中的其他非基元值。考虑下面的代码:
const features = ["feature1", "feature2"];
useEffect(() => {
// Callback
}, [features]);
在这里,当我们将数组作为一个dependency数组传递时,React将只存储对它的引用,并将其与数组的上一个引用进行比较。但是,由于它是在组件内部声明的,因此在每次渲染时都会重新创建features数组,这意味着它的引用每次都是新的,因此不等于useEffect跟踪的引用。最终,即使数组没有被更改,回调函数也会在每个render方法上运行。对于基元类型的值(如字符串和数字)来说,这不是问题,因为它们在JavaScript中是按值来比较的,而不是按引用来比较。
有几种方法可以解决这个问题。第一个方法是将变量声明移到组件之外,这样就不会在每次渲染时重新创建它。但是,在某些情况下,这是不可能的,例如,如果我们正在跟踪的props,或者跟踪的依赖项是组件状态的一部分。另一种方法是使用自定义的deep compare hook来正确地跟踪依赖项引用。而更简单的解决方法是将值包装到usememohook中,这种做法会在重新渲染期间保留引用。
const features = useMemo(() => ["feature1", "feature2"], []);
useEffect(() => {
// Callback
}, [features]);
希望上面的这个列表能够帮助你避免最常见的React使用问题,并提高对主要问题的理解。
如果你有关于这篇文章的任何问题/评论或其他类型的反馈,请在此评论或在推特上告诉我。
原文:https://dev.to/clarity89/the-most-common-mistakes-when-using-react-45h2
本文为 CSDN 翻译,转载请注明来源出处。
【END】