@c4/use-effect-reducer
GitHub Repository: https://github.com/bolasblack/react-components/tree/develop/packages/useEffectReducer
import { Meta, Source } from '@storybook/addon-docs/blocks' import * as DogFetcherStories from './src/useEffectReducer.stories'
<style>{`.sbdocs .markdown-only { display: none !important; }`}</style>useEffectReducer takes its primary inspiration from Elm's TEA: update : Model -> Msg -> ( Model, Cmd Msg ). Reducers stay pure while returning both the next state and a description of the side effects to run. This hook brings that pattern to React—reducers output [state, effect], and an interpreter inside the hook coordinates effect execution, lifecycles, and cancellation.
Why use it?
- Effects fire from events, not from "watching state": reducers declare effects explicitly, so no more guessing with
useEffectdependency arrays. - Lifecycle & cancellation are first-class:
ctx.run(id, callback)andctx.cancel(id)manage scopedAbortControllers automatically, including component unmounts. - Centralized, testable logic: reducers remain pure (easy to snapshot test) while
onEffectis the single place where impure work lives. - Gradual adoption: the API mirrors
useReducer, includinginitfunctions and lazy initialization, so existing reducers can migrate incrementally.
API overview
const [state, dispatch] = useEffectReducer(reducer, onEffect, initialState)
reducer(prevState, action) => [nextState, effect?]onEffect(effect, ctx)receives the effect plus helpers:dispatch: send additional eventsgetState(): read the latest stateabort: rootAbortSignalrun(id, runner): launch cancellable async workcancel(id): abort a previously launched task
Example: DogFetcher (with auto-cancel & race safety)
Requirements:
- Clicking the button fetches a random dog image.
- The button stays disabled while loading.
- Rapid clicks should keep only the latest request; stale responses must be ignored.
- In-flight requests must cancel when the component unmounts.
For the traditional
useReducer + useEffectimplementation (and the pitfalls it carries), see David Khourshid's No, disabling a button is not app logic.
DogFetcher implemented with useEffectReducer
DogFetcher implemented with useReducer + useEffect
Code adapted from David Khourshid's No, disabling a button is not app logic.
Recipes
Looking for common patterns and solutions? Check out our collection of recipes:
- Conditional Confirmation Prompts | 中文版 - Learn how to conditionally show confirmation dialogs before performing actions
Further reading
- Elm – The Elm Architecture: the original
(Model, Cmd msg)pattern. - Redux is half of a pattern (DEV.to): formalizes
state + event = state' + effects. - There are so many fundamental misunderstandings about XState… (Medium): advocates event-driven effects over state-driven guessing.
- No, disabling a button is not app logic (DEV.to): walks through refactoring from
useState→useReducer + useEffect→ declarative state machine effects.