Skip to content

Commit

Permalink
rxfx/react 1.0.1 useClosureListener, useClosureFilter
Browse files Browse the repository at this point in the history
  • Loading branch information
deanrad committed Nov 17, 2022
1 parent 0d0e19c commit bdc4730
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 12 deletions.
10 changes: 6 additions & 4 deletions react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rxfx/react",
"version": "1.0.0",
"version": "1.0.1",
"license": "MIT",
"author": "Dean Radcliffe",
"repository": "https://github.com/deanrad/rxfx",
Expand Down Expand Up @@ -43,6 +43,7 @@
"@testing-library/dom": "^8.18.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.4.3",
"@types/jest": ">= 27.0.1",
"@types/react": "^18.0.21",
Expand All @@ -59,8 +60,9 @@
"jest-silent-reporter": ">= 0.5.0",
"lodash.merge": ">= 4.6.2",
"prettier": ">= 2.6.2",
"react": ">= 18.2.0",
"react-dom": ">= 18.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-test-renderer": "^17.0.2",
"rimraf": ">= 3.0.2",
"rollup": ">= 2.56.2",
"size-limit": ">= 5.0.3",
Expand All @@ -72,7 +74,7 @@
"react": ">=16.6.0"
},
"prettier": {
"printWidth": 80,
"printWidth": 100,
"tabWidth": 2,
"semi": true,
"singleQuote": true,
Expand Down
1 change: 1 addition & 0 deletions react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './useWhileMounted';
export * from './serviceHooks';
export * from './useClosureHandlers';
58 changes: 58 additions & 0 deletions react/src/useClosureHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useEffect, useRef } from 'react';
import { last, Subscription } from 'rxjs';

import { defaultBus as bus, EffectObserver, EventHandler } from '@rxfx/bus';

export interface HandlerArgs<TMatchType> {
matches: {
match: ((i: TBusItem) => i is TMatchType) | ((i: TBusItem) => boolean);
};
}

export type TBusItem = any; // because defaultBus is type Bus<any>
export interface ListenerArgs<TConsequence, TMatchType extends TBusItem = TBusItem>
extends HandlerArgs<TMatchType> {
handler: EventHandler<TMatchType, TConsequence>;
observeWith?: EffectObserver<TConsequence>;
}
export interface FilterArgs<TMatchType extends TBusItem = TBusItem>
extends HandlerArgs<TMatchType> {
filter: (item: TMatchType) => TBusItem | null | undefined;
}

/**
* Registers a listener, but un-registers and re-registers it on any change of deps.
* Existing handlings will be terminated. This may run more often that you expect,
* given the nature of deps and unstable fucntions.
* @see https://reactjs.org/docs/hooks-faq.html#what-can-i-do-if-my-effect-dependencies-change-too-often
*/
export function useClosureListener<TConsequence, TMatchType extends TBusItem = TBusItem>(
args: ListenerArgs<TConsequence, TMatchType>,
deps: any[] = []
): Subscription {
const lastSub = useRef(new Subscription()); /* just to make .unsubscribe() safe */
useEffect(() => {
lastSub.current = bus.listen(args.matches.match, args.handler, args.observeWith);

return () => lastSub.current.unsubscribe();
}, deps);
return lastSub.current;
}

/**
* Registers a filters, but un-registers and re-registers it on any change of deps.
* This may run more often that you expect, given the nature of deps and unstable fucntions.
* @see https://reactjs.org/docs/hooks-faq.html#what-can-i-do-if-my-effect-dependencies-change-too-often
*/
export function useClosureFilter<TMatchType extends TBusItem = TBusItem>(
args: FilterArgs<TMatchType>,
deps: any[] = []
): Subscription {
const lastSub = useRef(new Subscription()); /* just to make .unsubscribe() safe */
useEffect(() => {
lastSub.current = bus.listen(args.matches.match, args.filter);

return () => lastSub.current.unsubscribe();
}, deps);
return lastSub.current;
}
74 changes: 74 additions & 0 deletions react/test/useClosureHandler.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { renderHook } from '@testing-library/react-hooks';
import { createEvent } from '@rxfx/service';
import { defaultBus } from '@rxfx/bus';
import { Subscription } from 'rxjs';
import { ListenerArgs, useClosureListener } from '../src/useClosureHandlers';

describe('useClosureListener', () => {
let errorSub: Subscription;
beforeAll(() => (errorSub = defaultBus.errors.subscribe(console.error)));
afterAll(() => errorSub?.unsubscribe());
beforeEach(() => jest.clearAllMocks());

const UPLOAD_COMPLETE = createEvent<void>('upload/complete');
const mockResultValue = 2;
const handlerSpy = jest.fn().mockResolvedValue(mockResultValue);
const nextSpy = jest.fn();
const handlerArgs = {
matches: UPLOAD_COMPLETE,
handler: handlerSpy,
observeWith: { next: nextSpy },
};

describe('First invocation', () => {
it('subscribes its listener the first time', async () => {
const { result } = renderHook(
(props = []) => {
useClosureListener<number>(...props);
},
{ initialProps: [handlerArgs, []] }
);
defaultBus.trigger(UPLOAD_COMPLETE());

expect(handlerSpy).toBeCalledWith(UPLOAD_COMPLETE());
await Promise.resolve();
expect(nextSpy).toBeCalledWith(mockResultValue);
});
});

describe('When deps change', () => {
it('subscribes the new listener instead', async () => {
const firstDeps = [1];
const secondDeps = [2];

const { result, rerender } = renderHook(
(props = []) => useClosureListener<number>(...props),
{ initialProps: [handlerArgs, firstDeps] }
);

// The first render will call through with the first handler value
defaultBus.trigger(UPLOAD_COMPLETE());
await Promise.resolve();
expect(nextSpy).toBeCalledWith(mockResultValue);

// The 2nd render will call through with the 2nd handler value - but not both
// since the subscription was unsubscribed!
rerender([
{
...handlerArgs,
handler: jest.fn().mockResolvedValue(3.14),
},
secondDeps,
]);
defaultBus.trigger(UPLOAD_COMPLETE());
await Promise.resolve();
expect(nextSpy.mock.calls).toEqual([[2], [3.14]]);
});
});
});

describe('useClosureFilter', () => {
it('works just like useClosureListener but for filters', () => {
expect.assertions(0); // trust me ;)
});
});
97 changes: 89 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2522,6 +2522,7 @@ __metadata:
"@testing-library/dom": ^8.18.1
"@testing-library/jest-dom": ^5.16.5
"@testing-library/react": ^13.4.0
"@testing-library/react-hooks": ^8.0.1
"@testing-library/user-event": ^14.4.3
"@types/jest": ">= 27.0.1"
"@types/react": ^18.0.21
Expand All @@ -2538,8 +2539,9 @@ __metadata:
jest-silent-reporter: ">= 0.5.0"
lodash.merge: ">= 4.6.2"
prettier: ">= 2.6.2"
react: ">= 18.2.0"
react-dom: ">= 18.2.0"
react: ^17.0.2
react-dom: ">= 17.0.2"
react-test-renderer: 17.0.2
rimraf: ">= 3.0.2"
rollup: ">= 2.56.2"
rxjs: ^7.3.0
Expand Down Expand Up @@ -2759,6 +2761,28 @@ __metadata:
languageName: node
linkType: hard

"@testing-library/react-hooks@npm:^8.0.1":
version: 8.0.1
resolution: "@testing-library/react-hooks@npm:8.0.1"
dependencies:
"@babel/runtime": ^7.12.5
react-error-boundary: ^3.1.0
peerDependencies:
"@types/react": ^16.9.0 || ^17.0.0
react: ^16.9.0 || ^17.0.0
react-dom: ^16.9.0 || ^17.0.0
react-test-renderer: ^16.9.0 || ^17.0.0
peerDependenciesMeta:
"@types/react":
optional: true
react-dom:
optional: true
react-test-renderer:
optional: true
checksum: 7fe44352e920deb5cb1876f80d64e48615232072c9d5382f1e0284b3aab46bb1c659a040b774c45cdf084a5257b8fe463f7e08695ad8480d8a15635d4d3d1f6d
languageName: node
linkType: hard

"@testing-library/react@npm:^13.4.0":
version: 13.4.0
resolution: "@testing-library/react@npm:13.4.0"
Expand Down Expand Up @@ -11280,7 +11304,7 @@ __metadata:
languageName: node
linkType: hard

"react-dom@npm:>= 18.2.0":
"react-dom@npm:>= 17.0.2, react-dom@npm:>= 18.2.0":
version: 18.2.0
resolution: "react-dom@npm:18.2.0"
dependencies:
Expand All @@ -11292,20 +11316,57 @@ __metadata:
languageName: node
linkType: hard

"react-is@npm:^17.0.1":
version: 17.0.2
resolution: "react-is@npm:17.0.2"
checksum: 9d6d111d8990dc98bc5402c1266a808b0459b5d54830bbea24c12d908b536df7883f268a7868cfaedde3dd9d4e0d574db456f84d2e6df9c4526f99bb4b5344d8
"react-error-boundary@npm:^3.1.0":
version: 3.1.4
resolution: "react-error-boundary@npm:3.1.4"
dependencies:
"@babel/runtime": ^7.12.5
peerDependencies:
react: ">=16.13.1"
checksum: f36270a5d775a25c8920f854c0d91649ceea417b15b5bc51e270a959b0476647bb79abb4da3be7dd9a4597b029214e8fe43ea914a7f16fa7543c91f784977f1b
languageName: node
linkType: hard

"react-is@npm:^18.0.0":
"react-is@npm:^16.12.0 || ^17.0.0 || ^18.0.0, react-is@npm:^18.0.0":
version: 18.2.0
resolution: "react-is@npm:18.2.0"
checksum: e72d0ba81b5922759e4aff17e0252bd29988f9642ed817f56b25a3e217e13eea8a7f2322af99a06edb779da12d5d636e9fda473d620df9a3da0df2a74141d53e
languageName: node
linkType: hard

"react-is@npm:^17.0.1, react-is@npm:^17.0.2":
version: 17.0.2
resolution: "react-is@npm:17.0.2"
checksum: 9d6d111d8990dc98bc5402c1266a808b0459b5d54830bbea24c12d908b536df7883f268a7868cfaedde3dd9d4e0d574db456f84d2e6df9c4526f99bb4b5344d8
languageName: node
linkType: hard

"react-shallow-renderer@npm:^16.13.1":
version: 16.15.0
resolution: "react-shallow-renderer@npm:16.15.0"
dependencies:
object-assign: ^4.1.1
react-is: ^16.12.0 || ^17.0.0 || ^18.0.0
peerDependencies:
react: ^16.0.0 || ^17.0.0 || ^18.0.0
checksum: 6052c7e3e9627485120ebd8257f128aad8f56386fe8d42374b7743eac1be457c33506d153c7886b4e32923c0c352d402ab805ef9ca02dbcd8393b2bdeb6e5af8
languageName: node
linkType: hard

"react-test-renderer@npm:17.0.2":
version: 17.0.2
resolution: "react-test-renderer@npm:17.0.2"
dependencies:
object-assign: ^4.1.1
react-is: ^17.0.2
react-shallow-renderer: ^16.13.1
scheduler: ^0.20.2
peerDependencies:
react: 17.0.2
checksum: e6b5c6ed2a0bde2c34f1ab9523ff9bc4c141a271daf730d6b852374e83acc0155d58ab71a318251e953ebfa65b8bebb9c5dce3eba1ccfcbef7cc4e1e8261c401
languageName: node
linkType: hard

"react@npm:>= 18.2.0":
version: 18.2.0
resolution: "react@npm:18.2.0"
Expand All @@ -11315,6 +11376,16 @@ __metadata:
languageName: node
linkType: hard

"react@npm:^17.0.2":
version: 17.0.2
resolution: "react@npm:17.0.2"
dependencies:
loose-envify: ^1.1.0
object-assign: ^4.1.1
checksum: b254cc17ce3011788330f7bbf383ab653c6848902d7936a87b09d835d091e3f295f7e9dd1597c6daac5dc80f90e778c8230218ba8ad599f74adcc11e33b9d61b
languageName: node
linkType: hard

"readable-stream@npm:1 || 2, readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.1.5, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.6, readable-stream@npm:~2.3.6":
version: 2.3.7
resolution: "readable-stream@npm:2.3.7"
Expand Down Expand Up @@ -11856,6 +11927,16 @@ __metadata:
languageName: node
linkType: hard

"scheduler@npm:^0.20.2":
version: 0.20.2
resolution: "scheduler@npm:0.20.2"
dependencies:
loose-envify: ^1.1.0
object-assign: ^4.1.1
checksum: c4b35cf967c8f0d3e65753252d0f260271f81a81e427241295c5a7b783abf4ea9e905f22f815ab66676f5313be0a25f47be582254db8f9241b259213e999b8fc
languageName: node
linkType: hard

"scheduler@npm:^0.23.0":
version: 0.23.0
resolution: "scheduler@npm:0.23.0"
Expand Down

0 comments on commit bdc4730

Please sign in to comment.