目录引言冗余的 state简单示例问题分析解决方案重复的 state简单示例问题分析解决方案使用 useEffect 更新 state简单示例问题分析解决方案使用 useEffect
本文源于翻译 Avoid These Common Pitfalls Of React useState 由公众号KooFE前端团队完成翻译
useState 是我们使用最频繁的 React hook,在代码中随处可见,但是也经常会出现一些错误的用法。
或许你已经经历过这些错误的用法,但是可能还没有意识到这是错误,比如写出了一些冗余的、重复的、矛盾的 state,让你不得不额外使用 useEffect 来处理它们。由于这些错误用法的存在,会让代码的可读性变差,提高了代码的维护成本。
了解这些易犯的错误,可以让我们获得如下收益:
在本文中,将介绍一些关于 useState 的常见错误,以便在今后的工作中避免这些错误。
对于初级开发者来说,定义和使用冗余的 state 是一个比较常见的错误。如果一个 state 依赖了另外一个 state,就是这种典型的错误用法。
下面是一个简单的组件,允许用户编辑自己的姓名,其中第一个输入框是用户的姓氏,后一个输入框是用户的名字,然后将姓名组合在一起渲染在输入框的下面。
代码实现如下:
import { useState } from "react";
function RedundantState() {
const [firstName, setFirstName] = useState(""); // 姓氏
const [lastName, setLastName] = useState(""); // 名字
const [fullName, setFullName] = useState(""); // 姓名
const onChangeFirstName = (event) => {
setFirstName(event.target.value);
setFullName(`${event.target.value} ${lastName}`);
};
const onChangeLastName = (event) => {
setLastName(event.target.value);
setFullName(`${firstName} ${event.target.value}`);
};
return (
<>
<fORM>
<input
value={firstName}
onChange={onChangeFirstName}
placeholder="First Name"
/>
<input
value={lastName}
onChange={onChangeLastName}
placeholder="Last Name"
/>
</form>
<div>Full name: {fullName}</div>
</>
);
}
很明显,这段代码中的 fullName 是冗余的 state
可能你会说,先后依次更新 firstName 和 fullName 会导致额外的渲染周期。
const onChangeFirstName = (event) => {
setFirstName(event.target.value);
setFullName(`${event.target.value} ${lastName}`);
};
但是,React state 的更新是批量更新,所以不会为每个 state 更新做单独的渲染。
因此,在大多数情况下,性能方面的差异不大。问题在于可维护性和引入错误的风险。让我们再次看一下示例代码:
const onChangeFirstName = (event) => {
setFirstName(event.target.value);
setFullName(`${event.target.value} ${lastName}`);
};
const onChangeLastName = (event) => {
setLastName(event.target.value);
setFullName(`${firstName} ${event.target.value}`);
};
每次更新firstName 或 lastName 时,我们都必须要更新 fullName。在更复杂的场景中,这很容易被遗漏。因此,这会导致代码更难重构,引入 bug 的可能性也会增加。
如前所述,在大多数情况下,我们不必担心性能。但是,如果被依赖的 state 是大型的数组或需要大量的计算,则可以使用 useMemo 来做优化处理。
fullName 可以由 firstName 和 lastName 直接拼接而成。
export function RedundantState() {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const fullName = `${firstName} ${lastName}`;
...
return (
<>
<form>
...
</form>
<div>Full name: {fullName}</div>
</>
);
}
在多个 state 中存在重复的数据,也是一个比较常见的错误。通常在做数据的转换、排序或过滤时会遇到这种情况。另一种常见情况是选择展示不同的数据,比如接下来介绍的例子。
这个组件用于显示项目列表,用户可以单击相应的按钮来打开 modal 弹窗。
在下面的代码中就存在这种错误用法。
import { useState } from "react";
// const items = [
// {
// id: "item-1",
// text: "Item 1",
// },
// ...
// ]
function DuplicateState({ items }) {
const [selectedItem, setSelectedItem] = useState();
const onClickItem = (item) => {
setSelectedItem(item);
};
return (
<>
{selectedItem && <Modal item={selectedItem} />}
<ul>
{items.map((row) => (
<li key={row.id}>
{row.text}
<button onClick={() => onClickItem(row)}>Open</button>
</li>
))}
</ul>
</>
);
}
这段代码中的问题是,将 item 原封不动地拷贝到了 state 中。
在上面的代码中,这种重复的数据违反了单一数据源原则。事实上,一旦用户选择了任何一项,我们就会出现两个数据源:selectedItem 状态和 items 数组中的数据。
假如,用户能够在 modal 弹窗中编辑这些数据。可能会是这样的:
这就是问题所在。selectedItem 状态仍将包含旧数据。它将不同步。你可以想象,在更复杂的情况下,这可能会成为一个令人讨厌的 bug。
当然,我们可以写代码来实现 selectedItem 状态同步。但我们不得不使用 useEffect 来监听 items 数组中的变化。
一个更简单的解决方案是只跟踪选定的 id。正如你所看到的,该解决方案 “冗余的 state” 部分中的解决方案非常相似:我们只需从 id 中计算出 selectedItem 变量。
// const items = [
// {
// id: "item-1",
// text: "Item 1",
// },
// ...
// ]
function DuplicateState({ items }) {
const [selectedItemId, setSelectedItemId] = useState();
const selectedItem = items.find(({ id }) => id === selectedItemId);
const onClickItem = (itemId) => {
setSelectedItemId(itemId);
};
return (
<>
{selectedItem && <Modal item={selectedItem} />}
<ul>
{items.map((row) => (
<li key={row.id}>
{row.text}
<button onClick={() => onClickItem(row.id)}>Open</button>
</li>
))}
</ul>
</>
);
}
另一个常见问题是使用 useEffect 来监听变量的变化。
我们继续使用上一节的示例:
在组件中,当 items 发生变化后,使用 useEffect 同步给 selectedItem。
import { useEffect, useState } from "react";
// const items = [
// {
// id: "item-1",
// text: "Item 1",
// },
// ...
// ]
function DuplicateState({ items }) {
const [selectedItem, setSelectedItem] = useState();
useEffect(() => {
if (selectedItem) {
setSelectedItem(items.find(({ id }) => id === selectedItem.id));
}
}, [items]);
const onClickItem = (item) => {
setSelectedItem(item);
};
return (
<>
{selectedItem && <Modal item={selectedItem} />}
<ul>
{items.map((row) => (
<li key={row.id}>
{row.text}
<button onClick={() => onClickItem(row)}>Open</button>
</li>
))}
</ul>
</>
);
}
这段代码能够正常工作,并同步保持 selectedItem 状态。是不是觉得它的实现方式有点 hack?
这种方法存在多个问题:
function DuplicateState({ items }) {
const [selectedItem, setSelectedItem] = useState();
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
setSelectedItem(items.find(({ id }) => id === selectedItem.id));
}, [items]);
...
如果你想使用 useEffect 或在另一个开发人员的代码中看到它,问问自己是否真的需要它。也许可以通过前面介绍的方法来避免这种情况。
您可能已经猜到了:上一节的解决方案也帮助我们删除 useEffect。如果我们只存储所选项目的 ID 而不是整个对象,那么就没有什么可同步的。
import { useState } from "react";
// const items = [
// {
// id: "item-1",
// text: "Item 1",
// },
// ...
// ]
function DuplicateState({ items }) {
const [selectedItemId, setSelectedItemId] = useState();
const selectedItem = items.find(({ id }) => id === selectedItemId);
const onClickItem = (id) => {
setSelectedItem(id);
};
return (
<>
{selectedItem && <Modal item={selectedItem} />}
<ul>
{items.map((row) => (
<li key={row.id}>
{row.text}
<button onClick={() => onClickItem(row.id)}>Open</button>
</li>
))}
</ul>
</>
);
}
与上一节相关的另外一个常见问题是使用 useEffect 对状态的变化做出反应。但解决方案略有不同。
这是一个显示产品的组件。用户可以通过单击按钮显示或隐藏产品详细信息。无论何时显示或隐藏产品信息,我们都会触发一个动作(在本例中,会触发一个埋点数据上报)。
import { useEffect, useState } from "react";
function ProductView({ name, details }) {
const [isDetailsVisible, setIsDetailsVisible] = useState(false);
useEffect(() => {
trackEvent({ event: "Toggle Product Details", value: isDetailsVisible });
}, [isDetailsVisible]);
const toggleDetails = () => {
setIsDetailsVisible(!isDetailsVisible);
};
return (
<div>
{name}
<button onClick={toggleDetails}>Show details</button>
{isDetailsVisible && <ProductDetails {...details} />}
</div>
);
}
代码中的 useEffect 会侦听 isDetailsVisible 是否变化,并相应地触发埋点事件。
代码中的问题如下:
在许多情况下,可以删除用于监听 state 变化的 useEffect。通常,我们可以将这些功能放在更新 state 的代码旁边。在这里,我们可以将 trackEvent(...) 移动到 toggleDetails 函数中。
function ProductView({ name, details }) {
const [isDetailsVisible, setIsDetailsVisible] = useState(false);
const toggleDetails = () => {
setIsDetailsVisible(!isDetailsVisible);
trackEvent({ event: "Toggle Product Details", value: !isDetailsVisible });
};
return (
<div>
{name}
<button onClick={toggleDetails}>Show details</button>
{isDetailsVisible && <ProductDetails {...details} />}
</div>
);
}
当您使用相互依赖的多个 state 时,这些状态可能存在多种组合,稍有不慎就会设置出错误的 state,让这些 state 呈现出相互矛盾的渲染结果。因此,我们需要更直观的方式来组织和管理这些状态组合。
下面是一个很基本的数据请求的示例,组件可以处于不同的状态:要么正在加载数据,要么发生错误,要么已成功获取数据。
export function ContradictingState() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setIsLoading(true);
setError(null);
fetchData()
.then((data) => {
setData(data);
setIsLoading(false);
})
.catch((error) => {
setIsLoading(false);
setData(null);
setError(error);
});
}, []);
...
这种方法的问题是,如果我们不小心,我们可能会产生有矛盾的 state。例如,在上面的示例中,当发生错误时,我们可能忘记将 isLoading 设置为 false。
对于哪些 state 是允许组合的,也是很难理解的。在上面的例子中,理论上我们可以有 8 种不同的 state 组合。但你不能很直观的看到哪些状态组合是真正存在的。
多个状态之间相互依赖,更推荐用 useReducer 来替代 useState。
const initialState = {
data: [],
error: null,
isLoading: false
};
function reducer(state, action) {
switch (action.type) {
case "FETCH":
return {
...state,
error: null,
isLoading: true
};
case "SUCCESS":
return {
...state,
error: null,
isLoading: false,
data: action.data
};
case "ERROR":
return {
...state,
isLoading: false,
error: action.error
};
default:
throw new Error(`action "${action.type}" not implemented`);
}
}
export function NonContradictingState() {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
dispatch({ type: "FETCH" });
fetchData()
.then((data) => {
dispatch({ type: "SUCCESS", data });
})
.catch((error) => {
dispatch({ type: "ERROR", error });
});
}, []);
...
这样一来,就可以大大减少了我们的理解成本。我们可以很直观地看到我们有 3 个动作和 4 个可能的组件状态(“FETCH”、“SUCCESS”、“ERROR”和初始状态)。
我们这里提到的最后一个常见问题是(深度)嵌套对象的 state。如果我们只是渲染数据,这可能不存在什么问题。但是,一旦开始更新嵌套数据项,就会遇到一些麻烦。
这里我们有一个组件,用于渲染深度嵌套的注释。jsX 在这里并不重要,所以省略了,我们假设 updateComment 是绑定到按钮上的回调函数。
function NestedComments() {
const [comments, setComments] = useState([
{
id: "1",
text: "Comment 1",
children: [
{
id: "11",
text: "Comment 1 1"
},
{
id: "12",
text: "Comment 1 2"
}
]
},
{
id: "2",
text: "Comment 2"
},
{
id: "3",
text: "Comment 3",
children: [
{
id: "31",
text: "Comment 3 1",
children: [
{
id: "311",
text: "Comment 3 1 1"
}
]
}
]
}
]);
const updateComment = (id, text) => {
// this gets complicated
};
...
这种嵌套 state 的问题是,我们必须以不可变的方式更新它,否则组件不会重新渲染。上面示例中的深度嵌套注释,我们以硬编码的方式来实现:
const updateComment = (id, text) => {
setComments([
...comments.slice(0, 2),
{
...comments[2],
children: [
{
...comments[2].children[0],
children: [
{
...comments[2].children[0].children[0],
text: "New comment 311"
}
]
}
]
}
]);
};
这种实现方式非常复杂。
与深度嵌套的 state 不同,使用扁平的数据结构要容易得多。我们可以为每一个数据项增加 ID 字段,通过 ID 之间相互引用来描述嵌套关系。代码看起来像这样:
function FlatCommentsRoot() {
const [comments, setComments] = useState([
{
id: "1",
text: "Comment 1",
children: ["11", "12"],
},
{
id: "11",
text: "Comment 1 1"
},
{
id: "12",
text: "Comment 1 2"
},
{
id: "2",
text: "Comment 2",
},
{
id: "3",
text: "Comment 3",
children: ["31"],
},
{
id: "31",
text: "Comment 3 1",
children: ["311"]
},
{
id: "311",
text: "Comment 3 1 1"
}
]);
const updateComment = (id, text) => {
const updatedComments = comments.map((comment) => {
if (comment.id !== id) {
return comment;
}
return {
...comment,
text
};
});
setComments(updatedComments);
};
...
现在,通过它的 ID 找到正确的数据项,并在数组中替换它就容易多了。
以上就是React useState的错误用法避坑详解的详细内容,更多关于React useState错误避坑的资料请关注编程网其它相关文章!
--结束END--
本文标题: ReactuseState的错误用法避坑详解
本文链接: https://lsjlt.com/news/177498.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-01-12
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0