• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bolasblack / react-components / 19277046459

11 Nov 2025 07:59PM UTC coverage: 96.499% (-0.1%) from 96.61%
19277046459

push

github

bolasblack
feat: add new package `@c4/use-effect-reducer`

148 of 158 branches covered (93.67%)

Branch coverage included in aggregate %.

79 of 81 new or added lines in 2 files covered. (97.53%)

293 of 299 relevant lines covered (97.99%)

125.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.06
/packages/useEffectReducer/src/useEffectReducer.ts
1
import {
2
  ActionDispatch,
3
  AnyActionArg,
4
  RefObject,
5
  useCallback,
6
  useEffect,
7
  useReducer,
8
  useRef,
9
  useSyncExternalStore,
10
} from 'react'
11

12
export interface EffectRunContext<A extends AnyActionArg> {
13
  abort: AbortSignal
14
  dispatch: ActionDispatch<A>
15
}
16

17
export interface EffectContext<S, A extends AnyActionArg> {
18
  dispatch: ActionDispatch<A>
19
  getState(): S
20

21
  abort: AbortSignal
22
  run<T>(
23
    id: string,
24
    callback: (ctx: EffectRunContext<A>) => Promise<T>,
25
  ): Promise<T>
26
  cancel(id: string): void
27
}
28

29
type StateEffect<S, E> = readonly [S, E?]
30

31
export type ReducerFnResult<S, E> = StateEffect<S, E>
32

33
export type ReducerFn<S, A extends AnyActionArg, E> = (
34
  prevState: S,
35
  ...args: A
36
) => ReducerFnResult<S, E>
37

38
export type OnEffectFn<S, E, A extends AnyActionArg> = (
39
  effect: E,
40
  ctx: EffectContext<S, A>,
41
) => void | Promise<void>
42

43
type OnInitFn<I, S, E> = (i: I) => StateEffect<S, E>
44

45
export function useEffectReducer<S, A extends AnyActionArg>(
46
  reducer: ReducerFn<S, A, void>,
47
  initialState: S,
48
): [S, ActionDispatch<A>]
49
export function useEffectReducer<S, A extends AnyActionArg, E>(
50
  reducer: ReducerFn<S, A, E>,
51
  onEffect: OnEffectFn<S, E, A>,
52
  initialState: S,
53
): [S, ActionDispatch<A>]
54
export function useEffectReducer<S, I, A extends AnyActionArg, E>(
55
  reducer: ReducerFn<S, A, E>,
56
  initialArg: I,
57
  init: OnInitFn<I, S, E>,
58
): [S, ActionDispatch<A>]
59
export function useEffectReducer<S, I, A extends AnyActionArg, E>(
60
  reducer: ReducerFn<S, A, E>,
61
  onEffect: OnEffectFn<S, E, A>,
62
  initialArg: I,
63
  init: OnInitFn<I, S, E>,
64
): [S, ActionDispatch<A>]
65
export function useEffectReducer<S, I, A extends AnyActionArg, E>(
66
  reducer: ReducerFn<S, A, E>,
67
  onEffectOrInitialArgOrState: OnEffectFn<S, E, A> | I | S,
68
  initialArgOrStateOrInit?: I | S | OnInitFn<I, S, E>,
69
  initOrUndefined?: OnInitFn<I, S, E>,
70
): [S, ActionDispatch<A>] {
71
  type WrappedState = { state: S }
72

73
  let onEffect: OnEffectFn<S, E, A>
74
  let initialArgOrState: I | S
75
  let initFn: OnInitFn<I, S, E>
76
  if (typeof onEffectOrInitialArgOrState === 'function') {
452✔
77
    onEffect = onEffectOrInitialArgOrState as OnEffectFn<S, E, A>
448✔
78
    initialArgOrState = initialArgOrStateOrInit as any
448✔
79
    initFn = initOrUndefined as any
448✔
80
  } else {
81
    onEffect = (async () => {}) as OnEffectFn<S, E, A>
4✔
82
    initialArgOrState = onEffectOrInitialArgOrState as any
4✔
83
    initFn = initialArgOrStateOrInit as any
4✔
84
  }
85

86
  const { current: effectStore } = useLazyRef(() => createEffectStore<E>())
452✔
87

88
  const pendingEffectsRef = useRef<E[]>([])
452✔
89
  useEffect(() => {
452✔
90
    if (pendingEffectsRef.current.length > 0) {
452✔
91
      pendingEffectsRef.current.forEach(effect => {
120✔
92
        effectStore.push(effect)
568✔
93
      })
94
      pendingEffectsRef.current = []
120✔
95
    }
96
  })
97

98
  const wrappedReducer = useCallback(
452✔
99
    (prevState: WrappedState, ...args: A): WrappedState => {
100
      const res = reducer(prevState.state, ...args)
620✔
101

102
      if (res.length > 1) {
620✔
103
        pendingEffectsRef.current.push(res[1]!)
560✔
104
      }
105

106
      return { state: res[0] }
620✔
107
    },
108
    [reducer],
109
  )
110

111
  const wrappedInit = useCallback(
452✔
112
    (i: any): WrappedState => {
113
      const res: StateEffect<S, E> = initFn == null ? [i] : initFn(i)
84✔
114

115
      if (res.length > 1) {
84✔
116
        pendingEffectsRef.current.push(res[1]!)
8✔
117
      }
118

119
      return { state: res[0] }
84✔
120
    },
121
    [initFn],
122
  )
123

124
  const [state, dispatch] = useReducer(
452✔
125
    wrappedReducer,
126
    initialArgOrState,
127
    wrappedInit,
128
  )
129

130
  const latestStateRef = useLatestRef(state)
452✔
131
  const latestOnEffectRef = useLatestRef(onEffect)
452✔
132
  const abortControllersRef = useRef<Record<string, AbortController>>({})
452✔
133

134
  // Subscribe to effect queue
135
  const effectQueue = useSyncExternalStore(
452✔
136
    effectStore.subscribe,
137
    effectStore.getSnapshot,
138
    effectStore.getServerSnapshot,
139
  )
140

141
  // Process effects
142
  useEffect(() => {
452✔
143
    if (effectQueue.length === 0) return
324✔
144
    if (latestOnEffectRef.current == null) return
120!
145

146
    const effectRootId = 'process'
120✔
147
    const genEffectSubId = (id: string): string => `sub-processes$$$${id}`
120✔
148

149
    abortControllersRef.current[effectRootId]?.abort()
120✔
150
    const rootCtrl = new AbortController()
324✔
151
    abortControllersRef.current[effectRootId] = rootCtrl
324✔
152

153
    effectQueue.forEach(effect => {
324✔
154
      void latestOnEffectRef.current?.(effect, {
568✔
155
        dispatch,
156
        getState: () => latestStateRef.current.state,
12✔
157
        abort: rootCtrl.signal,
158
        run: (_id, callback) => {
159
          const id = genEffectSubId(_id)
56✔
160
          abortControllersRef.current[id]?.abort()
56✔
161
          const taskCtrl = new AbortController()
56✔
162
          abortControllersRef.current[id] = taskCtrl
56✔
163

164
          return callback({
56✔
165
            abort: taskCtrl.signal,
166
            dispatch: (...args) => {
167
              if (taskCtrl.signal.aborted) return
48✔
168
              dispatch(...args)
20✔
169
            },
170
          }).finally(() => {
171
            if (abortControllersRef.current[id] === taskCtrl) {
48✔
172
              delete abortControllersRef.current[id]
36✔
173
            }
174
          })
175
        },
176
        cancel: id => {
177
          const fullId = genEffectSubId(id)
8✔
178
          abortControllersRef.current[fullId]?.abort()
8✔
179
          delete abortControllersRef.current[fullId]
8✔
180
        },
181
      })
182
    })
183

184
    effectStore.clear()
324✔
185
  }, [effectQueue, effectStore, latestOnEffectRef, latestStateRef])
186

187
  // Cleanup on unmount
188
  useEffect(
452✔
189
    () => () => {
84✔
190
      Object.values(abortControllersRef.current).forEach(ctrl => {
84✔
191
        ctrl.abort()
88✔
192
      })
193
    },
194
    [],
195
  )
196

197
  return [state.state, dispatch]
452✔
198
}
199

200
interface EffectStore<E> {
201
  push(effect: E): void
202
  clear(): void
203
  subscribe(listener: () => void): () => void
204
  getSnapshot(): E[]
205
  getServerSnapshot(): E[]
206
}
207
function createEffectStore<E>(): EffectStore<E> {
208
  let queue: E[] = []
84✔
209
  const listeners = new Set<() => void>()
84✔
210

211
  return {
84✔
212
    push(effect) {
213
      queue = [...queue, effect]
568✔
214
      listeners.forEach(l => l())
568✔
215
    },
216

217
    clear() {
218
      if (queue.length > 0) {
120!
219
        queue = []
120✔
220
        listeners.forEach(l => l())
120✔
221
      }
222
    },
223

224
    subscribe(_listener) {
225
      const listener = (): void => _listener()
680✔
226
      listeners.add(listener)
84✔
227
      return () => listeners.delete(listener)
84✔
228
    },
229

230
    getSnapshot() {
231
      return queue
1,908✔
232
    },
233

234
    getServerSnapshot() {
NEW
235
      return []
×
236
    },
237
  }
238
}
239

240
function useLatestRef<T>(item: T): RefObject<T> {
241
  const ref = useRef<T>(null) as RefObject<T>
904✔
242
  ref.current = item
904✔
243
  return ref
904✔
244
}
245

246
function useLazyRef<T>(factory: () => T): RefObject<T> {
247
  const ref = useRef<T>(null) as RefObject<T>
452✔
248
  const isAssigned = useRef(false)
452✔
249

250
  if (!isAssigned.current) {
452✔
251
    isAssigned.current = true
84✔
252
    ref.current = factory()
84✔
253
  }
254

255
  return ref
452✔
256
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc