@@ -40,125 +40,139 @@ let io = io => IO(io);
4040// A reducer function takes the current state and an action and returns an update command
4141type reducer (' action , ' state ) = ('state, 'action) => update('action, 'state);
4242
43- // The react useReducer state stores the actual component state, along with a ref array of
44- // side effects. The side effects are collected in the reducer functions, then handled
45- // using a useEffect hook. The component should not be, nor need to be aware of the sideEffect business .
43+ // This new state type stores the caller's state along with a mutable array of effects that
44+ // need to be run at the appropriate time. The effects are given to us via the `update`
45+ // constructors like SideEffect, UpdateWithIO, IO, etc .
4646type stateAndSideEffects (' action , ' state ) = {
4747 state: 'state,
4848 sideEffects: ref (array(SideEffect . t('action, 'state))),
4949};
5050
51+ /**
52+ * Accepts a reducer function that emits `update` commands. Any updates that include side
53+ * effects (SideEffect, UpdateWithIO, etc.) will be handled by enqueuing the side effects
54+ * for execution outside of the reducer in a controlled fashion. The side effects are expected
55+ * to dispatch further reducer actions to cause state changes, etc. IO-based effects are expected
56+ * to produce actions that will be dispatched automatically.
57+ */
5158let useReducer = (reducer: reducer (' action , ' state ), initialState: ' state ) => {
52- let refReducer =
53- React . useRef(({state, sideEffects} as stateAndSideEffects, action) => {
54- let update = reducer(state, action);
55-
56- switch (update) {
57- | NoUpdate => stateAndSideEffects
58-
59- | Update (state ) => {... stateAndSideEffects, state}
60-
61- | UpdateWithSideEffect (state , sideEffect ) => {
62- state,
63- sideEffects:
64- ref (
65- Belt . Array . concat(
66- sideEffects^,
67- [| SideEffect . Uncancelable . lift(sideEffect)|] ,
68- ),
59+ // This wraps the given reducer function with the ability to capture the side effects
60+ // emitted by the various types of updates, and stick them in our mutable array of effects to run later
61+ let reducerWithSideEffects =
62+ ({state, sideEffects} as stateAndSideEffects, action) => {
63+ let update = reducer(state, action);
64+
65+ switch (update) {
66+ | NoUpdate => stateAndSideEffects
67+
68+ | Update (state ) => {... stateAndSideEffects, state}
69+
70+ | UpdateWithSideEffect (state , sideEffect ) => {
71+ state,
72+ sideEffects:
73+ ref (
74+ Belt . Array . concat(
75+ sideEffects^,
76+ [| SideEffect . Uncancelable . lift(sideEffect)|] ,
6977 ),
70- }
71-
72- | UpdateWithCancelableSideEffect ( state , cancelableSideEffect ) => {
73- state ,
74- sideEffects :
75- ref (
76- Belt . Array . concat (
77- sideEffects ^,
78- [| SideEffect . Cancelable . lift(cancelableSideEffect) |] ,
79- ) ,
78+ ) ,
79+ }
80+
81+ | UpdateWithCancelableSideEffect ( state , cancelableSideEffect ) => {
82+ state ,
83+ sideEffects :
84+ ref (
85+ Belt . Array . concat(
86+ sideEffects ^ ,
87+ [| SideEffect . Cancelable . lift(cancelableSideEffect) |] ,
8088 ),
81- }
82-
83- | SideEffect ( uncancelableSideEffect ) => {
84- ... stateAndSideEffects ,
85- sideEffects :
86- ref (
87- Belt . Array . concat (
88- stateAndSideEffects . sideEffects ^,
89- [| SideEffect . Uncancelable . lift(uncancelableSideEffect) |] ,
90- ) ,
89+ ) ,
90+ }
91+
92+ | SideEffect ( uncancelableSideEffect ) => {
93+ ... stateAndSideEffects ,
94+ sideEffects :
95+ ref (
96+ Belt . Array . concat(
97+ stateAndSideEffects . sideEffects ^ ,
98+ [| SideEffect . Uncancelable . lift(uncancelableSideEffect) |] ,
9199 ),
92- }
93-
94- | CancelableSideEffect ( cancelableSideEffect ) => {
95- ... stateAndSideEffects ,
96- sideEffects :
97- ref (
98- Belt . Array . concat (
99- stateAndSideEffects . sideEffects ^,
100- [| SideEffect . Cancelable . lift(cancelableSideEffect) |] ,
101- ) ,
100+ ) ,
101+ }
102+
103+ | CancelableSideEffect ( cancelableSideEffect ) => {
104+ ... stateAndSideEffects ,
105+ sideEffects :
106+ ref (
107+ Belt . Array . concat(
108+ stateAndSideEffects . sideEffects ^ ,
109+ [| SideEffect . Cancelable . lift(cancelableSideEffect) |] ,
102110 ),
111+ ),
112+ }
113+
114+ | UpdateWithIO (state , ioAction ) =>
115+ // The IO must have an 'action type for both the success and error channels - this
116+ // way we know that the errors have been properly handled and translated to the appropriate action.
117+ // Run the IO to get the success and error actions, then just send them.
118+ // TODO: we don't have cancelable IOs (yet?)
119+ let sideEffect : SideEffect . t (' action , ' state ) = (
120+ context => {
121+ ioAction
122+ |> Relude . IO . unsafeRunAsync(
123+ fun
124+ | Ok (action ) => context. send(action)
125+ | Error (action ) => context. send(action),
126+ );
127+ None ;
103128 }
104-
105- | UpdateWithIO (state , ioAction ) =>
106- // The IO must have an 'action type for both the success and error channels - this
107- // way we know that the errors have been properly handled and translated to the appropriate action.
108- // Run the IO to get the success and error actions, then just send them.
109- // TODO: we don't have cancelable IOs (yet?)
110- let sideEffect : SideEffect . t (' action , ' state ) = (
111- context => {
112- ioAction
113- |> Relude . IO . unsafeRunAsync(
114- fun
115- | Ok (action ) => context. send(action)
116- | Error (action ) => context. send(action),
117- );
118- None ;
119- }
120- );
121- {
122- state,
123- sideEffects:
124- ref (
125- Belt . Array . concat(
126- stateAndSideEffects. sideEffects^,
127- [| sideEffect|] ,
128- ),
129+ );
130+ {
131+ state,
132+ sideEffects:
133+ ref (
134+ Belt . Array . concat(
135+ stateAndSideEffects. sideEffects^,
136+ [| sideEffect|] ,
129137 ),
130- } ;
131-
132- | IO ( ioAction ) =>
133- let sideEffect : SideEffect . t ( ' action , ' state ) = (
134- context => {
135- ioAction
136- |> Relude . IO . unsafeRunAsync(
137- fun
138- | Ok ( action ) => context . send(action)
139- | Error (action ) => context. send(action),
140- ) ;
141- None ;
142- }
143- ) ;
144- {
145- ... stateAndSideEffects ,
146- sideEffects :
147- ref (
148- Belt . Array . concat (
149- stateAndSideEffects . sideEffects ^,
150- [| sideEffect |] ,
151- ) ,
138+ ) ,
139+ } ;
140+
141+ | IO ( ioAction ) =>
142+ let sideEffect : SideEffect . t ( ' action , ' state ) = (
143+ context => {
144+ ioAction
145+ |> Relude . IO . unsafeRunAsync(
146+ fun
147+ | Ok (action ) => context. send(action)
148+ | Error ( action ) => context . send(action),
149+ ) ;
150+ None ;
151+ }
152+ ) ;
153+ {
154+ ... stateAndSideEffects ,
155+ sideEffects :
156+ ref (
157+ Belt . Array . concat(
158+ stateAndSideEffects . sideEffects ^ ,
159+ [| sideEffect |] ,
152160 ),
153- } ;
161+ ) ,
154162 };
155- });
163+ };
164+ };
165+
166+ // Our new initial state is the caller's state, plus our initial empty array of effects to run
167+ let initialStateWithSideEffects = {
168+ state: initialState,
169+ sideEffects: ref ([||] ),
170+ };
156171
172+ // Plug our new reducer function into the React user reducer. This reducer takes the `update`s from the caller's
173+ // reducers and enqueues the side effects in a mutable array for processing below in a separate useEffect
157174 let ({state, sideEffects}, send ) =
158- React . useReducer(
159- refReducer |> React . Ref . current,
160- {state: initialState, sideEffects: ref ([||] )},
161- );
175+ React . useReducer(reducerWithSideEffects, initialStateWithSideEffects);
162176
163177 // This registers the side effects that were emitted by the reducer in a react effect hook.
164178 // When the hook runs, it will execute all the side effects and will
@@ -183,6 +197,6 @@ let useReducer = (reducer: reducer('action, 'state), initialState: 'state) => {
183197 [| sideEffects|] ,
184198 );
185199
186- // Finally, we return our initial state, and the send function for use in the component
200+ // Finally, we return our initial state, and the send function for use in the calling component
187201 (state, send);
188- };
202+ };
0 commit comments