Hey! Thank you for sharing, but I believe you’ve forgot to post the link to the library itself.
I’m a fan of the Elm pattern and use it as well. Here’s what I came to:
// In a page component
let (state, dispatch) = ReactX.useEffectReducer(
~reducer=Logic.execute,
~runEffect=(effect, dispatch) => Runtime.runEffect(effect, ~dispatch, ~onNavigate=navigate),
~initialState=State.make(~target=patternId == "new" ? Create : Edit(patternId)),
~noEffect=T.Effect.NoEffect,
)
and here’s an example of a page goodness:
module Dialog = {
type t = [#DeleteConfirm]
}
module Alert = {
type t = [
| #PatternLoadFailed
| #PatternSaveFailed
| #PatternDeleteFailed
| #FileUploadFailed
| #PreviewLoadFailed
| #ThumbnailUploadFailed
]
}
module PreviewStage = {
type t =
| Idle
| Loading(int) // revision number to handle stale responses
| Ready({centerDrawing: Drawing.t, displayDrawing: Drawing.t})
| Failed
}
module Effect = {
type rec t =
| NoEffect
| FetchPattern(Uuid.t)
| SavePattern({isNew: bool, pattern: Pattern.Entity.t})
| DeletePattern(Uuid.t)
| UploadFile({fileName: string, content: string})
| UploadThumbnail({content: string, mimeType: string, revision: int})
| LoadPreview({url: string, revision: int})
| Navigate(string)
| Batch(array<t>)
}
module Action = {
type t =
| PageMounted
| PatternLoaded(Pattern.View.t)
| LoadFailed(ApiError.t)
| FormChanged(PatternForm__State.t)
| FormInputChanged
| ChangesDiscarded
| SaveRequested
| SaveSucceeded(Pattern.View.t)
| SaveFailed(ApiError.t)
| DialogOpened(Dialog.t)
| DialogClosed
| DeleteConfirmed
| DeleteSucceeded
| DeleteFailed(ApiError.t)
| AlertDismissed
| FileUploadStarted({fileName: string, content: string})
| FileUploadSucceeded(string)
| FileUploadFailed(ApiError.t)
| FileRemoved
| PreviewLoaded({centerDrawing: Drawing.t, revision: int})
| PreviewFailed(int) // revision
| ThumbnailUploadStarted({content: string, mimeType: string})
| ThumbnailCaptured(string) // dataURL from snapshot
| ThumbnailUploadSucceeded({url: string, revision: int})
| ThumbnailUploadFailed({error: ApiError.t, revision: int})
| ThumbnailRemoved
}
module type State = {
type t
type target = Create | Edit(Uuid.t)
let make: (~target: target) => t
// Accessors
let target: t => target
let pattern: t => AsyncData.t<Pattern.View.t>
let submissionState: t => SubmissionState.t
let formState: t => PatternForm__State.t
let isFormDirty: t => bool
let formRevision: t => int
let currentDialog: t => option<Dialog.t>
let alert: t => option<Alert.t>
let previewStage: t => PreviewStage.t
let thumbnailState: t => PatternThumbnail__State.t
// Derived state
let isPresentable: t => bool
let isTransient: t => bool
let canCaptureSnapshot: t => bool
let centerAspectRatio: t => option<float>
}
…and the key functions interfaces:
// Logic
let execute: (State.t, T.Action.t) => (State.t, T.Effect.t)
// Runtime
let runEffect: (T.Effect.t, ~dispatch: T.Action.t => unit, ~onNavigate: string => unit) => unit
This is a very powerful pattern that covers pages of any complexity with only one React hook. Easily unit-testable, traceable, debugable. Love it. Thank you, Elm.
I’m curious how’s your interpretation differs in implementation 
For those who might be interesting, here’s the hook implementation itself:
// Effect-as-data pattern hook (Elm Architecture)
type effectReducer<'state, 'action, 'effect> = ('state, 'action) => ('state, 'effect)
type effectInterpreter<'effect, 'action> = ('effect, 'action => unit) => unit
// Internal: state + last effect produced by reducer
type stateWithEffect<'state, 'effect> = {
state: 'state,
lastEffect: 'effect,
}
/**
* useEffectReducer implements the Elm Architecture pattern for React.
*
* The reducer is a pure function that takes state and action, returns new state and an effect.
* The effect interpreter runs side effects and dispatches new actions.
*
* @param reducer Pure function: (state, action) => (newState, effect)
* @param runEffect Effect interpreter: (effect, dispatch) => unit
* @param initialState Initial state value
* @param noEffect Value representing "no effect" (used to skip effect execution)
* @returns Tuple of (state, dispatch)
*/
let useEffectReducer = (
~reducer: effectReducer<'state, 'action, 'effect>,
~runEffect: effectInterpreter<'effect, 'action>,
~initialState: 'state,
~noEffect: 'effect,
~withDebugLogging: bool=false,
): ('state, 'action => unit) => {
// Wrap reducer to store effect alongside state
let wrappedReducer = (stateWithEffect, action) => {
if withDebugLogging {
Console.log2("[useEffectReducer] Running action", action)
}
let (newState, effect) = reducer(stateWithEffect.state, action)
if withDebugLogging {
Console.log2("[useEffectReducer] New state", newState)
Console.log2("[useEffectReducer] Effect", effect)
}
{state: newState, lastEffect: effect}
}
let (stateWithEffect, dispatch) = React.useReducer(
wrappedReducer,
{state: initialState, lastEffect: noEffect},
)
// Run effect whenever it changes (not on every render)
React.useEffect1(() => {
if stateWithEffect.lastEffect !== noEffect {
runEffect(stateWithEffect.lastEffect, dispatch)
}
noEffectCleanup
}, [stateWithEffect.lastEffect])
(stateWithEffect.state, dispatch)
}