--- title: useReducer Hook description: "Details about the useReducer React hook in ReScript" canonical: "/docs/react/latest/hooks-reducer" --- # useReducer `React.useReducer` helps you express your state in an action / reducer pattern. ## Usage ```res let (state, dispatch) = React.useReducer(reducer, initialState) ``` ```js var match = React.useReducer(reducer, initialState); ``` An alternative to [useState](./hooks-state). 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](/docs/manual/latest/record), [variants](/docs/manual/latest/variant) and [pattern matching](/docs/manual/latest/pattern-matching-destructuring) features for easy expression of your action and state transitions. ## Examples ### Counter Example with `React.useReducer` ```res // 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))} } ``` ```js function reducer(state, action) { if (action) { return { count: state.count - 1 | 0 }; } else { return { count: state.count + 1 | 0 }; } } function Counter(Props) { var match = React.useReducer(reducer, { count: 0 }); var dispatch = match[1]; return React.createElement(React.Fragment, undefined, "Count:" + String(match[0].count), React.createElement("button", { onClick: (function (param) { return Curry._1(dispatch, /* Dec */1); }) }, "-"), React.createElement("button", { onClick: (function (param) { return Curry._1(dispatch, /* Inc */0); }) }, "+")); } ``` > 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: ```res // TodoApp.res type todo = { id: int, content: string, completed: bool, } type action = | AddTodo(string) | RemoveTodo(int) | ToggleTodo(int) type state = { todos: array, 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 =>
  • {React.string(todo.content)} dispatch(ToggleTodo(todo.id))} />
  • ) <>

    {React.string("Todo List:")}

    } ``` ```js function reducer(state, action) { switch (action.TAG | 0) { case /* AddTodo */0 : var todos = state.todos.concat([{ id: state.nextId, content: action._0, completed: false }]); return { todos: todos, nextId: state.nextId + 1 | 0 }; case /* RemoveTodo */1 : var id = action._0; var todos$1 = state.todos.filter(function (todo) { return todo.id !== id; }); return { todos: todos$1, nextId: state.nextId }; case /* ToggleTodo */2 : var id$1 = action._0; var todos$2 = Belt_Array.map(state.todos, (function (todo) { if (todo.id === id$1) { return { id: todo.id, content: todo.content, completed: !todo.completed }; } else { return todo; } })); return { todos: todos$2, nextId: state.nextId }; } } var initialTodos = [{ id: 1, content: "Try ReScript & React", completed: false }]; function TodoApp(Props) { var match = React.useReducer(reducer, { todos: initialTodos, nextId: 2 }); var dispatch = match[1]; var todos = Belt_Array.map(match[0].todos, (function (todo) { return React.createElement("li", undefined, todo.content, React.createElement("button", { onClick: (function (param) { return Curry._1(dispatch, { TAG: /* RemoveTodo */1, _0: todo.id }); }) }, "Remove"), React.createElement("input", { checked: todo.completed, type: "checkbox", onChange: (function (param) { return Curry._1(dispatch, { TAG: /* ToggleTodo */2, _0: todo.id }); }) })); })); return React.createElement(React.Fragment, undefined, React.createElement("h1", undefined, "Todo List:"), React.createElement("ul", undefined, todos)); } ```
    ## Lazy Initialization ```res let (state, dispatch) = React.useReducerWithMapState(reducer, initialState, initial) ``` ```js var match = React.useReducer(reducer, initialState, init); ``` 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: ```res // 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))} } ``` ```js function init(initialCount) { return { count: initialCount }; } function reducer(state, action) { if (typeof action === "number") { if (action !== 0) { return { count: state.count - 1 | 0 }; } else { return { count: state.count + 1 | 0 }; } } else { return { count: action._0 }; } } function Counter(Props) { var initialCount = Props.initialCount; var match = React.useReducer(reducer, initialCount, init); var dispatch = match[1]; return React.createElement(React.Fragment, undefined, "Count:" + String(match[0].count), React.createElement("button", { onClick: (function (param) { return Curry._1(dispatch, /* Dec */1); }) }, "-"), React.createElement("button", { onClick: (function (param) { return Curry._1(dispatch, /* Inc */0); }) }, "+")); } ```