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 = Array.concat(
state.todos,
[{id: state.nextId, content: content, completed: false}],
)
{todos: todos, nextId: state.nextId + 1}
| RemoveTodo(id) =>
let todos = Array.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>
</>
}