Skip to content

Commit 422af5f

Browse files
committed
Add useReducer docs
1 parent 6bc71ab commit 422af5f

File tree

1 file changed

+357
-0
lines changed

1 file changed

+357
-0
lines changed
Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
---
2+
title: useReducer Hook
3+
description: "Details about the useReducer React hook in ReScript"
4+
canonical: "/docs/react/latest/hooks-reducer"
5+
category: "Hooks & State Management"
6+
---
7+
8+
# useReducer
9+
10+
<Intro>
11+
12+
`React.useReducer` helps you express your state in an action / reducer pattern.
13+
14+
</Intro>
15+
16+
## Usage
17+
18+
<CodeTab labels={["ReScript", "JS Output"]}>
19+
20+
```res
21+
let (state, dispatch) = React.useReducer(reducer, initialState)
22+
```
23+
24+
```js
25+
var match = React.useReducer(reducer, initialState);
26+
```
27+
28+
</CodeTab>
29+
30+
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`.
31+
32+
`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.
33+
34+
**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/pattern-matching-destructuring) features for easy expression of your action and state transitions.
35+
36+
## Examples
37+
38+
### Counter Example with `React.useReducer`
39+
40+
<CodeTab labels={["ReScript", "JS Output"]}>
41+
42+
```res
43+
// Counter.res
44+
45+
type action = Inc | Dec
46+
type state = {count: int}
47+
48+
let reducer = (state, action) => {
49+
switch action {
50+
| Inc => {count: state.count + 1}
51+
| Dec => {count: state.count - 1}
52+
}
53+
}
54+
55+
@react.component
56+
let make = () => {
57+
let (state, dispatch) = React.useReducer(reducer, {count: 0})
58+
59+
<>
60+
{React.string("Count:" ++ Belt.Int.toString(state.count))}
61+
<button onClick={(_) => dispatch(Dec)}> {React.string("-")} </button>
62+
<button onClick={(_) => dispatch(Inc)}> {React.string("+")} </button>
63+
</>
64+
}
65+
```
66+
67+
```js
68+
function reducer(state, action) {
69+
if (action) {
70+
return {
71+
count: state.count - 1 | 0
72+
};
73+
} else {
74+
return {
75+
count: state.count + 1 | 0
76+
};
77+
}
78+
}
79+
80+
function Counter(Props) {
81+
var match = React.useReducer(reducer, {
82+
count: 0
83+
});
84+
var dispatch = match[1];
85+
return React.createElement(React.Fragment, undefined, "Count:" + String(match[0].count), React.createElement("button", {
86+
onClick: (function (param) {
87+
return Curry._1(dispatch, /* Dec */1);
88+
})
89+
}, "-"), React.createElement("button", {
90+
onClick: (function (param) {
91+
return Curry._1(dispatch, /* Inc */0);
92+
})
93+
}, "+"));
94+
}
95+
```
96+
97+
</CodeTab>
98+
99+
> 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.
100+
101+
### Basic Todo List App with More Complex Actions
102+
103+
You can leverage the full power of variants to express actions with data payloads to parametrize your state transitions:
104+
105+
<CodeTab labels={["ReScript", "JS Output"]}>
106+
107+
```res
108+
// TodoApp.res
109+
110+
type todo = {
111+
id: int,
112+
content: string,
113+
completed: bool,
114+
}
115+
116+
type action =
117+
| AddTodo(string)
118+
| RemoveTodo(int)
119+
| ToggleTodo(int)
120+
121+
type state = {
122+
todos: array<todo>,
123+
nextId: int,
124+
}
125+
126+
let reducer = (state, action) =>
127+
switch action {
128+
| AddTodo(content) =>
129+
let todos = Js.Array2.concat(
130+
state.todos,
131+
[{id: state.nextId, content: content, completed: false}],
132+
)
133+
{todos: todos, nextId: state.nextId + 1}
134+
| RemoveTodo(id) =>
135+
let todos = Js.Array2.filter(state.todos, todo => todo.id !== id)
136+
{...state, todos: todos}
137+
| ToggleTodo(id) =>
138+
let todos = Belt.Array.map(state.todos, todo =>
139+
if todo.id === id {
140+
{
141+
...todo,
142+
completed: !todo.completed,
143+
}
144+
} else {
145+
todo
146+
}
147+
)
148+
{...state, todos: todos}
149+
}
150+
151+
let initialTodos = [{id: 1, content: "Try ReScript & React", completed: false}]
152+
153+
@react.component
154+
let make = () => {
155+
let (state, dispatch) = React.useReducer(
156+
reducer,
157+
{todos: initialTodos, nextId: 2},
158+
)
159+
160+
let todos = Belt.Array.map(state.todos, todo =>
161+
<li>
162+
{React.string(todo.content)}
163+
<button onClick={_ => dispatch(RemoveTodo(todo.id))}>
164+
{React.string("Remove")}
165+
</button>
166+
<input
167+
type_="checkbox"
168+
checked=todo.completed
169+
onChange={_ => dispatch(ToggleTodo(todo.id))}
170+
/>
171+
</li>
172+
)
173+
174+
<> <h1> {React.string("Todo List:")} </h1> <ul> {React.array(todos)} </ul> </>
175+
}
176+
```
177+
178+
```js
179+
function reducer(state, action) {
180+
switch (action.TAG | 0) {
181+
case /* AddTodo */0 :
182+
var todos = state.todos.concat([{
183+
id: state.nextId,
184+
content: action._0,
185+
completed: false
186+
}]);
187+
return {
188+
todos: todos,
189+
nextId: state.nextId + 1 | 0
190+
};
191+
case /* RemoveTodo */1 :
192+
var id = action._0;
193+
var todos$1 = state.todos.filter(function (todo) {
194+
return todo.id !== id;
195+
});
196+
return {
197+
todos: todos$1,
198+
nextId: state.nextId
199+
};
200+
case /* ToggleTodo */2 :
201+
var id$1 = action._0;
202+
var todos$2 = Belt_Array.map(state.todos, (function (todo) {
203+
if (todo.id === id$1) {
204+
return {
205+
id: todo.id,
206+
content: todo.content,
207+
completed: !todo.completed
208+
};
209+
} else {
210+
return todo;
211+
}
212+
}));
213+
return {
214+
todos: todos$2,
215+
nextId: state.nextId
216+
};
217+
218+
}
219+
}
220+
221+
var initialTodos = [{
222+
id: 1,
223+
content: "Try ReScript & React",
224+
completed: false
225+
}];
226+
227+
function TodoApp(Props) {
228+
var match = React.useReducer(reducer, {
229+
todos: initialTodos,
230+
nextId: 2
231+
});
232+
var dispatch = match[1];
233+
var todos = Belt_Array.map(match[0].todos, (function (todo) {
234+
return React.createElement("li", undefined, todo.content, React.createElement("button", {
235+
onClick: (function (param) {
236+
return Curry._1(dispatch, {
237+
TAG: /* RemoveTodo */1,
238+
_0: todo.id
239+
});
240+
})
241+
}, "Remove"), React.createElement("input", {
242+
checked: todo.completed,
243+
type: "checkbox",
244+
onChange: (function (param) {
245+
return Curry._1(dispatch, {
246+
TAG: /* ToggleTodo */2,
247+
_0: todo.id
248+
});
249+
})
250+
}));
251+
}));
252+
return React.createElement(React.Fragment, undefined, React.createElement("h1", undefined, "Todo List:"), React.createElement("ul", undefined, todos));
253+
}
254+
```
255+
256+
</CodeTab>
257+
258+
259+
## Lazy Initialization
260+
261+
262+
263+
<CodeTab labels={["ReScript", "JS Output"]}>
264+
265+
```res
266+
let (state, dispatch) =
267+
React.useReducerWithMapState(reducer, initialState, initial)
268+
```
269+
270+
```js
271+
var match = React.useReducer(reducer, initialState, init);
272+
```
273+
274+
</CodeTab>
275+
276+
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)`.
277+
278+
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:
279+
280+
<CodeTab labels={["ReScript", "JS Output"]}>
281+
282+
```res
283+
// Counter.res
284+
285+
type action = Inc | Dec | Reset(int)
286+
type state = {count: int}
287+
288+
let init = initialCount => {
289+
{count: initialCount}
290+
}
291+
292+
let reducer = (state, action) => {
293+
switch action {
294+
| Inc => {count: state.count + 1}
295+
| Dec => {count: state.count - 1}
296+
| Reset(count) => init(count)
297+
}
298+
}
299+
300+
@react.component
301+
let make = (~initialCount: int) => {
302+
let (state, dispatch) = React.useReducerWithMapState(
303+
reducer,
304+
initialCount,
305+
init,
306+
)
307+
308+
<>
309+
{React.string("Count:" ++ Belt.Int.toString(state.count))}
310+
<button onClick={_ => dispatch(Dec)}> {React.string("-")} </button>
311+
<button onClick={_ => dispatch(Inc)}> {React.string("+")} </button>
312+
</>
313+
}
314+
```
315+
316+
```js
317+
function init(initialCount) {
318+
return {
319+
count: initialCount
320+
};
321+
}
322+
323+
function reducer(state, action) {
324+
if (typeof action === "number") {
325+
if (action !== 0) {
326+
return {
327+
count: state.count - 1 | 0
328+
};
329+
} else {
330+
return {
331+
count: state.count + 1 | 0
332+
};
333+
}
334+
} else {
335+
return {
336+
count: action._0
337+
};
338+
}
339+
}
340+
341+
function Counter(Props) {
342+
var initialCount = Props.initialCount;
343+
var match = React.useReducer(reducer, initialCount, init);
344+
var dispatch = match[1];
345+
return React.createElement(React.Fragment, undefined, "Count:" + String(match[0].count), React.createElement("button", {
346+
onClick: (function (param) {
347+
return Curry._1(dispatch, /* Dec */1);
348+
})
349+
}, "-"), React.createElement("button", {
350+
onClick: (function (param) {
351+
return Curry._1(dispatch, /* Inc */0);
352+
})
353+
}, "+"));
354+
}
355+
```
356+
357+
</CodeTab>

0 commit comments

Comments
 (0)