翻译自:https://github.com/mithi/react-philosophies[1] 2.5k star 原文作者:mithi[2] 已获作者授权
《React 开发思想纲领》
是:
React
时的一些思考重构方法论
,SOLID 原则
以及极限编程
等思想的变体,仅仅是在 React
中的实践而已 🙂你可能会觉得我写的这些非常基础。但以下示例都来自一些复杂大型项目的线上代码。
《React 开发思想纲领》
的灵感来源于我实际开发中遇到的各种场景。
ESLint
来静态分析你的代码,开启 rule-of-hooks
和 exhaustive-deps
这两个规则来捕获 React
错误。严格模式
吧,都 2202 年了。直面依赖
,解决在useMemo
,useCallback
和 useEffect
上 exhaustive-deps
规则提示的 warning 或 error 问题。 可以将最新的值挂在 ref 上来保证这些 hook 在回调中拿到的都是最新的值,同时避免不必要的重新渲染。都加上 key
。只在最顶层使用 hook
,不要在循环、条件或嵌套语句中使用 hook。不能对已经卸载的组件执行状态更新
的控制台警告。错误边界(Error Boundary)
来防止白屏,还可以用它来向错误监控平台(比如 Sentry
)上报错误,并设置报警。tree-shaking
!Prettier
来保证代码的格式化一致性!Typescript
和 NextJS
这样的框架来提升开发体验。Code Climate
(或其他类似的)开源库。这类工具会自动检测代码异味(Code Smell,代码中的任何可能导致深层次问题的症状),它可以促使我去处理项目里留下的技术债。译者注:程序员的目标是解决客户的问题,代码只是副产品
依赖加的越多,提供给浏览器的代码就越多。扪心问问自己,你是否真的使用了某个库的 feature?
🙈 你真的需要它吗? 看看这些你可能不需要的依赖
Redux
?有可能需要,但其实 React 本身也是一个状态管理库
。Apollo client
?Apollo client
有许多很强大的功能,比如数据规范化。但使用的同时也会显著提高包体积。如果你的项目使用的并非是 Apollo client
特有的 feature,可以考虑使用一些轻量的库来替代,比如 react-query
或 SWR
(或者根本不用)。Axios
呢?Axios 是一个很棒的库,它的一些特性不容易通过原生的 fetch
API 来复刻。但是如果使用 Axios
只是因为它有更好的 API,完全可以考虑在 fetch
上做一层封装(比如 redaxios
或自己实现)。取决于你的 App 是否真正地使用了 Axios
的核心 feature。Decimal.js
呢?或许 Big.js
或者其他轻量的库就足够了。Lodash
/underscoreJS
呢?推荐你看看【你不需要系列之“你不需要 Lodash/Underscore”】[3]。MomentJS
呢?【你不需要系列之“你不需要 Momentjs”】[4]。浅色
/深色
模式)而使用 Context
,考虑下用 css 变量
代替。Javascript
,CSS 也足够强大。【你不需要系列之“你不需要 JavaScript”】[5]"我们的软件在未来会如何迭代?可能会这样或者那样,如果在当下就开始往这些方向进行代码设计,这就叫 future-proof(防过时,面向未來编程)。"
不要这样搞! 应该在面临需求的时候再去实现相应功能,而不是在你预见到可能需要的时候。代码应该越少越好!
1.3.1 检测代码异味(Code Smell),并在必要时对其进行处理。
当你意识到某个地方出现了问题,那就马上处理掉。但如果当前不容易修复,或者没有时间,那请至少添加一条注释(FIXME
或者 TODO
),附上对该问题的简要描述。来让项目里的每个人都知道这里有问题,让他们意识到当他们遇到这样的情况时也该这样做。
🙈 来看看这些容易发现的代码异味
切记,代码异味并不一定意味着代码需要修改,它只是告诉你,你应该可以想出更好的方式来实现相同的功能。
1.3.2 无情的重构。简单比复杂好。
💁♀️ 小技巧: 简化复杂的条件语句
,最好能提前 return。
🙈 提前 return 的示例
# ❌ 不太好
if (loading) {
return <LoadingScreen />
} else if (error) {
return <ErrorScreen />
} else if (data) {
return <DataScreen />
} else {
throw new Error('This should be impossible')
}
# ✅ 推荐
if (loading) {
return <LoadingScreen />
}
if (error) {
return <ErrorScreen />
}
if (data) {
return <DataScreen />
}
throw new Error('This should be impossible')
💁♀️ 小技巧: 比起传统的循环语句,链式的高阶函数更优雅
如果没有明显的性能差异,尽量使用链式的高阶函数(map
, filter
, find
, findIndex
, some
等) 来代替传统的循环语句。
💁♀️ 小技巧: 可以在 setState
时传入回调函数,所以没必要把 state
作为一个依赖项
你不用把 setState
和 dispatch
放在 useEffect
和 useCallback
这些 hook 的依赖数组中。ESLint 也不会给你提示,因为 React 已经确保了它们不会出错。
# ❌ 不太好
const decrement = useCallback(() => setCount(count - 1), [setCount, count])
const decrement = useCallback(() => setCount(count - 1), [count])
# ✅ 推荐
const decrement = useCallback(() => setCount(count => (count - 1)), [])
💁♀️ 小技巧: 如果你的 useMemo
或 useCallback
没有任何依赖,那你可能用错了
# ❌ 不太好
const MyComponent = () => {
const functionToCall = useCallback(x: string => `Hello ${x}!`,[])
const iAmAConstant = useMemo(() => { return {x: 5, y: 2} }, [])
/* 接下来可能会用到 functionToCall 和 iAmAConstant */
}
# ✅ 推荐
const I_AM_A_CONSTANT = { x: 5, y: 2 }
const functionToCall = (x: string) => `Hello ${x}!`
const MyComponent = () => {
/* 接下来可能会用到 functionToCall 和 I_AM_A_CONSTANT */
}
💁♀️ 小技巧: 巧用 hook 封装自定义的 context,会提升 API 可读性
它不仅看起来更清晰,而且你只需要 import 一次,而不是两次。
❌ 不太好
// 你每次需要 import 两个变量
import { useContext } from 'react';
import { SomethingContext } from 'some-context-package';
function App() {
const something = useContext(SomethingContext); // 看起来 ok,但可以更好
// ...
}
✅ 推荐
// 在另一个文件中,定义这个 hook
function useSomething() {
const context = useContext(SomethingContext);
if (context === undefined) {
throw new Error('useSomething must be used within a SomethingProvider');
}
return context;
}
// 你只需要 import 一次
import { useSomething } from 'some-context-package';
function App() {
const something = useSomething(); // 看起来会更清晰
// ...
}
💁♀️ 小技巧: 在写组件之前,先思考该怎么用它
设计 API 很难,README 驱动开发(RDD)
是个很有用的办法,可以帮助你设计出更好的 API。并不是说应该无脑使用 RDD,但它背后的思想是很值得学习的。我自己发现,在设计实现组件 API 之前,使用 RDD 通常比不用时设计地更好。
太长不看版
Context
不是解决状态共享问题的银弹。useEffect
拆分成独立的小 useEffect
。useCallback
, useMemo
和 useEffect
依赖数组中的依赖项最好都是基本类型。useCallback
, useMemo
和 useEffect
中放入太多的依赖项。useReducer
,而不是使用很多个 useState
。Context
不一定要放在整个 app 的全局。把 Context
放在组件树中尽可能低的位置。同样的道理,你的变量,注释和状态(和普通代码)也应该放在靠近他们被使用的地方。冗余的状态指可以通过其他状态经过推导得到的状态,不需要单独维护(类似 Vue computed),当你有冗余的状态时,一些状态可能会丢失同步性,在面对复杂交互的场景时,你可能会忘记更新它们。
删除这些冗余的状态,除了避免同步错误外,这样的代码也更容易维护和推理,而且代码更少。
为了避免掉入这种坑,最好将基本类型(boolean
, string
, number
等)作为 props 传递。(传递基本类型也能更好的让你使用 React.memo
进行优化)
组件应该仅仅只了解和它运作相关的内容就足够了。应该尽可能地与其他组件产生协作,而不需要知道它们是什么或做什么。
这样做的好处是,组件间的耦合会更松散,依赖程度会更低。低耦合更利于组件修改,替换和移除,而不会影响其他组件。
什么是「单一职责原则」?
一个组件应该有且只有一个职责。应该尽可能的简单且实用,只有完成其职责的责任。
具有各种职责的组件很难被复用。几乎不可能只复用它的部分能力,很容易与其他代码耦合在一起。那些抽离了逻辑的组件,改起来负担不大而且复用性更强。
如何判断一个组件是否符合单一职责?
可以试着用一句话来描述这个组件。如果它只负责一个职责,描述起来会很简单。如果描述中出现了“和“或“或”,那么这个组件很大概率不是单一职责的。
检查组件的 state,props 和 hooks,以及组件内部声明的变量和方法(不应该太多)。问问自己:是否这些内容必须组合到一起这个组件才能工作?如果有些不需要,可以考虑把它们抽离到其他地方,或者把这个大组件拆解成小组件。
Chrome 插件 - React 开发者工具
的 profiler!useMemo
主要用在大开销的计算上。React.memo
, useMemo
, 和 useCallback
来减少重新渲染,它们不该有过多的依赖项,且这些依赖项最好都是基本类型。React.memo
, useCallback
或 useMemo
它们都是为了什么而使用的(是否真的能防止重新渲染?是否能证明在这些场景中真的可以显著提高性能? Memoization 有时会起到反作用,所以需要关注!)Context
应该按逻辑分开,不要在一个 provider 中管理多个 value。如果其中某个值变化了,所有使用该 context 的组件(即便没有用到这个值),都会重新渲染。state
和 dispatch
来优化 context
。lazy loading(懒加载)
和 bundle/code splitting(代码分割)
。tannerlinsley/react-virtual
或其它类似的库。source-map-explorer
或者 @next/bundle-analyzer
(用于 NextJS) 来进行包体积分析。react-hook-forms
,它在性能和开发体验各方面都做的比较好。Jest
,React testing library
,Cypress
,和 Mock service worker
。End
翻译的不好,请大家见谅。如有任何想法,欢迎评论交流
如果本文对你有帮助,点赞 👍 支持下我吧,你的「赞」是我创作的动力。
关于我,目前是字节跳动一线开发,工作四年半,工作中使用 React,业余时间开发喜欢 Vue。
参考资料
[1]
https://github.com/mithi/react-philosophies: https://github.com/mithi/react-philosophies
[2]
mithi: https://github.com/mithi
[3]
【你不需要系列之“你不需要 Lodash/Underscore”】: https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore
[4]
【你不需要系列之“你不需要 Momentjs”】: https://github.com/you-dont-need/You-Dont-Need-Momentjs
[5]
【你不需要系列之“你不需要 JavaScript”】: https://github.com/you-dont-need/You-Dont-Need-JavaScript