|
1 | | -# rescript-react-update |
| 1 | +# rescript-react-restate |
2 | 2 |
|
3 | | -> useReducer with updates and side effects! |
| 3 | +This library is a fork and re-design of [rescript-react-update](https://github.com/bloodyowl/rescript-react-update). |
| 4 | + |
| 5 | +Essentially what introduce is a fix on the effect cancellation mechanism. A fix in terms of following |
| 6 | +React's philosophy about how we should ensure effects cancellation before re-render. |
| 7 | + |
| 8 | +As a consequence, of the fix, the library also introduce an elegant approach in the separation between pure state management and side effects. By introducing the concept of `deferred actions` and `schedulers`. |
| 9 | + |
| 10 | +A `deferred action` is an `action` that is not immediately dispatched, but rather scheduled to be dispatched later. |
| 11 | +In contrast with `reducers` (that given an action and the state, provides the new state), a `scheduler` is a function that given the current context and a deferred action, can execute (user defined) side effects, and return (or not) a cleanup/cancellation function associated with. |
4 | 12 |
|
5 | 13 | ## Installation |
6 | 14 |
|
7 | 15 | ```console |
8 | | -$ yarn add rescript-react-update |
| 16 | +$ yarn add rescript-react-restate |
9 | 17 | ``` |
10 | 18 |
|
11 | 19 | or |
12 | 20 |
|
13 | 21 | ```console |
14 | | -$ npm install --save rescript-react-update |
| 22 | +$ npm install --save rescript-react-restate |
15 | 23 | ``` |
16 | 24 |
|
17 | | -Then add `rescript-react-update` to your `bsconfig.json` `bs-dependencies` field. |
| 25 | +Then add `rescript-react-restate` to your `bsconfig.json` `bs-dependencies` field. |
18 | 26 |
|
19 | | -## ReactUpdate.useReducer |
| 27 | +## Handling side effects (Asynchronous actions, logging, etc.) |
20 | 28 |
|
21 | | -```reason |
22 | | -type state = int; |
23 | | -
|
24 | | -type action = |
25 | | - | Increment |
26 | | - | Decrement; |
27 | | -
|
28 | | -[@react.component] |
29 | | -let make = () => { |
30 | | - let (state, send) = |
31 | | - ReactUpdate.useReducer((state, action) => |
32 | | - switch (action) { |
33 | | - | Increment => Update(state + 1) |
34 | | - | Decrement => Update(state - 1) |
35 | | - }, |
36 | | - 0 |
37 | | - ); |
38 | | - <div> |
39 | | - {state->React.int} |
40 | | - <button onClick={_ => send(Decrement)}> {"-"->React.string} </button> |
41 | | - <button onClick={_ => send(Increment)}> {"+"->React.string} </button> |
42 | | - </div>; |
43 | | -}; |
44 | | -``` |
| 29 | +`Restate` powers up reducers by allow them not just update state, but also defer an action to be dispatched later if is desired. This is useful when you want to handle side effects (like logging, network requests, etc.) after the state has been updated. |
45 | 30 |
|
46 | | -### Lazy initialisation |
| 31 | +```reason |
47 | 32 |
|
48 | | -## ReactUpdate.useReducerWithMapState |
| 33 | +// Your state |
49 | 34 |
|
50 | | -If you'd rather initialize state lazily (if there's some computation you don't want executed at every render for instance), use `useReducerWithMapState` where the first argument is a function taking `unit` and returning the initial state. |
| 35 | +type state = int |
51 | 36 |
|
52 | | -```reason |
53 | | -type state = int; |
| 37 | +// Your actions (both immediate and deferred ones) |
54 | 38 |
|
55 | 39 | type action = |
56 | 40 | | Increment |
57 | | - | Decrement; |
58 | | -
|
59 | | -[@react.component] |
| 41 | + | Decrement |
| 42 | +
|
| 43 | +type deferredAction = |
| 44 | + | LogIncrement |
| 45 | + | LogDecrement |
| 46 | +
|
| 47 | +// Because of implementation details related on how we clean up the side effects, we need to provide a way to identify each the deferred action. In other words, deferred actions must have a unique identifier. |
| 48 | +module DeferredAction: Restate.HasDeferredAction with type t = deferredAction = { |
| 49 | + type t = deferredAction |
| 50 | + let variantId = action => |
| 51 | + switch action { |
| 52 | + | LogIncrement => "LogIncrement" |
| 53 | + | LogDecrement => "LogDecrement" |
| 54 | + } |
| 55 | +} |
| 56 | +
|
| 57 | +// Instantiate the reducer with your deferred action type |
| 58 | +module RestateReducer = Restate.MakeReducer(DeferredAction) |
| 59 | +
|
| 60 | +// A Reducer now can update the state and schedule deferred actions (if they need to) |
| 61 | +let reducer = (state, action) => |
| 62 | + switch action { |
| 63 | + | Increment => |
| 64 | + RestateReducer.UpdateWithDeferred( |
| 65 | + state + 1, |
| 66 | + LogIncrement, |
| 67 | + ) |
| 68 | + | Decrement => |
| 69 | + RestateReducer.UpdateWithDeferred( |
| 70 | + state - 1, |
| 71 | + LogDecrement, |
| 72 | + ) |
| 73 | + } |
| 74 | +
|
| 75 | +// A Scheduler handle deferred actions by triggering side effects and returning a cleanup function (if necessary) |
| 76 | +let scheduler: (RestateReducer.self<state, action>, deferredAction) => option<unit=>unit> = |
| 77 | + (self, deferredAction) => |
| 78 | + switch deferredAction { |
| 79 | + | LogIncrement => |
| 80 | + Js.log2("increment side effect: ", self.state) |
| 81 | + // Note: The state on the cleanup will the content of this scope, and |
| 82 | + // not the previous one that exist at moment of running the function. |
| 83 | + Some(() => Js.log2("increment cleanup: ", self.state)) |
| 84 | + | LogDecrement => |
| 85 | + Js.log2("decrement side effect: ", self.state) |
| 86 | + Some(() => Js.log2("decrement cleanup: ", self.state)) |
| 87 | + } |
| 88 | +
|
| 89 | +@react.component |
60 | 90 | let make = () => { |
61 | | - let (state, send) = |
62 | | - ReactUpdate.useReducerWithMapState( |
63 | | - (state, action) => |
64 | | - switch (action) { |
65 | | - | Increment => Update(state + 1) |
66 | | - | Decrement => Update(state + 1) |
67 | | - }, |
68 | | - () => 0 |
69 | | - ); |
| 91 | + let (state, send, _defer) = RestateReducer.useReducer(reducer, scheduler, 0) |
70 | 92 | <div> |
71 | 93 | {state->React.int} |
72 | 94 | <button onClick={_ => send(Decrement)}> {"-"->React.string} </button> |
73 | 95 | <button onClick={_ => send(Increment)}> {"+"->React.string} </button> |
74 | | - </div>; |
75 | | -}; |
| 96 | + </div> |
| 97 | +} |
76 | 98 | ``` |
77 | 99 |
|
78 | | -### Cancelling a side effect |
79 | | - |
80 | | -The callback you pass to `SideEffects` & `UpdateWithSideEffect` returns an `option(unit => unit)`, which is the cancellation function. |
| 100 | +## Lazy initialisation |
81 | 101 |
|
82 | | -```reason |
83 | | -// doesn't cancel |
84 | | -SideEffects(({send}) => { |
85 | | - Js.log(1); |
86 | | - None |
87 | | -}); |
88 | | -// cancels |
89 | | -SideEffects(({send}) => { |
90 | | - let request = Request.make(); |
91 | | - request->Future.get(payload => send(Receive(payload))) |
92 | | - Some(() => { |
93 | | - Request.cancel(request) |
94 | | - }) |
95 | | -}); |
96 | | -``` |
| 102 | +If you'd rather initialize state lazily (if there's some computation you don't want executed at every render for instance), use `useReducerWithMapState` where the first argument is a function taking `unit` and returning the initial state. |
97 | 103 |
|
98 | | -If you want to copy/paste old reducers that don't support cancellation, you can use `ReactUpdateLegacy` instead in place of `ReactUpdate`. Its `SideEffects` and `UpdateWithSideEffects` functions accept functions that return `unit`. |
|
0 commit comments