Skip to content

Commit 66e7336

Browse files
authored
fix: Interceptors work on manager-dispatched actions (#3365)
1 parent 47e00c6 commit 66e7336

File tree

15 files changed

+411
-354
lines changed

15 files changed

+411
-354
lines changed

.changeset/silly-flies-juggle.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@data-client/test': patch
3+
---
4+
5+
fix: Interceptors work on manager-dispatched actions.
6+
7+
For example, renderHook now can use resolverFixtures to resolve fetches for subscriptions.
8+
This was not possible before as SubscriptionManager's dispatches would not be intercepted with the
9+
previous implementation.

.changeset/sour-walls-kick.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@data-client/core': patch
3+
---
4+
5+
internal: Controller.bindMiddleware() to be used in applyMiddleware.
6+
7+
This API is not intended to be used elsewhere, but will become the standard interface between
8+
Controller's and applyMiddleware.

packages/core/src/controller/Controller.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,19 @@ import {
3535
} from './actions/index.js';
3636
import ensurePojo from './ensurePojo.js';
3737
import type { EndpointUpdateFunction } from './types.js';
38+
import { ReduxMiddlewareAPI } from '../manager/applyManager.js';
3839
import type { GCInterface } from '../state/GCPolicy.js';
3940
import { ImmortalGCPolicy } from '../state/GCPolicy.js';
4041
import { initialState } from '../state/reducer/createReducer.js';
4142
import selectMeta from '../state/selectMeta.js';
42-
import type { ActionTypes, State } from '../types.js';
43+
import type { ActionTypes, Dispatch, State } from '../types.js';
4344

4445
export type GenericDispatch = (value: any) => Promise<void>;
4546
export type DataClientDispatch = (value: ActionTypes) => Promise<void>;
4647

47-
interface ConstructorProps<D extends GenericDispatch = DataClientDispatch> {
48+
export interface ControllerConstructorProps<
49+
D extends GenericDispatch = DataClientDispatch,
50+
> {
4851
dispatch?: D;
4952
getState?: () => State<unknown>;
5053
memo?: Pick<MemoCache, 'denormalize' | 'query' | 'buildQueryKey'>;
@@ -68,22 +71,23 @@ const unsetState = (): State<unknown> => {
6871
* @see https://dataclient.io/docs/api/Controller
6972
*/
7073
export default class Controller<
74+
// NOTE: We template on entire dispatch, so we can be contravariant on ActionTypes
7175
D extends GenericDispatch = DataClientDispatch,
7276
> {
7377
/**
7478
* Dispatches an action to Reactive Data Client reducer.
7579
*
7680
* @see https://dataclient.io/docs/api/Controller#dispatch
7781
*/
78-
declare readonly dispatch: D;
82+
declare protected _dispatch: D;
7983
/**
8084
* Gets the latest state snapshot that is fully committed.
8185
*
8286
* This can be useful for imperative use-cases like event handlers.
8387
* This should *not* be used to render; instead useSuspense() or useCache()
8488
* @see https://dataclient.io/docs/api/Controller#getState
8589
*/
86-
declare readonly getState: () => State<unknown>;
90+
declare getState: () => State<unknown>;
8791
/**
8892
* Singleton to maintain referential equality between calls
8993
*/
@@ -102,13 +106,35 @@ export default class Controller<
102106
getState = unsetState,
103107
memo = new MemoCache(),
104108
gcPolicy = new ImmortalGCPolicy(),
105-
}: ConstructorProps<D> = {}) {
106-
this.dispatch = dispatch;
109+
}: ControllerConstructorProps<D> = {}) {
110+
this._dispatch = dispatch;
107111
this.getState = getState;
108112
this.memo = memo;
109113
this.gcPolicy = gcPolicy;
110114
}
111115

116+
// TODO: drop when drop support for destructuring (0.14 and below)
117+
set dispatch(dispatch: D) {
118+
/* istanbul ignore next */
119+
this._dispatch = dispatch;
120+
}
121+
122+
// TODO: drop when drop support for destructuring (0.14 and below)
123+
get dispatch(): D {
124+
return this._dispatch;
125+
}
126+
127+
bindMiddleware({
128+
dispatch,
129+
getState,
130+
}: {
131+
dispatch: D;
132+
getState: ReduxMiddlewareAPI['getState'];
133+
}) {
134+
this._dispatch = dispatch;
135+
this.getState = getState;
136+
}
137+
112138
/*************** Action Dispatchers ***************/
113139

114140
/**

packages/core/src/controller/__tests__/Controller.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,15 @@ describe('Controller', () => {
5555
},
5656
};
5757
const getState = () => state;
58+
const dispatch = jest.fn(() => Promise.resolve());
5859
const controller = new Controller({
59-
dispatch: jest.fn(() => Promise.resolve()),
60+
dispatch,
6061
getState,
6162
});
6263
const article = await controller.fetchIfStale(CoolerArticleResource.get, {
6364
id: payload.id,
6465
});
65-
expect(controller.dispatch.mock.calls.length).toBe(0);
66+
expect(dispatch.mock.calls.length).toBe(0);
6667
expect(article.title).toBe(payload.title);
6768
});
6869
it('should fetch if result stale', () => {
@@ -92,15 +93,16 @@ describe('Controller', () => {
9293
},
9394
};
9495
const getState = () => state;
96+
const dispatch = jest.fn(() => Promise.resolve());
9597
const controller = new Controller({
96-
dispatch: jest.fn(() => Promise.resolve()),
98+
dispatch,
9799
getState,
98100
});
99101
controller.fetchIfStale(CoolerArticleResource.get, {
100102
id: payload.id,
101103
});
102104

103-
expect(controller.dispatch.mock.calls.length).toBe(1);
105+
expect(dispatch.mock.calls.length).toBe(1);
104106
});
105107
});
106108
});

packages/core/src/manager/applyManager.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,13 @@ export default function applyManager(
1818
}
1919
return managers.map((manager, i) => {
2020
if (!manager.middleware) manager.middleware = manager.getMiddleware?.();
21-
return ({ dispatch, getState }) => {
21+
return (api: ReduxMiddlewareAPI) => {
2222
if (i === 0) {
23-
(controller as any).dispatch = dispatch;
24-
(controller as any).getState = getState;
23+
controller.bindMiddleware(api);
2524
}
2625
// controller is a superset of the middleware API
2726
return (manager as Manager & { middleware: ReduxMiddleware }).middleware(
28-
controller as Controller<any>,
27+
controller as any,
2928
);
3029
};
3130
});

packages/endpoint/src/__tests__/endpoint.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import nock from 'nock';
22

3-
import type { default as TEndpoint, EndpointInstance } from '../endpoint';
3+
import type { default as TEndpoint } from '../endpoint';
44
import { EndpointInterface } from '../interface';
55
import Entity from '../schemas/Entity';
66

packages/react/src/__tests__/integration-garbage-collection.web.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,21 @@ import {
66
} from '@data-client/react';
77
import { MockResolver } from '@data-client/test';
88
import { render, screen, act } from '@testing-library/react';
9-
import { ArticleResource } from '__tests__/new';
9+
import { Article, ArticleResource } from '__tests__/new';
1010
import '@testing-library/jest-dom';
1111
import { useState } from 'react';
1212

1313
const mockGetList = jest.fn();
1414
const mockGet = jest.fn();
1515
const GC_INTERVAL = 5000;
1616

17-
const ListView = ({ onSelect }: { onSelect: (id: number) => void }) => {
18-
const articles = useSuspense(ArticleResource.getList);
17+
const ArticleList = ({
18+
articles,
19+
onSelect,
20+
}: {
21+
articles: Article[];
22+
onSelect: (id: number) => void;
23+
}) => {
1924
return (
2025
<div>
2126
{articles.map(article => (
@@ -26,18 +31,30 @@ const ListView = ({ onSelect }: { onSelect: (id: number) => void }) => {
2631
</div>
2732
);
2833
};
34+
const ArticleDetail = ({ article }: { article: Article }) => {
35+
return (
36+
<div>
37+
<h3>{article.title}</h3>
38+
<p>{article.content}</p>
39+
</div>
40+
);
41+
};
42+
43+
const ListView = ({ onSelect }: { onSelect: (id: number) => void }) => {
44+
const articles = useSuspense(ArticleResource.getList);
45+
return <ArticleList articles={articles} onSelect={onSelect} />;
46+
};
2947

3048
const DetailView = ({ id }: { id: number }) => {
3149
const article = useSuspense(ArticleResource.get, { id });
3250
const [toggle, setToggle] = useState(false);
3351

3452
return (
35-
<div>
36-
<h3>{article.title}</h3>
37-
<p>{article.content}</p>
53+
<>
54+
<ArticleDetail article={article} />{' '}
3855
<button onClick={() => setToggle(!toggle)}>Toggle Re-render</button>
3956
{toggle && <div>Toggle state: {toggle.toString()}</div>}
40-
</div>
57+
</>
4158
);
4259
};
4360

packages/react/src/components/__tests__/provider.native.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,10 @@ describe('<DataProvider />', () => {
8989

9090
it('should not change dispatch function on re-render', () => {
9191
let dispatch;
92+
let controller;
9293
let count = 0;
9394
function DispatchTester() {
95+
controller = useController();
9496
dispatch = useController().dispatch;
9597
count++;
9698
return null;
@@ -100,6 +102,7 @@ describe('<DataProvider />', () => {
100102
const { rerender } = render(tree);
101103
expect(dispatch).toBeDefined();
102104
let curDisp = dispatch;
105+
let curController = controller;
103106
rerender(tree);
104107
expect(curDisp).toBe(dispatch);
105108
expect(count).toBe(1);
@@ -110,6 +113,7 @@ describe('<DataProvider />', () => {
110113
rerender(<DataProvider managers={managers}>{chil}</DataProvider>);
111114
expect(count).toBe(1);
112115
curDisp = dispatch;
116+
curController = controller;
113117
rerender(<DataProvider managers={managers}>{chil}</DataProvider>);
114118
expect(curDisp).toBe(dispatch);
115119
expect(count).toBe(1);
@@ -120,22 +124,23 @@ describe('<DataProvider />', () => {
120124
{chil}
121125
</ControllerContext.Provider>,
122126
);
123-
expect(curDisp).not.toBe(dispatch);
127+
expect(curController).not.toBe(controller);
124128
expect(count).toBe(2);
125129
});
130+
126131
it('should change state', () => {
127-
let dispatch: any, state;
132+
let controller: any, state;
128133
let count = 0;
129134
function ContextTester() {
130-
dispatch = useController().dispatch;
135+
controller = useController();
131136
state = useContext(StateContext);
132137
count++;
133138
return null;
134139
}
135140
const chil = <ContextTester />;
136141
const tree = <DataProvider>{chil}</DataProvider>;
137142
render(tree);
138-
expect(dispatch).toBeDefined();
143+
expect(controller.dispatch).toBeDefined();
139144
expect(state).toBeDefined();
140145
const action: SetResponseAction = {
141146
type: SET_RESPONSE,
@@ -150,7 +155,7 @@ describe('<DataProvider />', () => {
150155
},
151156
};
152157
act(() => {
153-
dispatch(action);
158+
controller.dispatch(action);
154159
});
155160
expect(count).toBe(2);
156161
expect(state).toMatchSnapshot();

packages/react/src/components/__tests__/provider.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,10 @@ describe('<DataProvider />', () => {
8585

8686
it('should not change dispatch function on re-render', () => {
8787
let dispatch;
88+
let controller;
8889
let count = 0;
8990
function DispatchTester() {
91+
controller = useController();
9092
dispatch = useController().dispatch;
9193
count++;
9294
return null;
@@ -96,6 +98,7 @@ describe('<DataProvider />', () => {
9698
const { rerender } = render(tree);
9799
expect(dispatch).toBeDefined();
98100
let curDisp = dispatch;
101+
let curController = controller;
99102
rerender(tree);
100103
expect(curDisp).toBe(dispatch);
101104
expect(count).toBe(1);
@@ -106,6 +109,7 @@ describe('<DataProvider />', () => {
106109
rerender(<DataProvider managers={managers}>{chil}</DataProvider>);
107110
expect(count).toBe(1);
108111
curDisp = dispatch;
112+
curController = controller;
109113
rerender(<DataProvider managers={managers}>{chil}</DataProvider>);
110114
expect(curDisp).toBe(dispatch);
111115
expect(count).toBe(1);
@@ -116,7 +120,7 @@ describe('<DataProvider />', () => {
116120
{chil}
117121
</ControllerContext.Provider>,
118122
);
119-
expect(curDisp).not.toBe(dispatch);
123+
expect(curController).not.toBe(controller);
120124
expect(count).toBe(2);
121125
});
122126

0 commit comments

Comments
 (0)