前言
React是一个非常灵活的前端框架,因为它不会强制开发者使用哪个自带的API或者第三方库来完成某个功能。例如React不会强制你一定要使用Class Component或者Functional Component来开发某个组件,这完全是开发者根据自身的场景自己决定的。而对于第三方库的使用来说,React更加没有要求,例如对于状态管理,React生态就有一堆非常受欢迎的库可以使用,例如Redux,Mobx,XState和Jotai等等。正是因为React的灵活性,React官网只说自己是一个UI library而不是UI Framework。
很强的灵活性有好处也有坏处。好处是社区生态繁荣,开发者可以根据自己特殊的需求来开发满足某个特殊场景的库来配合React使用。而坏处也很明显,那就是实现一样的功能选择太多的话,对于初级开发者来说很不友好。因为他们不知道自己需要使用哪个原生的API或者第三方库来完成某个功能的开发。如果你选错的话,可能会导致开发出的应用有性能问题,或者降低开发效率。其实不止第三方库的使用,即使是使用原生的React API,不同的程序员写出的组件也是千差万别,代码质量也是高低不齐。基于这些原因,我打算写一系列关于React最佳实践的文章,来介绍一些React开发中经常遇到的问题,以及如何使用最佳实践来解决这些问题。了解这些最佳实践不但可以帮你在开发的时候做更好的技术决定,还可以让你在前端面试的时候如虎添翼。
本篇文章是这个系列文章的第一篇,会为大家介绍5个最佳实践。
避免大组件,要学会组件拆分
由于React对组件的大小是没有限制的,所以我们在日常开发中可能会写出一些比较大的组件,例如下面这个简化的例子:
上面的App组件在我看来是一个需要拆分的大组件,因为它包含了Nav和Content这两个子组件的全部内容,而这会导致我们的组件难以测试和维护。对于大组件我们的最佳实践就是按职责拆分,父组件只关心子组件的布局问题。基于这个思路,上面的大组件可以被拆分为Nav和Content两个子组件和一个App大组件,大组件为每一个子组件创建一个wrapper div来定义它们的布局。换句话来说就是:大组件不需要关心子组件的内容,它只需要定义子组件在其内部是如何布局的,并且传递必要的上下文信息即可。这样我们的代码就变为:
这样写的好处是有很多,首先大组件和子组件可以分别进行测试和开发,并且子组件内部的变化和升级不会影响到大组件的代码。
如果我们的组件实在太大,重构时大量复制粘贴代码很麻烦的话,可以使用VSCode的glean插件来自动化这个操作。Glean可以快速帮我们提取某段JSX代码到一个单独的组件。下图展示了如何利用glean来重构App组件:
上图中点击完Extract Component to File并填上文件名后该内容就被抽取到一个独立的组件了。
避免出现嵌套组件
我在日常工作中会看到一些程序员在一个组件里面定义另外一个组件:
上面的代码写起来的确十分方便,因为Child组件是定义在Parent组件里面的,所以它可以直接使用Parent组件里面定义的handleClick函数而不用定义和传递props。不过这种写法却有可能隐藏着严重的性能问题:Child组件会在每次Parent组件重新渲染的时候被重定义,这也就意味着旧的Child组件定义会被销毁,新的组件会被创建。对于嵌套组件我们的最佳实践是:一般不定义嵌套组件,所有的组件都要提出父组件单独定义。基于这个最佳实践,上面的代码可以被改成:
重构完后,Child组件被单独拎出来定义,通过onClick的props接受来自父组件的handleClick函数,这不但可以避免组件被重新定义的性能问题,还可以让我们组件的职责更加分明和便于测试。不过这里把Child组件和Parent组件的定义都放在同一个文件其实也是一个反模式的做法,我们马上就会说到。
每个文件只定义一个组件
在上面的例子中Child和Parent组件都被定义在了同一个文件中,这种做法在Child组件只需要被Parent组件使用的时候是没有问题的。可是如果Child组件也需要被export出来被其它组件使用的话,代码就会开始变得混乱了,例如可能会有下面这种import语句出现:
因此我们这里更好的做法应该是:一个文件里面只定义一个组件。在上面的例子中就需要将Parent和Child两个组件拆分成两个文件:Parent.jsx和Child.jsx。这个最佳实践是可以用eslint-plugin-react里面的no-multi-comp规则来自动约束的。
其实在我的团队里面,我不但要求不同的组件定义到不同的文件里面,我还要求每一个组件都要有一个单独的文件夹,组件的定义放在它对应文件夹的index.jsx文件里面。我这样要求组员的理由是:一个React组件往往有很多配套的文件,例如单元测试,CSS Module定义或者StoryBook的Story等等。这些文件都是和组件强相关的,我们把它们和组件文件放在一起的话有利于后面我们对代码的重构。举个例子下面是我们项目代码里面Navbar组件的目录结构:
试想一下哪天我们项目不需要这个组件了,我们只需删除Navbar这个文件夹即可,而无需担心还有和这个组件关联的逻辑散落在项目的其它地方。
使用useMemo来避免组件里面的重复计算
假如我们的组件里面需要进行一些比较耗费CPU的计算:
上面这个组件调用了expensiveCalculation函数,这个函数会在组件每次渲染的时候被调用。这里面有一个问题就是无论count字段是否改变,这个大计算量的函数都会被调用,这其实会造成一定程度的性能消耗。对于这个场景我们的优化思路是使用useMemo来避免expensiveCalculation函数的重复调用,具体做法是:
useMemo这个hook可以保证只有在count这个状态变化的时候expensiveCalculation才会被调用。如果你对useMemo不够熟悉的话可以参考一下我写的超详细React Hook实践指南。
避免使用无用的div
我们知道React是不允许我们在组件的render里面返回一个dom数组的。因为这个原因很多开发者在写组件的时候都喜欢在最外层包一个毫无意义的div:
上面的写法会导致最终渲染在页面的无用div越来越多,这不但会影响到我们页面的accessibility,还可能会让无用的div干扰我们对页面的debug。这里我们的最佳实践是使用React.Fragment来替代这个无用的div:
和空div不同,React的Fragment元素不会生成一个新的div,也就避免了上面说到的这些问题。每次都写React.Fragment其实不够方便,因此React为你提供了更加方便的写法:
总结
上面为大家总结了5个我们在日常开发React应用时可以使用到的最佳实践,后面我会不断更新这个系列的内容,为大家带来更多的React最佳实践。
个人技术动态
创作不易,如果你从这篇文章中学到东西,请给我点一下赞或者关注,你的支持是我继续创作的最大动力!
同时欢迎关注公众号进击的大葱一起学习成长
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。