React & Immer 不可变数据结构的处理
Immer 是一个 immutable 库,核心实现是利用 ES6 的 proxy,几乎以最小的成本实现了 js 的不可变数据结构,简单易用、体量小巧、设计巧妙,满足了我们对JS不可变数据结构的需求。
无奈网络上完善的文档实在太少,所以自己写了一份,本篇文章以贴近实战的思路和流程,对 Immer 进行了全面的讲解。
Immer 可以在需要使用不可变数据结构的任何上下文中使用。例如与 React state、React 或 Redux reducers 或者 configuration management 结合使用。不可变的数据结构允许(高效)的变化检测:如果对对象的引用没有改变,那么对象本身也没有改变。此外,它使克隆对象相对便宜:数据树的未更改部分不需要复制,并且在内存中与相同状态的旧版本共享
一般来说,这些好处可以通过确保您永远不会更改对象、数组或映射的任何属性来实现,而是始终创建一个更改后的副本。在实践中,这可能会导致代码编写起来非常麻烦,并且很容易意外违反这些约束。 Immer 将通过解决以下痛点来帮助您遵循不可变数据范式:
- Immer 将检测到意外 mutations 并抛出错误。
- Immer 将不再需要创建对不可变对象进行深度更新时所需的典型样板代码:如果没有 Immer,则需要在每个级别手动制作对象副本。通常通过使用大量
...
展开操作。使用 Immer 时,会对draft
对象进行更改,该对象会记录更改并负责创建必要的副本,而不会影响原始对象。 - 使用 Immer 时,您无需学习专用 API 或数据结构即可从范例中受益。使用 Immer,您将使用纯 JavaScript 数据结构,并使用众所周知的安全地可变 JavaScript API。
useState + Immer
useState
hook 假定存储在其中的任何 state 都被视为不可变的。使用 Immer 可以大大简化 React 组件状态的深度更新。
import { useCallback, useState } from 'react';
import { produce } from 'immer';
const TodoList = () => {
const [todos, setTodos] = useState([
{
id: 'React',
title: 'Learn React',
done: true
},
{
id: 'Immer',
title: 'Try Immer',
done: false
}
]);
const handleToggle = useCallback((id: any) => {
setTodos(
produce((draft) => {
const todo: any = draft.find((todo) => todo.id === id);
todo.done = !todo?.done
})
);
}, []);
const handleAdd = useCallback(() => {
setTodos(
produce((draft) => {
draft.push({
id: 'todo_' + Math.random(),
title: 'a new todo',
done: false
})
})
);
}, [])
return (
<div>
<button onClick={handleAdd}>新增</button>
<div className="list-c">
{todos.map(item => {
return (
<ul key={item.id} className={item.done ? 'done' : ''}>
<li>{item.id}</li>
<li>{item.title}</li>
<li style={{color: 'blue', paddingLeft: '20px', cursor: 'pointer'}} onClick={()=>handleToggle(item.id)}>change done</li>
</ul>
)
})}
</div>
</div>
)
}
export default TodoList;
useImmer
可以省掉 produce
包装的更新模式。
import { useCallback } from 'react';
import { useImmer } from 'use-immer';
const TodoList = () => {
const [todos, setTodos] = useImmer([
{
id: 'React2',
title: 'Learn React 2',
done: true
},
{
id: 'Immer2',
title: 'Try Immer 2',
done: false
}
]);
const handleToggle = useCallback((id: any) => {
setTodos((draft) => {
const todo: any = draft.find((todo) => todo.id === id);
todo.done = !todo.done;
})
}, [])
const handleAdd = useCallback(() => {
setTodos((draft) => {
draft.push({
id: 'todo2_' + Math.random(),
title: 'A new todo 2',
done: false
})
})
}, [])
return (
<div>
<button onClick={handleAdd}>新增</button>
<div className="list-c">
{todos.map(item => {
return (
<ul key={item.id} className={item.done ? 'done' : ''}>
<li>{item.id}</li>
<li>{item.title}</li>
<li style={{color: 'blue', paddingLeft: '20px', cursor: 'pointer'}} onClick={()=>handleToggle(item.id)}>change done</li>
</ul>
)
})}
</div>
</div>
)
}
export default TodoList;
useReducer + Immer
与 useState
类似,需要使用 produce
包装的更新模式,useReducer
也与 Immer 巧妙结合
import { useCallback, useReducer } from 'react';
import { produce } from 'immer';
const TodoList = () => {
const [todos, dispatch] = useReducer(
produce((draft, action) => {
switch (action.type) {
case 'toggle':
const todo: any = draft.find(todo => todo.id === action.id);
todo.done = !todo.done;
break;
case 'add':
draft.push({
id: action.id,
title: 'A new todo 3',
done: false
})
break;
default:
break;
}
}),
[
{
id: 'React3',
title: 'Learn React 3',
done: true
},
{
id: 'Immer3',
title: 'Try Immer3',
done: false
}
]
)
const handleToggle = useCallback((id) => {
dispatch({
type: 'toggle',
id: id
})
}, [])
const handleAdd = useCallback(() => {
dispatch({
type: 'add',
id: 'todo3_' + Math.random()
})
}, [])
return (
<div>
<button onClick={handleAdd}>新增</button>
<div className="list-c">
{todos.map(item => {
return (
<ul key={item.id} className={item.done ? 'done' : ''}>
<li>{item.id}</li>
<li>{item.title}</li>
<li style={{color: 'blue', paddingLeft: '20px', cursor: 'pointer'}} onClick={()=>handleToggle(item.id)}>change done</li>
</ul>
)
})}
</div>
</div>
)
}
export default TodoList;
useImmerReducer
useImmerReducer Reducer方式省去 produce
包装的更新模式。
import { useCallback } from 'react';
import { useImmerReducer } from 'use-immer';
const TodoList = () => {
const [todos, dispatch] = useImmerReducer(
(draft, action: any) => {
switch (action.type) {
case 'toggle':
const todo = draft.find((todo) => todo.id === action.id);
todo.done = !todo.done;
break;
case 'add':
draft.push({
id: action.id,
title: 'A new todo 4',
done: false
})
break;
case default:
break;
}
},
[
{
id: 'React4',
title: 'Learn React 4',
done: true
},
{
id: 'Immer4',
title: 'Try Immer4',
done: false
}
]
);
const handleToggle = useCallback((id) => {
dispatch({
type: 'toggle',
id
})
}, []);
const handleAdd = useCallback(() => {
dispatch({
type: 'add',
id: 'todo4_' + Math.random()
})
}, []);
return (
<div>
<button onClick={handleAdd}>新增</button>
<div className="list-c">
{todos.map(item => {
return (
<ul key={item.id} className={item.done ? 'done' : ''}>
<li>{item.id}</li>
<li>{item.title}</li>
<li style={{color: 'blue', paddingLeft: '20px', cursor: 'pointer'}} onClick={()=>handleToggle(item.id)}>change done</li>
</ul>
)
})}
</div>
</div>
)
}
export default TodoList;