Package Exports
- immer
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (immer) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Immer
Create the next immutable state tree by simply modifying the current tree
Immer (German for: always) is a tiny package that allows you to work with immutable state in a more convenient way. It is based on copy-on-write mechanism.
The basic idea is that, you will apply all your changes to a draftState, which is a proxy of the currentState and once all your mutations are completed, immer will produce the nextState based on the mutations to the draft state. This means that you can interact with your data by simply modifying it, while keeping all the benefits of immutable data.
Using immer is like having a personal assistant; he takes a letter (the current state), and gives you a copy (draft) to jot changes onto. Once you are done, the assistant will take your draft and produce the real immutable, final letter for you (the next state).
API
The immer package exposes a single function:
immer(currentState, fn: (draftState) => void): nextState
Example
const baseState = [
{
todo: "Learn typescript",
done: true
},
{
todo: "Try immer",
done: false
}
]
const nextState = immer(baseState, draftState => {
draftState.push({ todo: "Tweet about it" })
draftState[1].done = true
})
The interesting thing about immer
is that, the baseState
will be untouched, but the nextState
will reflect all changes made to draftState
.
// 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])
Benefits
- Use the language© to construct create your next state
- Strongly typed, no string based paths etc
- Deep updates are trivial
- Small, dependency free library with minimal api surface
- No accidental mutations of current state, but intentional mutations of a draft state
Auto freezing
Immer automatically freezes any state trees that are modified using `immer. This protects against accidental modifications of the state tree outside of an immer function. This comes with a performance impact, so it is recommended to disable this option in production. It is by default enabled.
Use setAutoFreeze(true / false)
to turn this feature on or off.
Reducer Example
A lot of words; here is a simple example of the difference that this approach could make in practice. The todo reducers from the official Redux todos-with-undo example
Note, this is just a sample application of the immer
package. Immer is not just designed to simplify Redux reducers. It can be used in any context where you have an immutable data tree that you want to clone and modify (with structural sharing)
const todo = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
id: action.id,
text: action.text,
completed: false
}
case 'TOGGLE_TODO':
if (state.id !== action.id) {
return state
}
return {
...state,
completed: !state.completed
}
default:
return state
}
}
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
todo(undefined, action)
]
case 'TOGGLE_TODO':
return state.map(t =>
todo(t, action)
)
default:
return state
}
}
After using immer, that simply becomes:
import immer from 'immer'
const todos = (state = [], action) =>
// immer produces nextState from draftState and returns it
immer(state, draftState => {
switch (action.type) {
case 'ADD_TODO':
draftState.push({
id: action.id,
text: action.text,
completed: false
})
return
case 'TOGGLE_TODO':
const todo = draftState.find(todo => todo.id === action.id)
todo.completed = !todo.completed
return
}
})
Creating middleware or a reducer wrapper that applies immer
automatically is left as exercise for the reader :-).
Limitations
- This package requires Proxies, so Safari > 10, no Internet Explorer
- Currently, only tree shaped states are supported. Cycles could potentially be supported as well (PR's welcome)
- Currently, only supports plain objects and arrays. Non-plain data structures (like
Map
,Set
) not (yet). (PR's welcome)
Pitfalls:
- Make sure to modify the draft state you get passed in in the callback function, not the original current state that was passed as the first argument to
immer
! - Since immer uses proxies, reading huge amounts of data from state comes with an overhead. If this ever becomes an issue (measure before you optimize!), do the current state analysis before entering the
immer
block or read from thecurrentState
rather than thedraftState