Skip to content

Commit ece1aba

Browse files
authored
feat(utils): use dispatching reducer to sync dispatch action (#68172)
Use the new dispatching reducer in trace view to sync react to actions
1 parent f76c673 commit ece1aba

File tree

3 files changed

+153
-117
lines changed

3 files changed

+153
-117
lines changed

static/app/utils/useDispatchingReducer.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ describe('useDispatchingReducer', () => {
1616
const reducer = jest.fn().mockImplementation(s => s) as () => {};
1717
const initialState = {type: 'initial'};
1818
const {result} = reactHooks.renderHook(() =>
19+
// @ts-expect-error force undfined
1920
useDispatchingReducer(reducer, undefined, () => initialState)
2021
);
2122

static/app/utils/useDispatchingReducer.tsx

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import {useCallback, useMemo, useRef, useState} from 'react';
1+
import type React from 'react';
2+
import {
3+
type ReducerAction,
4+
type ReducerState,
5+
useCallback,
6+
useMemo,
7+
useRef,
8+
useState,
9+
} from 'react';
210

311
/**
412
* A hook that wraps a reducer to provide an observer pattern for the state.
@@ -10,20 +18,30 @@ import {useCallback, useMemo, useRef, useState} from 'react';
1018
*/
1119

1220
type ArgumentTypes<F extends Function> = F extends (...args: infer A) => any ? A : never;
13-
interface Middlewares<S, A> {
14-
['before action']: (S: Readonly<S>, A: A) => void;
15-
['before next state']: (P: Readonly<S>, S: Readonly<S>, A: A) => void;
21+
22+
export interface DispatchingReducerMiddleware<R extends React.Reducer<any, any>> {
23+
['before action']: (S: Readonly<ReducerState<R>>, A: React.ReducerAction<R>) => void;
24+
['before next state']: (
25+
P: Readonly<React.ReducerState<R>>,
26+
S: Readonly<React.ReducerState<R>>,
27+
A: React.ReducerAction<R>
28+
) => void;
1629
}
1730

18-
type MiddlewaresEvent<S, A> = {[K in keyof Middlewares<S, A>]: Set<Middlewares<S, A>[K]>};
31+
type MiddlewaresEvent<R extends React.Reducer<any, any>> = {
32+
[K in keyof DispatchingReducerMiddleware<R>]: Set<DispatchingReducerMiddleware<R>[K]>;
33+
};
1934

20-
class Emitter<S, A> {
21-
listeners: MiddlewaresEvent<S, A> = {
22-
'before action': new Set<Middlewares<S, A>['before action']>(),
23-
'before next state': new Set<Middlewares<S, A>['before next state']>(),
35+
class Emitter<R extends React.Reducer<any, any>> {
36+
listeners: MiddlewaresEvent<R> = {
37+
'before action': new Set<DispatchingReducerMiddleware<R>['before action']>(),
38+
'before next state': new Set<DispatchingReducerMiddleware<R>['before next state']>(),
2439
};
2540

26-
on(key: keyof Middlewares<S, A>, fn: Middlewares<S, A>[keyof Middlewares<S, A>]) {
41+
on(
42+
key: keyof DispatchingReducerMiddleware<R>,
43+
fn: DispatchingReducerMiddleware<R>[keyof DispatchingReducerMiddleware<R>]
44+
) {
2745
const store = this.listeners[key];
2846
if (!store) {
2947
throw new Error(`Unsupported reducer middleware: ${key}`);
@@ -33,9 +51,9 @@ class Emitter<S, A> {
3351
store.add(fn);
3452
}
3553

36-
removeListener(
37-
key: keyof Middlewares<S, A>,
38-
listener: Middlewares<S, A>[keyof Middlewares<S, A>]
54+
off(
55+
key: keyof DispatchingReducerMiddleware<R>,
56+
listener: DispatchingReducerMiddleware<R>[keyof DispatchingReducerMiddleware<R>]
3957
) {
4058
const store = this.listeners[key];
4159
if (!store) {
@@ -47,8 +65,8 @@ class Emitter<S, A> {
4765
}
4866

4967
emit(
50-
key: keyof Middlewares<S, A>,
51-
...args: ArgumentTypes<Middlewares<S, A>[typeof key]>
68+
key: keyof DispatchingReducerMiddleware<R>,
69+
...args: ArgumentTypes<DispatchingReducerMiddleware<R>[typeof key]>
5270
) {
5371
const store = this.listeners[key];
5472
if (!store) {
@@ -59,13 +77,15 @@ class Emitter<S, A> {
5977
}
6078
}
6179

62-
export function useDispatchingReducer<S, A>(
63-
reducer: React.Reducer<S, A>,
64-
initialState: S,
65-
initializer?: (arg: S) => S
66-
): [S, React.Dispatch<A>, Emitter<S, A>] {
67-
const emitter = useMemo(() => new Emitter<S, A>(), []);
68-
const [state, setState] = useState(initialState ?? (initializer?.(initialState) as S));
80+
export function useDispatchingReducer<R extends React.Reducer<any, any>>(
81+
reducer: R,
82+
initialState: ReducerState<R>,
83+
initializer?: (arg: ReducerState<R>) => ReducerState<R>
84+
): [ReducerState<R>, React.Dispatch<ReducerAction<R>>, Emitter<R>] {
85+
const emitter = useMemo(() => new Emitter<R>(), []);
86+
const [state, setState] = useState(
87+
initialState ?? (initializer?.(initialState) as ReducerState<R>)
88+
);
6989

7090
const reducerRef = useRef(reducer);
7191
reducerRef.current = reducer;
@@ -75,7 +95,7 @@ export function useDispatchingReducer<S, A>(
7595
stateRef.current = state;
7696

7797
const wrappedDispatch = useCallback(
78-
(action: A) => {
98+
(action: ReducerAction<R>) => {
7999
// @TODO it is possible for a dispatched action to throw an error
80100
// and break the reducer. We should probably catch it, I'm just not sure
81101
// what would be the best mechanism to handle it. If we opt to rethrow,

0 commit comments

Comments
 (0)