React & Immer
egghead.io lesson 8: Using Immer with _useState_. Or: _useImmer_
useState + Immer
TheuseState hook assumes any state that is stored inside it is treated as immutable. Deep updates in the state of React components can be greatly simplified as by using Immer. The following example shows how to useproduce in combination withuseState, and can be tried onCodeSandbox.
importReact,{ useCallback, useState}from"react";
import{produce}from"immer";
constTodoList=()=>{
const[todos, setTodos]=useState([
{
id:"React",
title:"Learn React",
done:true
},
{
id:"Immer",
title:"Try Immer",
done:false
}
]);
const handleToggle=useCallback((id)=>{
setTodos(
produce((draft)=>{
const todo= 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>{*/ See CodeSandbox */}</div>)
}
useImmer
Since all state updaters follow the same pattern where the update function is wrapped inproduce, it is also possible to simplify the above by leveraging theuse-immer package that will wrap updater functions inproduce automatically:
importReact,{ useCallback}from"react";
import{ useImmer}from"use-immer";
constTodoList=()=>{
const[todos, setTodos]=useImmer([
{
id:"React",
title:"Learn React",
done:true
},
{
id:"Immer",
title:"Try Immer",
done:false
}
]);
const handleToggle=useCallback((id)=>{
setTodos((draft)=>{
const todo= draft.find((todo)=> todo.id=== id);
todo.done=!todo.done;
});
},[]);
const handleAdd=useCallback(()=>{
setTodos((draft)=>{
draft.push({
id:"todo_"+Math.random(),
title:"A new todo",
done:false
});
});
},[]);
// etc
For the full demo seeCodeSandbox.
useReducer + Immer
Similarly touseState,useReducer combines neatly with Immer as well, as demonstrated in thisCodeSandbox:
importReact,{useCallback, useReducer}from"react"
import{produce}from"immer"
constTodoList=()=>{
const[todos, dispatch]=useReducer(
produce((draft, action)=>{
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",
done:false
})
break
default:
break
}
}),
[
/* initial todos */
]
)
const handleToggle=useCallback(id=>{
dispatch({
type:"toggle",
id
})
},[])
const handleAdd=useCallback(()=>{
dispatch({
type:"add",
id:"todo_"+Math.random()
})
},[])
// etc
}
useImmerReducer
...which again, can be slightly shorted byuseImmerReducer from theuse-immer package (demo):
importReact,{ useCallback}from"react";
import{ useImmerReducer}from"use-immer";
constTodoList=()=>{
const[todos, dispatch]=useImmerReducer(
(draft, action)=>{
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",
done:false
});
break;
default:
break;
}
},
[/* initial todos */]
);
//etc
Redux + Immer
Redux + Immer is extensively covered in the documentation ofRedux Toolkit. For Redux without Redux Toolkit, the same trick as applied touseReducer above can be applied: wrap the reducer function withproduce, and you can safely mutate the draft!
For example:
import{produce}from"immer"
// Reducer with initial state
constINITIAL_STATE=[
/* bunch of todos */
]
const todosReducer=produce((draft, action)=>{
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",
done:false
})
break
default:
break
}
})