关于如何设计 state 有以下几点原则可参考:
将相关的 state 放到一起
例如
const [x, setX] = useState(0)
const [y, setY] = useState(0)
或者是
const [position, setPosition] = useState({ x: 0, y: 0 })
其实两种方式都是可以的,当一组 state 经常是一起变化的,那更好的方式是第二种,将其组合到一起,使含义更清晰,也提醒开发者不要忘记这是一组需要同时改变的 state。
避免出现矛盾的 state
假设一个场景:一个表单组件中,用户提交后,将 isSending 设置为真,提交按钮 disabled,发起请求,请求返回后,将 isSending 设置为假,并将 isSent 设置为真,卸载表单,改为感谢标语。
export default function Form () {
const [isSending, setIsSending] = useState(false)
const [isSent, setIsSent] = useState(false)
const handleSubmit = async (e) => {
e.preventDefault()
setIsSending(true)
await fetch('/form')
setIsSending(false)
setIsSent(true)
}
return (
{
isSent ?
(
<form onSubmit={handleSubmit}>
<button disabled={isSending} type='submit'></button>
</form>
) : (
<p>感谢提交</p>
)
}
)
}
可以将state这样改进
const [status, setStatus] = useState('typing') // typing, sent, sending
const isSent = status === 'sent'
const isSending = status === 'sending'
前者更加容易藏 bug,因为你可能会忘记 isSending 和 isSent 逻辑上是不可以同时为真的,导致忘记成对设置,所以建议用后者,使用更好维护的方式来构造 state
避免多余的 state
避免像下面例子这样去声明 state,当 state 能用当前其他 state 计算出来时,就不应该“多余”声明
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [fullName, setFullName] = useState('')
const handleFirstNameChange = e => {
setFirstName(e.target.value)
setFullName(e.target.value + ' ' + lastName)
}
const handleLastNameChange = e => {
setLastName(e.target.value)
setFullName(firstName + ' ' + e.target.value)
}
改进
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const fullName = firstName + ' ' + lastName
const handleFirstNameChange = e => {
setFirstName(e.target.value)
}
const handleLastNameChange = e => {
setLastName(e.target.value)
}
避免重复的 state
这段代码声明两个state:一个列表,一个保存当前选中的列表 item。这样做不好的点在于,state selectedItem 这个重复了,即列表 items 里第一项跟 selectedItem 是同一个对象。
const initialItems = [
{ title: 'pretzels', id: 0 },
{ title: 'crispy seaweed', id: 1 },
{ title: 'granola bar', id: 2 },
];
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(
items[0]
);
更好的方式是保存选中的 item 的 id,来实现相同的逻辑,改进如下:
const initialItems = [
{ title: 'pretzels', id: 0 },
{ title: 'crispy seaweed', id: 1 },
{ title: 'granola bar', id: 2 },
];
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(
items[0].id
);
避免嵌套太深的 state
当声明了层级很深的 state,你要 setState 时,不借助 useImmer
这样的库时,set 方法会写的比较麻烦,而且处理层级很深的 state 的理解成本比扁平的 state 更高的。
const [tree, setTree] = useState(
{
id: 1,
title: '1',
children: [
{
id: 2,
title: '2',
children: [
{
id: 3,
title: '3',
children: [
{
id: 4,
title: '4',
children: []
}
]
}
]
}
]
}
)
// 当要改变某个项的时候
const handleTreeChange = (id, props) => {
// 根据 id 递归查找,并更新
const dfs = item => {
if (!item) return null
if (item.id === id) {
return {
...item, ...props
}
}
if (item?.children?.length) {
item.children = item.children.map(dfs)
}
return {...item}
}
setTree(dfs(tree))
}
如同上面例子,要更新层级嵌套比较深的 state,理解成本比较高,也容易出错,所以更好的做法是,将数据扁平化
const [tree, setTree] = useState({
1: {
title: '1',
childrenIds: [2]
},
2: {
title: '2',
childrenIds: [3]
},
3: {
title: '3',
childrenIds: [4]
},
4: {
title: '4',
childrenIds: []
}
})
const handleInfoChange = (id, props) => {
setState(tree=>{
const result = {...tree, [id]: {
...tree[id], ...props
}}
return result
})
}
const handleDelItem = (id) => {
setTree(tree => {
const result = {...tree}
result[id].children = []
delete result[id]
return result
})
}