react-components/packages/useEffectReducer at develop · bolasblack/react-components

@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 useEffect dependency arrays.
  • Lifecycle & cancellation are first-class: ctx.run(id, callback) and ctx.cancel(id) manage scoped AbortControllers automatically, including component unmounts.
  • Centralized, testable logic: reducers remain pure (easy to snapshot test) while onEffect is the single place where impure work lives.
  • Gradual adoption: the API mirrors useReducer, including init functions 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 events
    • getState(): read the latest state
    • abort: root AbortSignal
    • run(id, runner): launch cancellable async work
    • cancel(id): abort a previously launched task

Example: DogFetcher (with auto-cancel & race safety)

Requirements:

  1. Clicking the button fetches a random dog image.
  2. The button stays disabled while loading.
  3. Rapid clicks should keep only the latest request; stale responses must be ignored.
  4. In-flight requests must cancel when the component unmounts.

For the traditional useReducer + useEffect implementation (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:

View all recipes →

Further reading