你必须避免的 10 个 React 错误用法

简介: 你必须避免的 10 个 React 错误用法

本文是作者在实际工作经验中总结提炼出的错误使用 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>
}

解决这个问题的方法有很多,比如 React Context Hook,或者类似 Redux 的库。

但是使用 Redux 需要额外编写一些代码,它更适合单个状态改变很多东西的复杂场景。简单的项目选择使用 Context Hook 是更好的选择。


2. 导入代码超出实际所用的代码


React 是一个前端框架,它有着不小的代码体积。

我们在编写 React 程序时,应该避免导入很多用不到的模块。因为它们也会被打包到运行时代码发送到用户的客户端/浏览器/移动设备上。额外的依赖会导致应用的体积膨胀,增加用户的加载时间,让网页变慢,降低用户体验度。


import _ from 'lodash'  // 整个包导入
import _map from 'lodash/map' // 只导入需要的包

为了保证良好的用户体验度,我们应该让 FCP 保持在 1.8 秒以内,所以我们需要简化代码体积。

现代的打包工具都有摇树功能,使用各种方式来缩小和压缩我们用于生产的代码,比如 webpack。但是在有些情况下它不能很好的去处无用的代码,我们最好知道那些代码应该被打包,而不是仅仅依靠打包工具来尝试修复我们的代码问题。

现在的 JavaScript 已经经历了多次重大更新,拥有了非常多的新功能。在过去我们需要使用 lodash 这类库来实现这些功能,但是现在 lodash 的优势在慢慢减少。

youmightnotneed.com/lodash/ 上面可以查看如何使用现代 JavaScript 取代 lodash。

当然这取决于你的用户是使用什么版本的浏览器和 JavaScript。但是我们大都会用 babel 或者类似的转译器来处理这个问题。而且现在几乎每个人都在用 Chrome 了,对吧?

其他库也是同样的道理。


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>
}

将组件和逻辑放到一起会让组件变得复杂,当修改或者增加业务逻辑时,对开发者来说更加复杂,而且想了解整个流程也更加具有挑战性。


const Example = () => {
  const { data, error } = useData()
  return <div>...</div>
}

将组件和逻辑分离,有两个好处:

  1. 关注分离点。
  2. 重用业务逻辑。


4. 每次渲染的重复工作


即使你是经验丰富的 React 老手,可能仍然做不到对渲染这件事完全了解。

渲染是经常发生并且很多时候是出乎意料的。

这是使用 React 编写组件的核心原则之一,在编写 React 组件时应该牢记在心。

同时意味着,在渲染组件的时候会重新执行某些逻辑。

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>)
}

上面的例子是一个项目列表的展示,其中需要通过某些条件来过滤列表,最终展示给用户。这种数据过滤在前端中是不可避免的,所以我们可以使用 useMemo 来缓存过滤数据的过程,这样只有当 items 和 filter 发生变化时它才会重新渲染。


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>
}

正确的做法是:

  • 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>
}

6. useState 使用不当


useState 同样是 React 中使用率最高的两个 Hook 之一。

但是令很多人困惑的是,useState 可能并不会按照他的预期去工作。

比如一个图片压缩组件:


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}/>
}

应该修改为:


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}/>
}

原因在于函数是基于当前闭包使用的状态。但是状态更新后,会触发渲染,并创建新的上下文,而不会影响之前的闭包。

所以要让程序按照预期执行,必须使用下面的语法:


setFiles(oldFiles => [...oldFiles, ...res.data])

7. 布尔运算符的错误使用


大多数情况下我们都会使用布尔值来控制页面上某些元素的渲染,这是非常正常的事情。

除此之外还有几种其他方式来处理这种逻辑,最常用的是 && 运算符,这也完全是 JavaScript 的功能,但有时它会有一些意想不到的后果。


const total = 0
const Component = () => total && `商品总数: ${total}`

当我们需要展示商品数量时,如果数量为 0,那么只会展示 0,而不是商品总数:0。

原因是 JavaScript 会将 0

所以最好不要依赖 JavaScript 的布尔值真假比较。

正确的方式如下:


const total = 0
const Component = () => {
  const hasItem = total > 0
  return hasItem && `商品总数: ${total}`
}

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>)
}

这种代码没有功能性上的错误,但是在可读性方面做得很差。

解决它的办法有两种。

第一种是使用条件判断代替三元表达式。


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>)
}

如果每个分支中的组件比较复杂,我们更进一步,我们使用抽象来封装组件。


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>)
}

大多数情况下使用条件判断的方式就够用了。使用抽象封装组件的方式有个缺点,就是组件太过于散乱,同步逻辑比较麻烦。


9. 不定义 propTypes 或者不解构 props


React 的大多数东西和 JavaScript 几乎是一样的。React 的 props 也只是 JavaScript 中的对象,这也就意味着我们可以在对象中传递许多不同的值,而组件很难知道它们。

这样组件在使用 props 时就变得比较麻烦。

很多人喜欢这么访问 props。


const Example = (props) => {
  return <div>
    <h1>{props.title}</h1>
    <p>{props.content}</p>
  </div>
}

在不使用 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
}

这样组件需要哪些 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 />

总结


React 为我们提供了一个强大的开发生态及开发工具集,我们可以比过去更加轻易地创建 Web 应用。不过,它是一套工具,是工具就可能会被滥用。

只有按照预期去使用工具,并且以优先使用 JavaScript 的方式,才能使我们创建出逻辑更清晰、功能更强大、性能更卓越的代码。

作为开发者,持续改进我们的代码,让用户用起来舒服,让其他开发者读起来舒服,是我们应该努力的方向和目标。

我的这 10 条建议,可以作为你用好 React 的一个起点,希望能够帮你规避很多开发过程中容易出现的错误。



相关文章
|
10月前
react-withRouter 用法
react-withRouter 用法
116 0
|
1月前
|
缓存 JavaScript 前端开发
react.js高级用法
【8月更文挑战第27天】react.js高级用法
30 2
|
28天前
|
Web App开发 监控 前端开发
React 性能监测工具大揭秘!Chrome DevTools 高级用法来袭,让你的 React 应用性能飙升!
【8月更文挑战第31天】在前端开发中,React 框架虽简化了高效、交互性强的用户界面构建,但应用复杂性增加亦可能引发性能问题。此时,Chrome DevTools 凭其性能面板成为了优化应用性能的重要工具,能帮助开发者记录与分析加载时间、渲染及脚本执行等性能指标,定位并解决性能瓶颈。同时,其 React 开发者扩展工具允许实时监控组件状态变化,进一步提升性能。结合运用这些功能,将有助于打造流畅的用户体验。
53 0
|
4月前
|
前端开发 UED
React 防抖与节流用法
React 防抖与节流用法
137 0
|
4月前
|
前端开发 JavaScript
react中refs的作用是什么?有几种用法?
react中refs的作用是什么?有几种用法?
|
4月前
|
存储 前端开发 JavaScript
[React] useRef用法和特性
[React] useRef用法和特性
|
前端开发 JavaScript
React Hooks 用法详解3
React Hooks 用法详解
|
前端开发 JavaScript API
React Hooks 用法详解2
React Hooks 用法详解
|
缓存 前端开发 JavaScript
React Hooks 用法详解1
React Hooks 用法详解
|
存储 缓存 前端开发
React.memo() 和 useMemo() 的用法是什么,有哪些区别?
React.memo() 和 useMemo() 的用法是什么,有哪些区别?
164 0