返回

【React】836- React 使用中值得优化的 7 个点

发布时间:2022-11-15 17:18:41 392
# javascript# php# java# java# 数据

【React】836- React 使用中值得优化的 7 个点_迭代

作者:SimonMa

前言

原文链接:https://dev.to/awnton/7-code-smells-in-react-components-5f66

作者信息:Anton Gunnarsson

翻译许可:

【React】836- React 使用中值得优化的 7 个点_javascript_02

图片

Agree

正文

自从使用 ​​React​​ 后,我见过越来越多可值得优化的点,比如:

  • 大量的 ​​props​
  • ​props​​​ 的不兼容性
  • ​props​​​ 复制为 ​​state​
  • 返回 ​​JSX​​ 的函数
  • ​state​​​ 的多个状态
  • ​useState​​​ 过多
  • 复杂的 ​​useEffect​

在本文中,我想分享几个技巧,这些技巧将改善你的​​React​​代码。

大量的 props

如果需要把大量的 ​​props​​ 传递到一个组件中,那么很有可能 该组件可再进一步拆分。

问题来了,“大量” 具体是多少呢?答案是 看情况。

假设你正在开发 一个包含 20 个或更多 ​​props​​​ 的组件时,你想再添加一些 ​​props​​ 完善其他功能,这时有两点可以参考 是否应拆分组件:

该组件是否做了多件事?

像函数一样,一个组件应该只做好一件事,所以考虑下 将组件拆分成多个小组件是否会更好。

例如,该组件存在 ​​props​​​ 的不兼容性 或 返回 ​​JSX​​ 的函数。

该组件是否可被合成

开发中,组合是一种很好的模式但经常被忽视。

如果你的组件中存在将不相干逻辑塞到一起的情况,是时候考虑使用组合了。

假设我们有一个表单组件来处理某组织的用户信息:

<ApplicationForm
user={userData}
organization={organizationData}
categories={categoriesData}
locations={locationsData}
onSubmit={handleSubmit}
onCancel={handleCancel}
...
/>

通过该组件的 ​​props​​ ,我们可看到它们都与组件提供的功能密切相关。

该组件看起来并无大碍,但如果将其中的一些 ​​props​​ 分担到子组件,那么数据流就会更清晰。

<ApplicationForm onSubmit={handleSubmit} onCancel={handleCancel}>
<ApplicationUserForm user={userData} />
<ApplicationOrganizationForm organization={organizationData} />
<ApplicationCategoryForm categories={categoriesData} />
<ApplicationLocationsForm locations={locationsData} />
ApplicationForm>

现在,我们已经看到该表单组件只处理提交和取消动作,其他范围内的事情,都交给了对应的子组件。

是否传递了很多有关配置的 props

在某些情况下,将多个有关配置的 ​​props​​​ 组合成一个 ​​options​​ 是个不错的实践。

假设我们有一个可显示某种表格的组件:

<Grid
data={gridData}
pagination={false}
autoSize={true}
enableSort={true}
sortOrder="desc"
disableSelection={true}
infiniteScroll={true}
...
/>

我们可以很清楚地看出,该组件除了 ​​data​​​ 外其余的 ​​props​​ 都是与配置有关的。

如果将多个配置 ​​props​​​ 合成为一个 ​​options​​ ,就可更好地控制组件的选项,规范性也得到提升。

const options = {
pagination: false,
autoSize: true,
enableSort: true,
sortOrder: 'desc',
disableSelection: true,
infiniteScroll: true,
...
}
<Grid
data={gridData}
options={options}
/>

props 的不兼容性

避免组件之间传递不兼容的 ​​props​​。

假设你的组件库中有一个 ​​ 组件,而该组件开始时仅用于处理文本,但过了一段时间后,你将它用于电话号码处理。

你的实现可能如下:

function Input({ value, isPhoneNumberInput, autoCapitalize }) {
if (autoCapitalize) capitalize(value)
return <input value={value} type={isPhoneNumberInput ? 'tel' : 'text'} />
}

问题在于,​​isPhoneNumberInput​​​ 与 ​​autoCapitalize​​ 之间并不存在关联,将一个手机号首字母大写是没有任何意义的。

在这种情况下,我们可以将其分割成多个小组件,来明确具体的职责,如果有共享逻辑,可以将其放到 ​​hooks​​ 中。

function TextInput({ value, autoCapitalize }) {
if (autoCapitalize) capitalize(value)
useSharedInputLogic()
return <input value={value} type="text" />
}
function PhoneNumberInput({ value }) {
useSharedInputLogic()
return <input value={value} type="tel" />
}

虽然上面例子有点勉强,可当发现组件的​​props​​存在不兼容性时,是时候考虑拆分组件了。

props 复制为 state

如何更好地将 ​​props​​​ 作为 ​​state​​ 的初始值。

有如下组件:

function Button({ text }) {
const [buttonText] = useState(text)
return <button>{buttonText}button>
}

该组件将 ​​text​​​ 作为 ​​useState​​ 的初始值,可能会导致意想不到的行为。

实际上该组件已经关掉了 ​​props​​​ 的更新通知,如果 ​​text​​​ 在上层被更新,它将仍呈现 接受到 ​​text​​ 的第一次值,这更容易使组件出错。

一个更实际场景是,我们想基于 ​​props​​​ 通过大量计算来得到新的 ​​state​​。

在下面的例子中,​​slowlyFormatText​​​ 函数用于格式化 ​​text​​,注意 需要很长时间才能完成。

function Button({ text }) {
const [formattedText] = useState(() => slowlyFormatText(text))
return <button>{formattedText}button>
}

解决此问题 最好的方案是 使用 ​​useMemo​​​ 代替 ​​useState​​。

function Button({ text }) {
const formattedText = useMemo(() => slowlyFormatText(text), [text])
return <button>{formattedText}button>
}

现在 ​​slowFormatFormat​​​ 仅在 ​​text​​ 更改时运行,并且没有阻断 上层组件更新。

进一步阅读:Writing resilient components by Dan Abramov。

返回 JSX 的函数

不要从组件内部的函数中返回 ​​JSX​​。

这种模式虽然很少出现,但我还是时不时碰到。

仅举一个例子来说明:

function Component() {
const topSection = () => {
return (
<header>
<h1>Component headerh1>
header>
)
}
const middleSection = () => {
return (
<main>
<p>Some textp>
main>
)
}
const bottomSection = () => {
return (
<footer>
<p>Some footer textp>
footer>
)
}
return (
<div>
{topSection()}
{middleSection()}
{bottomSection()}
div>
)
}

该例子虽然看起来没什么问题,但其实这会破坏代码的整体性,使维护变得困难。

要么把函数返回的 ​​JSX​​ 直接内联到组件内,要么将其拆分成一个组件。

有一点需要注意,如果你创建了一个新组件,不必将其移动到新文件中的。

如果多个组件紧密耦合,将它们保存在同一个文件中是有意义的。

state 的多个状态

避免使用多个布尔值来表示组件状态。

当编写一个组件并多次迭代后,很容易出现这样一种情况,即内部有多个布尔值来表示 该组件处于哪种状态。

比如下面的例子:

function Component() {
const [isLoading, setIsLoading] = useState(false)
const [isFinished, setIsFinished] = useState(false)
const [hasError, setHasError] = useState(false)
const fetchSomething = () => {
setIsLoading(true)
fetch(url)
.then(() => {
setIsLoading(false)
setIsFinished(true)
})
.catch(() => {
setHasError(true)
})
}
if (isLoading) return <Loader />
if (hasError) return <Error />
if (isFinished) return <Success />
return <button onClick={fetchSomething} />
}

当按钮被点击时,我们将 ​​isLoading​​​ 设置为 ​​true​​​,并通过 ​​fetch​​ 执行网络请求。

如果请求成功,我们将 ​​isLoading​​​ 设置为 ​​false​​​,​​isFinished​​​ 设置为 ​​true​​​,如果有错误,将 ​​hasError​​​ 设置为 ​​true​​。

虽然这在技术上是可行的,但很难推断出组件处于什么状态,而且不容易维护。

并且有可能最终处于“不可能的状态”,比如我们不小心同时将 ​​isLoading​​​ 和 ​​isFinished​​​ 设置为 ​​true​​。

解决此问题一劳永逸的方案是 使用枚举来管理状态。

在其他语言中,枚举是一种定义变量的方式,该变量只允许设置为预定义的常量值集合,虽然在​​JavaScript​​ 中不存在枚举,但我们可以使用字符串作为枚举:

function Component() {
const [state, setState] = useState('idle')
const fetchSomething = () => {
setState('loading')
fetch(url)
.then(() => {
setState('finished')
})
.catch(() => {
setState('error')
})
}
if (state === 'loading') return <Loader />
if (state === 'error') return <Error />
if (state === 'finished') return <Success />
return <button onClick={fetchSomething} />
}

通过这种方式,完全杜绝了出现 不可能状态的情况,并更利用扩展。

如果你使用 ​​TypeScript​​ 开发的话,则可以从定义时就实现枚举:

const [state, setState] = useState<'idle' | 'loading' | 'error' | 'finished'>('idle')

useState 过多

避免在同一个组件中使用太多的 ​​useState​​。

一个包含许多 ​​useState​​ 的组件可能会做多件事情,可以考虑是否要拆分它。

当然也存在一些复杂的场景,我们需要在组件中管理一些复杂的状态。

下面是自动输入组件的例子:

function AutocompleteInput() {
const [isOpen, setIsOpen] = useState(false)
const [inputValue, setInputValue] = useState('')
const [items, setItems] = useState([])
const [selectedItem, setSelectedItem] = useState(null)
const [activeIndex, setActiveIndex] = useState(-1)
const reset = () => {
setIsOpen(false)
setInputValue('')
setItems([])
setSelectedItem(null)
setActiveIndex(-1)
}
const selectItem = (item) => {
setIsOpen(false)
setInputValue(item.name)
setSelectedItem(item)
}
...
}

我们有一个 ​​reset​​​ 函数,可以重置所有状态,还有一个 ​​selectItem​​ 函数,可更新一些状态。

这些函数都离不开 ​​useState​​ 定义的状态。如果功能继续迭代,那么函数就会越来越多,状态也会随之增加,数据流就会变得模糊不清。

在这种情况下,使用 ​​useReducer​​​ 来代替 过多的 ​​useState​​ 是一个不错的选择。

const initialState = {
isOpen: false,
inputValue: "",
items: [],
selectedItem: null,
activeIndex: -1
}
function reducer(state, action) {
switch (action.type) {
case "reset":
return {
...initialState
}
case "selectItem":
return {
...state,
isOpen: false,
inputValue: action.payload.name,
selectedItem: action.payload
}
default:
throw Error()
}
}
function AutocompleteInput() {
const [state, dispatch] = useReducer(reducer, initialState)
const reset = () => {
dispatch({ type: 'reset' })
}
const selectItem = (item) => {
dispatch({ type: 'selectItem', payload: item })
}
...
}

通过使用 ​​reducer​​,我们封装了管理状态的逻辑,并将复杂的逻辑移出了组件,这使得组件更容易维护。

进一步阅读:state reducer pattern by Kent C. Dodds。

复杂的 useEffect

避免在 ​​useEffect​​ 中做太多事情,它们使代码易于出错,并且难以推理。

下面的例子中 犯了一个很大的错误:

function Post({ id, unlisted }) {
...
useEffect(() => {
fetch(`/posts/${id}`).then(/* do something */)
setVisibility(unlisted)
}, [id, unlisted])
...
}

当 ​​unlisted​​​ 改变时,即使 ​​id​​​ 没有变,也会调用 ​​fetch​​。

正确的写法应该是 将多个依赖分离:

function Post({ id, unlisted }) {
...
useEffect(() => { // when id changes fetch the post
fetch(`/posts/${id}`).then(/* ... */)
}, [id])
useEffect(() => { // when unlisted changes update visibility
setVisibility(unlisted)
}, [unlisted])
...
}

结束语

以上就是我分享的全部。请记住,这些绝不是规则,而是表明某些东西可能是“错误的”。



【React】836- React 使用中值得优化的 7 个点_javascript_03


特别声明:以上内容(图片及文字)均为互联网收集或者用户上传发布,本站仅提供信息存储服务!如有侵权或有涉及法律问题请联系我们。
举报
评论区(0)
按点赞数排序
用户头像
精选文章
thumb 中国研究员首次曝光美国国安局顶级后门—“方程式组织”
thumb 俄乌线上战争,网络攻击弥漫着数字硝烟
thumb 从网络安全角度了解俄罗斯入侵乌克兰的相关事件时间线