# React错误实践
# 1. Props 透传
props 透传是将单个 props 从父组件向下多层传递的做法。 理想状态下,props 不应该超过两层。 当我们选择多层传递时,会导致一些性能问题,这也让 React 官方比较头疼。 props 透传会导致不必要的重新渲染。因为 React 组件总会在 props 发生变化时重新渲染,而那些不需要 props,只是提供传递作用的中间层组件都会被渲染。 除了性能问题外,props 透传会导致数据难以跟踪,对很多试图看懂代码的人来说也是一种很大的挑战。
const A = () => {
const [title, setTitle] = useState('')
return <B title={title} />
}
const B = ({ title }) => {
return <C title={title} />
}
const C = ({ title }) => {
return <D title={title} />
}
const D = ({ title }) => {
return <div>{title}</div>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
解决这个问题的方法有很多,比如 React Context Hook,或者类似 Redux 的库。 但是使用 Redux 需要额外编写一些代码,它更适合单个状态改变很多东西的复杂场景。简单的项目选择使用 Context Hook 是更好的选择。
# 2. 导入代码超出实际所用的代码
React 是一个前端框架,它有着不小的代码体积。 我们在编写 React 程序时,应该避免导入很多用不到的模块。因为它们也会被打包到运行时代码发送到用户的客户端/浏览器/移动设备上。额外的依赖会导致应用的体积膨胀,增加用户的加载时间,让网页变慢,降低用户体验度。
import _ from 'lodash' // 整个包导入
import _map from 'lodash/map' // 只导入需要的包
2
3
为了保证良好的用户体验度,我们应该让 FCP 保持在 1.8 秒以内,所以我们需要简化代码体积。 现代的打包工具都有摇树功能,使用各种方式来缩小和压缩我们用于生产的代码,比如 webpack。但是在有些情况下它不能很好的去处无用的代码,我们最好知道那些代码应该被打包,而不是仅仅依靠打包工具来尝试修复我们的代码问题。 现在的 JavaScript 已经经历了多次重大更新,拥有了非常多的新功能。在过去我们需要使用 lodash 这类库来实现这些功能,但是现在 lodash 的优势在慢慢减少。
# 3. 关于业务逻辑和组件逻辑分离
在过去,很多人认为 React 组件应该包含逻辑,逻辑是组件的一部分。但是拿到今天来看,这个观点是有问题的。
const Example = () => {
const [data, setData] = useState([])
useEffect(() => {
fetch('...')
.then(res => res.json())
.then(data => {
const filteredData = data.filter(item => item.status === ture)
setData(filteredData)
})
}, [])
return <div>...</div>
}
2
3
4
5
6
7
8
9
10
11
12
将组件和逻辑放到一起会让组件变得复杂,当修改或者增加业务逻辑时,对开发者来说更加复杂,而且想了解整个流程也更加具有挑战性。
const Example = () => {
const { data, error } = useData()
return <div>...</div>
}
2
3
4
将组件和逻辑分离,有两个好处:
- 关注分离点。
- 重用业务逻辑。
# 4. 每次渲染的重复工作
即使你是经验丰富的 React 老手,可能仍然做不到对渲染这件事完全了解。 渲染是经常发生并且很多时候是出乎意料的。 这是使用 React 编写组件的核心原则之一,在编写 React 组件时应该牢记在心。 同时意味着,在渲染组件的时候会重新执行某些逻辑。
# (1).缓存数据
React 提供了 useMemo 和 useCallback 两个 Hook,如果使用得当,这些 Hook 可以缓存计算结果或者函数,来减少不必要的重复渲染,最终提高性能。
import React, { useMemo } from 'react'
const MemoExample = ({ items, filter }) => {
const filteredItems = useMemo(() => {
return items.filter(filter )
}, [filter, items])
return filteredItems.map(item => <p>{item}</p>)
}
2
3
4
5
6
7
8
9
上面的例子是一个项目列表的展示,其中需要通过某些条件来过滤列表,最终展示给用户。这种数据过滤在前端中是不可避免的,所以我们可以使用 useMemo 来缓存过滤数据的过程,这样只有当 items 和 filter 发生变化时它才会重新渲染。
useCallback
可以理解为,就是 callback
加了一个memoize
# (2).尽量避免异步使用hook
react 中多次使用 setState 会进行批量更新,如:
setState({ a:a+1 })
setState({ b:b+1 })
setState({ c:c+1 })
2
3
会被 react 合并为:
setState({
a:a+1 ,
b:b+1 ,
c:c+1
})
2
3
4
5
当进行异步执行 hook 后:
handerClick=()=>{
setTimeout(() => {
setState({ a:a+1 })
setState({ b:b+1 })
setState({ c:c+1 })
}, 0)
}
2
3
4
5
6
7
我们会发现,执行异步操作后,组件都更新渲染了三次 ,此时的批量更新失效了。此外异步操作也会导致一些 hook 的执行顺序变得混乱,这也是为什么 react 官方推荐在 hook 内使用异步而不是使用异步包裹 hook 的原因。
# 5. useEffect 使用不当
useEffect 是 React 中使用率最高的 Hooks 之一。 在 class 组件的时代,componentDidMount 是一个通用的生命周期函数,用来做一些数据请求,事件绑定等。 在 Hooks 时代,useEffect 已经取代了它。但是不正确的使用 useEffect 可能会导致最终创建多个事件绑定。 下面就是一个错误的用法。
import React, { useMemo } from 'react'
const useEffectBadExample = () => {
useEffect(() => {
const clickHandler = e => console.log('e:', e)
document.getElementById('btn').addEventListener('click', clickHandler)
})
return <button id="btn">click me</button>
}
2
3
4
5
6
7
8
9
10
正确的做法是:
useEffect 的回调函数应该返回一个函数,用来解除绑定。
useEffect 应该提供第二个参数,为空数组,保证只会运行一次。
如果第二个参数为对象属性,则表示在该属性更新时执行。
import React, { useMemo } from 'react'
const UseEffectBadExample = () => {
useEffect(() => {
const clickHandler = e => console.log('e:', e)
document.getElementById('btn').addEventListener('click', clickHandler)
return () => document.getElementById('btn').removeEventListener('click', clickHandler)
}, [])
return <button id="btn">click me</button>
}
2
3
4
5
6
7
8
9
10
11
# 6. useState 使用不当
useState 同样是 React 中使用率最高的两个 Hook 之一。 但是令很多人困惑的是,useState 可能并不会按照他的预期去工作。 比如一个图片压缩组件:参考React实战视频讲解:进入学习 (opens new window)
function Compress() {
const [files, setFiles] = useState([])
const handleChange = (newFiles) => {
api(newFiles).then((res)=>{
const cloneFiles = [...files]// 这里的 file 始终是[]
cloneFiles.map(
// 一些逻辑...
)
setFiles(cloneFiles)
})
}
return <input type="upload" multiple onChange={handleChange}/>
}
2
3
4
5
6
7
8
9
10
11
12
13
应该修改为:
function Compress() {
const [files, setFiles] = useState([])
const handleChange = (newFiles) => {
api(newFiles).then((res)=>{
setFiles((oldFiles) => {
const cloneFiles = [...files]// 这里的 file 是最新的 (因为这里闭包了)
return cloneFiles.map(
// 一些逻辑...
)
})
})
}
return <input type="upload" multiple onChange={handleChange}/>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
原因在于函数是基于当前闭包使用的状态。但是状态更新后,会触发渲染,并创建新的上下文,而不会影响之前的闭包。 所以要让程序按照预期执行,必须使用下面的语法:
setFiles(oldFiles => [...oldFiles, ...res.data])
此外数组变更需要重新定义一个数组去替换掉它,否则你会发现视图并没有更新,原因是数组的赋值是引用传递的,react的虚拟dom发现state里面的data没有变化,所以不更新视图,而这时可以使用一个新数组。
错误:
let data = this.state.data; // 因为还是原地址引用
data.push(obj);
this.setState({
data: data
});
2
3
4
5
正确:
let data = [...this.state.data]; // 使用新数组
data.push(obj);
setState({
data: data
});
2
3
4
5
# 7. 布尔运算符的错误使用
大多数情况下我们都会使用布尔值来控制页面上某些元素的渲染,这是非常正常的事情。 除此之外还有几种其他方式来处理这种逻辑,最常用的是 && 运算符,这也完全是 JavaScript 的功能,但有时它会有一些意想不到的后果。
const total = 0
const Component = () => total && `商品总数: ${total}`
2
3
当我们需要展示商品数量时,如果数量为 0,那么只会展示 0,而不是商品总数:0。 原因是 JavaScript 会将 0 判断为false。 所以最好不要依赖 JavaScript 的布尔值真假比较。 正确的方式如下:
const total = 0
const Component = () => {
const hasItem = total > 0
return hasItem && `商品总数: ${total}`
}
2
3
4
5
6
# 8. 到处使用三元表达式进行条件渲染
三元表达式是一个非常简洁的语法,在简短的代码中非常令人满意。所以很多人喜欢在 React 中使用三元表达式来渲染组件。 但是它的问题在于难以扩展,在最简单的三元表达式中没什么问题,可一旦多个三元表达式组合到一起,就形成了难以阅读的超大型组件。
import React, { useMemo } from 'react'
const VIPExample = ({ vipLevel }) => {
return (<div>
会员系统 {vipLevel === 0 ? ( <button>开通 VIP</button>
) : vipLevel === 1 ? ( <p>尊敬的青铜VIP,您的特权有3项:...</p>
) : vipLevel === 2 ? ( <p>...</p>
) : <p>...</p>} </div>)
}
2
3
4
5
6
7
8
9
这种代码没有功能性上的错误,但是在可读性方面做得很差。 解决它的办法有两种。 第一种是使用条件判断代替三元表达式。
import React, { useMemo } from 'react'
const VIPDetail = (vipLevel) => {
if(vipLevel === 0) return <button>开通 VIP</button>
if(vipLevel === 1) return <p>尊敬的青铜VIP,您的特权有3项:...</p>
// ...
}
const VIPExample = ({ vipLevel }) => {
return (<div>
会员系统 {VIPDetail(vipLevel)} </div>)
}
2
3
4
5
6
7
8
9
10
11
12
如果每个分支中的组件比较复杂,我们更进一步,我们使用抽象来封装组件。
import React, { useMemo } from 'react'
const VIPZeroDetail = ({ vipLevel }) => {
if(vipLevel !== 0) return null
return <button>开通 VIP</button>
}
const VIPOneDetail = ({ vipLevel }) => {
if(vipLevel !== 1) return null
return <p>尊敬的青铜VIP,您的特权有3项:...</p>
}
// ...
const VIP = ({ vipLevel }) => {
return <>
<VIPZeroDetail vipLevel={vipLevel} />
<VIPOneDetail vipLevel={vipLevel} />
<!-->...<-->
</>
}const VIPExample = ({ vipLevel }) => { return (<div>
会员系统 <VIP vipLevel={vipLevel} />
</div>)}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
大多数情况下使用条件判断的方式就够用了。使用抽象封装组件的方式有个缺点,就是组件太过于散乱,同步逻辑比较麻烦。
# 9. 不定义 propTypes 或者不解构 props
React 的大多数东西和 JavaScript 几乎是一样的。React 的 props 也只是 JavaScript 中的对象,这也就意味着我们可以在对象中传递许多不同的值,而组件很难知道它们。 这样组件在使用 props 时就变得比较麻烦。 很多人喜欢这么访问 props。
const Example = (props) => {
return <div>
<h1>{props.title}</h1>
<p>{props.content}</p>
</div>
}
2
3
4
5
6
在不使用 TypeScript 或者不定义 propsTypes 的情况下,我们可以随意使用 props.xxx 的方式来访问 props。 为了解决这个问题,我们可以选择使用 TypeScript 为组件的 props 声明类型。 如果你没有使用 TypeScript,那么可以使用 propTypes。 同时建议将 props 以解构的方式使用。
const Example = ({ title, content }) => {
return <div>
<h1>{title}</h1>
<p>{content}</p>
</div>
}
Example.propTypes = {
title: PropTypes.string.isRequired,
content: PropTypes.string.isRequired
}
2
3
4
5
6
7
8
9
10
11
这样组件需要哪些 props,我们一目了然。 而且当我们试图访问 props 上面不存在的属性时,会得到警告。
# 10. 对大型应用的代码拆分
大型的应用意味着包含大量的组件。 这时我们应该使用代码拆分的方式将应用分成多个 js 文件,在用到哪些文件时再去加载它们。这样可以让应用的初始包体积很小,让用户启动网页的速度更快。 react-loadable 是一个专门处理这件事的第三方库,使用它我们可以很好的将组件进行拆分。
import Loadable from 'react-loadable'
import Loading from 'loading'
const LoadableComponent = Loadable({
loader: () => import('./component'),
loading: Loading
})
export default () => <LoadableComponent />
2
3
4
5
6
7
8
9
# 11.受控组件颗粒化
当一个页面有三个展示区域,分别做了三次请求,触发了三次setState
,渲染三次页面。一旦有一个区域重新拉取数据,另外两个区域也会受到牵连,触发重新加载。
class Index extends React.Component{
state :any={
dataA:null,
dataB:null,
dataC:null
}
async componentDidMount(){
/* 获取A区域数据 */
const dataA = await getDataA()
this.setState({ dataA })
/* 获取B区域数据 */
const dataB = await getDataB()
this.setState({ dataB })
/* 获取C区域数据 */
const dataC = await getDataC()
this.setState({ dataC })
}
render(){
const { dataA , dataB , dataC } = this.state
console.log(dataA,dataB,dataC)
return <div>
<div> { /* 用 dataA 数据做展示渲染 */ } </div>
<div> { /* 用 dataB 数据做展示渲染 */ } </div>
<div> { /* 用 dataC 数据做展示渲染 */ } </div>
</div>
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
接下来我们,把每一部分抽取出来,形成独立的渲染单元,每个组件都独立数据请求到独立渲染。
function ComponentA(){
const [ dataA, setDataA ] = useState(null)
useEffect(()=>{
getDataA().then(res=> setDataA(res.data) )
},[])
return <div> { /* 用 dataA 数据做展示渲染 */ } </div>
}
function ComponentB(){
const [ dataB, setDataB ] = useState(null)
useEffect(()=>{
getDataB().then(res=> setDataB(res.data) )
},[])
return <div> { /* 用 dataB 数据做展示渲染 */ } </div>
}
function ComponentC(){
const [ dataC, setDataC ] = useState(null)
useEffect(()=>{
getDataC().then(res=> setDataC(res.data) )
},[])
return <div> { /* 用 dataC 数据做展示渲染 */ } </div>
}
function Index (){
return <div>
<ComponentA />
<ComponentB />
<ComponentC />
</div>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
这样一来,彼此的数据更新都不会相互影响。
# 12.规范写法,合理处置细节问题
# (1).绑定事件尽量不要使用箭头函数
react
更新来大部分情况自于props
的改变(被动渲染),和state
改变(主动渲染)。当我们给未加任何更新限定条件子组件绑定事件的时候,或者是PureComponent
纯组件, 如果我们箭头函数使用的话。
<ChildComponent handerClick={()=>{ console.log(666) }} />
每次渲染时都会创建一个新的事件处理器,这会导致 ChildComponent
每次都会被渲染。
即便我们用箭头函数绑定给dom
元素。
<div onClick={ ()=>{ console.log(777) } } >hello,world</div>
每次react
合成事件事件的时候,也都会重新声明一个新事件。
function index(){
const handerClick1 = useMemo(()=>()=>{
console.log(777)
},[]) /* [] 存在当前 handerClick1 的依赖项*/
const handerClick = useCallback(()=>{ console.log(666) },[]) /* [] 存在当前 handerClick 的依赖项*/
return <div>
<ChildComponent handerClick={ handerClick } />
<div onClick={ handerClick1 } >hello,world</div>
</div>
}
2
3
4
5
6
7
8
9
10
11
对于dom
,如果我们需要传递参数。我们可以这么写。
function index(){
const handerClick1 = useMemo(()=>(event)=>{
const mes = event.currentTarget.dataset.mes
console.log(mes) /* hello,world */
},[])
return <div>
<div data-mes={ 'hello,world' } onClick={ handerClick1 } >hello,world</div>
</div>
}
2
3
4
5
6
7
8
9
# (2).函数式组件使用 useMemo 避免重复声明
函数式组件,数据更新就等于函数上下文的重复执行。那么函数里面的变量,方法就会重新声明。
function Index(){
const data={
data1:'点我有惊喜1',
data2:'点我有惊喜2',
data3:'点我有惊喜3'
}
const [ number , setNumber ] = useState(0)
const handerClick1 = ()=>{
/* 一些操作 */
}
const handerClick2 = ()=>{
/* 一些操作 */
}
const handerClick3 = ()=>{
/* 一些操作 */
}
return <div>
<a onClick={ handerClick1 } >{data.data1}</a>
<a onClick={ handerClick2 } >{data.data2}</a>
<a onClick={ handerClick3 } >{data.data3}</a>
<button onClick={ ()=> setNumber(number+1) } > 点击 { number } </button>
</div>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
每次点击button
的时候,都会执行Index
函数。handerClick1
, handerClick2
,handerClick3
都会重新声明。为了避免这个情况的发生,我们可以用 useMemo
做缓存,我们可以改成如下。
function Index(){
const data=useRef({ // useRef 可以用来缓存
data1:'点我有惊喜1',
data2:'点我有惊喜2',
data3:'点我有惊喜3'
})
const [ number , setNumber ] = useState(0)
const [ handerClick1 , handerClick2 ,handerClick3] = useMemo(()=>{
const fn1 = ()=>{
/* 一些操作 */
}
const fn2 = ()=>{
/* 一些操作 */
}
const fn3= ()=>{
/* 一些操作 */
}
return [fn1 , fn2 ,fn3]
},[]) /* 只有当数据里面的依赖项,发生改变的时候,才会重新声明函数。 */
return <div>
<a onClick={ handerClick1 } >点我有惊喜1</a>
<a onClick={ handerClick2 } >点我有惊喜2</a>
<a onClick={ handerClick3 } >点我有惊喜3</a>
<button onClick={ ()=> setNumber(number+1) } > 点击 { number } </button>
</div>
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26