Skip to content

Latest commit

 

History

History
356 lines (294 loc) · 9.48 KB

hooks-reducer.mdx

File metadata and controls

356 lines (294 loc) · 9.48 KB
title description canonical
useReducer Hook
Details about the useReducer React hook in ReScript
/docs/react/latest/hooks-reducer

useReducer

React.useReducer helps you express your state in an action / reducer pattern.

Usage

<CodeTab labels={["ReScript", "JS Output"]}>

let (state, dispatch) = React.useReducer(reducer, initialState)
var match = React.useReducer(reducer, initialState);

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

<CodeTab labels={["ReScript", "JS Output"]}>

// 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>
  </>
}
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:

<CodeTab labels={["ReScript", "JS Output"]}>

// 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> </>
}
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

<CodeTab labels={["ReScript", "JS Output"]}>

let (state, dispatch) =
  React.useReducerWithMapState(reducer, initialState, initial)
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:

<CodeTab labels={["ReScript", "JS Output"]}>

// 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>
  </>
}
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);
                    })
                }, "+"));
}