useReducer
React.useReducer
helps you express your state in an action / reducer pattern.
Usage
An alternative to useState. Accepts a reducer of type (state, action) => newState
, and returns the current state
paired with a dispatch
function (action) => unit
.
React.useReducer
is usually preferable to useState
when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. useReducer
also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.
Note: You will notice that the action / reducer pattern works especially well in ReScript due to its immutable records, variants and pattern matching features for easy expression of your action and state transitions.
Examples
Counter Example with React.useReducer
// Counter.res
type action = Inc | Dec
type state = {count: int}
let reducer = (state, action) => {
switch action {
| Inc => {count: state.count + 1}
| Dec => {count: state.count - 1}
}
}
@react.component
let make = () => {
let (state, dispatch) = React.useReducer(reducer, {count: 0})
<>
{React.string("Count:" ++ Belt.Int.toString(state.count))}
<button onClick={(_) => dispatch(Dec)}> {React.string("-")} </button>
<button onClick={(_) => dispatch(Inc)}> {React.string("+")} </button>
</>
}
React guarantees that dispatch function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.
Basic Todo List App with More Complex Actions
You can leverage the full power of variants to express actions with data payloads to parametrize your state transitions:
// TodoApp.res
type todo = {
id: int,
content: string,
completed: bool,
}
type action =
| AddTodo(string)
| RemoveTodo(int)
| ToggleTodo(int)
type state = {
todos: array<todo>,
nextId: int,
}
let reducer = (state, action) =>
switch action {
| AddTodo(content) =>
let todos = Js.Array2.concat(
state.todos,
[{id: state.nextId, content: content, completed: false}],
)
{todos: todos, nextId: state.nextId + 1}
| RemoveTodo(id) =>
let todos = Js.Array2.filter(state.todos, todo => todo.id !== id)
{...state, todos: todos}
| ToggleTodo(id) =>
let todos = Belt.Array.map(state.todos, todo =>
if todo.id === id {
{
...todo,
completed: !todo.completed,
}
} else {
todo
}
)
{...state, todos: todos}
}
let initialTodos = [{id: 1, content: "Try ReScript & React", completed: false}]
@react.component
let make = () => {
let (state, dispatch) = React.useReducer(
reducer,
{todos: initialTodos, nextId: 2},
)
let todos = Belt.Array.map(state.todos, todo =>
<li>
{React.string(todo.content)}
<button onClick={_ => dispatch(RemoveTodo(todo.id))}>
{React.string("Remove")}
</button>
<input
type_="checkbox"
checked=todo.completed
onChange={_ => dispatch(ToggleTodo(todo.id))}
/>
</li>
)
<> <h1> {React.string("Todo List:")} </h1> <ul> {React.array(todos)} </ul> </>
}
Lazy Initialization
You can also create the initialState
lazily. To do this, you can use React.useReducerWithMapState
and pass an init
function as the third argument. The initial state will be set to init(initialState)
.
It lets you extract the logic for calculating the initial state outside the reducer. This is also handy for resetting the state later in response to an action:
// Counter.res
type action = Inc | Dec | Reset(int)
type state = {count: int}
let init = initialCount => {
{count: initialCount}
}
let reducer = (state, action) => {
switch action {
| Inc => {count: state.count + 1}
| Dec => {count: state.count - 1}
| Reset(count) => init(count)
}
}
@react.component
let make = (~initialCount: int) => {
let (state, dispatch) = React.useReducerWithMapState(
reducer,
initialCount,
init,
)
<>
{React.string("Count:" ++ Belt.Int.toString(state.count))}
<button onClick={_ => dispatch(Dec)}> {React.string("-")} </button>
<button onClick={_ => dispatch(Inc)}> {React.string("+")} </button>
</>
}