关于使用React时,如何设计 state 的一些原则

37 阅读3分钟

关于如何设计 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
    })
}

React官方文档:# Choosing the State Structure