不可变对象操作库 immer 介绍

208 阅读1分钟

在 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 类似但是有差异的数据类型,而且 toJSfromJS 函数相比 JSON.stringifyJSON.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
    }))
}