1+ // PoC for Restate library: Let's make useReducer great again!
2+ // Redux like architecture with modern React effects approach.
3+
4+ // TODO: Look for a better name
5+ // HasDeferredAction "type class" should provide a way of identify for each deferred action.
6+ // By uniquely identify their variant constructor.
7+ module type HasDeferredAction = {
8+ type t
9+ let variantId : t => string
10+ }
11+
12+ module MakeReducer = (DeferredAction : HasDeferredAction ) => {
13+ /** API Types **/
14+ type dispatch <'action > = 'action => unit // Reducer Trigger Function
15+ type schedule = DeferredAction .t => unit // Scheduler Trigger Function
16+ // Magic Types based on ReactUpdate library
17+ type update <'state > =
18+ | NoUpdate // no update
19+ | Update ('state ) // update only
20+ | UpdateWithDeferred ('state , DeferredAction .t ) // update and defer a deferred action
21+ | Deferred (DeferredAction .t ) // no update, but defer a deferred action
22+ // Shape of Restate Reducer
23+ type self <'state , 'action > = {
24+ send : dispatch <'action >,
25+ defer : schedule ,
26+ state : 'state ,
27+ }
28+ // React Reducers looks like this:
29+ // type reducer<'state, 'action> = ('state, 'action) => 'state
30+ // vs Restate Reducer "reduce" function:
31+ type reducer <'state , 'action > = ('state , 'action ) => update <'state >
32+ // ^ Main difference is that our reduce function is wrapped in an custom "update" type.
33+ type scheduler <'state > = ('state , DeferredAction .t ) => option <unit => unit >
34+ // ^ Scheduler is like an impure reducer that group the async/side effects actions related code.
35+ // But instead of dispatch actions inmmediatly, in their case, we differ them into a queue.
36+
37+ /** Internal Types ***/
38+ // This our the library internal actions we can dispatch.
39+ // As you can see in the implementation, they act as a proxy to the user actions.
40+ type proxy <'action > =
41+ | WiredAction ('action )
42+ | PushDeferred (DeferredAction .t )
43+ | PopDeferred (Belt .List .t <DeferredAction .t >)
44+ type internalState <'action , 'state > = {
45+ userState : 'state ,
46+ // TODO: Ideally we should use a Queue data type. If not, maybe use Array instead.
47+ deferredActionsQueue : Belt .List .t <DeferredAction .t >
48+ }
49+
50+ // /** Usage: **/
51+ // 1. Init module;
52+ // module RestateReducer = Restate.MakeReducer(DeferredActions)
53+ // 2. Use the Hook;
54+ // RestateReduser.useReducer(reducer, scheduler, initialState)
55+ let useReducer = (
56+ reducer : reducer <'state , 'action >, // The reducer provided by the user
57+ scheduler : scheduler <'state >, // The scheduler provided by the user
58+ initialState : 'state
59+ ) => {
60+ // NOTE: We must follow the rules of React about effects cleanup!
61+ // After every re-render with changed dependencies,
62+ // React will first run the cleanup function (if you provided it) with the old values,
63+ // and then run your setup function with the new values
64+ // type effect<'action> = ('action => option<unit => unit>) => unit
65+ // Ref. https://react.dev/reference/react/useEffect#parameters
66+ let cleanupFnsRef : React .ref <Belt .Map .String .t <option <unit => unit >>> = React .useRef (Belt .Map .String .empty )
67+ let ({userState , deferredActionsQueue }, internalDispatch ) = React .useReducer (
68+ ({userState , deferredActionsQueue } as internalState , internalAction ) =>
69+ switch internalAction {
70+ | WiredAction (action ) =>
71+ switch reducer (userState , action ) {
72+ | NoUpdate => internalState
73+ | Update (state ) => {... internalState , userState : state }
74+ | UpdateWithDeferred (state , deferredAction ) => {
75+ userState : state ,
76+ deferredActionsQueue : Belt .List .concat (deferredActionsQueue , list {deferredAction }),
77+ }
78+ | Deferred (deferredAction ) => {
79+ ... internalState ,
80+ deferredActionsQueue : Belt .List .concat (deferredActionsQueue , list {deferredAction }),
81+ }
82+ }
83+ | PushDeferred (deferredAction ) => {
84+ ... internalState ,
85+ deferredActionsQueue : Belt .List .concat (deferredActionsQueue , list {deferredAction }),
86+ }
87+ | PopDeferred (tailDeferredActions ) => {
88+ ... internalState ,
89+ deferredActionsQueue : tailDeferredActions ,
90+ }
91+ }
92+ , {userState : initialState , deferredActionsQueue : list {}}
93+ )
94+ // Obs: Actually this useEffect and the other one compose the "scheduler"
95+ React .useEffect1 (() => {
96+ // CAUTION: Maybe we should run all of them in a single effect (?)
97+ // What happens if there are 2 consecutives of the same type?
98+ switch (deferredActionsQueue ) {
99+ | list {deferredAction , ... queueTail } =>
100+ // 1. If there is a previous cleanup function, run it
101+ cleanupFnsRef .current
102+ -> Belt .Map .String .get (DeferredAction .variantId (deferredAction ))
103+ -> Belt .Option .map (mPrevCleanupFn => mPrevCleanupFn -> Belt .Option .map (prevCleanupFn => prevCleanupFn ()))
104+ -> ignore
105+ // 2. Run the deferred action
106+ let mNewCleanupFn = scheduler (userState , deferredAction ) // CAUTION: Is reducerState the latest state?
107+ // 3. Update the cleanup function
108+ cleanupFnsRef .current = cleanupFnsRef .current -> Belt .Map .String .set (DeferredAction .variantId (deferredAction ), mNewCleanupFn )
109+ // 4. Pop the action from the queue
110+ internalDispatch (PopDeferred (queueTail ))
111+ | list {} => () // Stop condition!
112+ }
113+ None
114+ }, [deferredActionsQueue ])
115+ // In case of unmount, we must run all cleanup functions and empty their tracking map
116+ React .useEffect0 (() => {
117+ Some (() => {
118+ cleanupFnsRef .current
119+ -> Belt .Map .String .valuesToArray
120+ -> Belt .Array .forEach (
121+ mCleanupFn => mCleanupFn -> Belt .Option .forEach (cleanupFn => cleanupFn ())
122+ )
123+ // Not needed, given after unmount the ref is destroyed by React
124+ //cleanupFnsRef.current = Belt.Map.String.empty
125+ }
126+ )
127+ }
128+ )
129+ let defer : schedule = deferredAction => internalDispatch (PushDeferred (deferredAction ))
130+ let send : dispatch <'action > = action => internalDispatch (WiredAction (action ))
131+ // Notice that this is the API the user will receive.
132+ // This way, we hide implementation (unsafe) tricks.
133+ (userState , send , defer )
134+ }
135+ }
0 commit comments