在 React 中,由于 state 需要看成只读的,不能直接修改,所以在局部更新稍复杂的数据 state 是显得比较麻烦,典型的例子是需要实时更新上传进度的文件列表:
state = {
files: [
{ name: '1.log', progress: 100 },
{ name: '2.log', progress: 70 },
{ name: '3.log', progress: 0 }
]
}
长期以来我的做法是:
function deepCopy (obj) {
// XXX: should be better
return JSON.parse(JSON.stringify(obj))
}
updateProgress = (idx, p) => {
const cp = deepCopy(this.state.files)
cp[idx].progress = p
this.setState({ files: cp })
}
我也曾尝试过 immutable-js,但是引入了与 JS 类似但是有差异的数据类型,而且 toJS
和 fromJS
函数相比 JSON.stringify
和 JSON.parse
的开销不一定小,因此一直没在项目中使用。
前几天看到 immmer,发现这个库完美解决了局部更新 state 这一难题。
API:produce(currentState, producer: (draftState) => void): nextState
官方示例:
import produce from "immer"
const baseState = [
{
todo: "Learn typescript",
done: true
},
{
todo: "Try immer",
done: false
}
]
const nextState = produce(baseState, draftState => {
draftState.push({todo: "Tweet about it"})
draftState[1].done = true
})
// the new item is only added to the next state,
// base state is unmodified
expect(baseState.length).toBe(2)
expect(nextState.length).toBe(3)
// same for the changed 'done' prop
expect(baseState[1].done).toBe(false)
expect(nextState[1].done).toBe(true)
// unchanged data is structurally shared
expect(nextState[0]).toBe(baseState[0])
// changed data not (dûh)
expect(nextState[1]).not.toBe(baseState[1])
因此,改写上面的 updateProgress
方法,显得非常直观:
updateProgress = (idx, p) => {
this.setState(produce(this.state, draft => {
draft.files[idx].progress = p
}))
}
当 produce
的第一个参数是函数时,将返回一个 柯里化 的函数,即原函数的第二个参数被柯里化了。由于 setState
也可以接受函数做参数,因此上面的函数可以直接写成:
updateProgress = (idx, p) => {
this.setState(produce(draft => {
draft.files[idx].progress = p
}))
}