-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
rxfx/react 1.0.1 useClosureListener, useClosureFilter
- Loading branch information
Showing
5 changed files
with
228 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from './useWhileMounted'; | ||
export * from './serviceHooks'; | ||
export * from './useClosureHandlers'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ;) | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters