title | description | canonical |
---|---|---|
useReducer Hook |
Details about the useReducer React hook in ReScript |
/docs/react/latest/hooks-reducer |
React.useReducer
helps you express your state in an action / reducer pattern.
<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.
<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.
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 = 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> </>
}
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));
}
<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);
})
}, "+"));
}