🌳 What is “Elm-style”?
Elm popularized the Model-View-Update (MVU) pattern, where each update returns both the next model and the commands to run:
update : Msg -> Model -> ( Model, Cmd Msg )
This design keeps all logic about “what happens when event X occurs” in one place — the update function — instead of scattering side effects across random useEffects.
It also keeps reducers pure while making when and what side effects happen explicit and interpretable by a runtime.
React’s docs echo this philosophy: effects should be an escape hatch, not the default — see You Might Not Need an Effect.
If you want to see how this kind of modeling makes UI logic elegant and maintainable, check out David Khourshid’s classic post “No, disabling a button is not app logic.” 💡
⚛️ So why another Elm-style reducer?
I know there are several similar libraries — and I like many of them! — but I wanted a variant with different trade-offs:
🧱 Some are archived. For example,
useEffectReducerandreact-use-bireducerare now read-only.⚙️ Keep it tiny. Fits in one file with almost no dependencies — copy, tweak, or delete it whenever you want.
-
🧪 Effects as plain objects + separate interpreter. I prefer returning serializable effect descriptors and implementing the actual effect logic in one dedicated place. It’s easier to test reducers (assert on descriptors) without invoking real side effects. (Elm’s
update : Msg -> Model -> (Model, Cmd Msg)inspires this split.)Of course,
useEffectReducerdoesn’t stop you from returning a function as an effect — the API is flexible enough; it’s just not my personal preference 🙂. -
💬 Lower the barrier. I don’t want people curious about the Elm-style approach to feel like they must first study Elm’s
Cmd Msgsystem or learn a full-blown state machine library like XState just to try this pattern.Both XState and Elm are amazing — I’m genuinely thankful to David Khourshid and all of XState’s contributors, and to the Elm community. I’ve used XState in multiple real projects (it’s saved me countless times!), but it does have a learning curve. Moving from
useReducertouseEffectReducer, on the other hand, should feel smooth and approachable 🚀.)
🧪 Example usage
Let’s reimplement the example from David Khourshid’s article
“No, disabling a button is not app logic.”, but this time using useEffectReducer ✨
You can also check out both implementations on GitHub:
- 🪄 useEffectReducer version: useEffectReducer.stories.tsx → L121–203
- ⚙️ useReducer version: useEffectReducer.stories.tsx → L24–119
🔗 Related work
Here are some excellent existing takes on bringing Elm-style “state + effects” reducers into React:
-
davidkpiano/useEffectReducer— by David Khourshid, effectful reducers via anexechelper; archived. -
soywod/react-use-bireducer— returns[state, effects]and processes effects through a separate “effect reducer”; archived. -
ncthbrt/react-use-elmish— Elmish-style hook combining reducer logic with async helpers. -
redux-loop— Redux enhancer adding Elm-like effect tuples. -
dai-shi/use-reducer-async— extendsdispatchfor async actions; conceptually related. -
useReducerWithEmitEffect— Sophie Alpert’s gist that inspired much of this work.
Top comments (0)