Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useClosureListener and useClosureFilter #3

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,3 +1,4 @@
export * from './useWhileMounted';
export * from './serviceHooks';
export * from './Unmounter';
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.filter(args.matches.match, args.filter);

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

const UPLOAD_COMPLETE = createEvent<void>('upload/complete');

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

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', () => {
const filterSpy = jest.fn();
const { result } = renderHook(
(props = []) => {
useClosureFilter<number>(...props);
},
{ initialProps: [{ matches: UPLOAD_COMPLETE, filter: filterSpy }, []] }
);
defaultBus.trigger(UPLOAD_COMPLETE());
expect(filterSpy).toHaveBeenCalledWith(UPLOAD_COMPLETE());
});
});
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