- Notifications
You must be signed in to change notification settings - Fork85
Description
Hi, thank you for the inspiration and make this awesome libs. I have the following RFC proposed:
Suggestion
Remove the overlapping of state management inuseEventCallback
, as we haveuseObservable
,useState
, anduseReducer
already, making this hook just purely for the async event management. This hook is best to be treated as an adaptor to convert DOMEvent to DOMEvent stream.
Why
After using it for a while, I always thinkuseEventCallback
is not so intuitive to use. It accepts up to 3 arguments:callback
,initialState
, andinputs
. Why we need thisinitialState
if we want to introduce prop dependencies? At the current implementation, what this hook would returns is not just a memorized event handler as a callback, but a tuple of handler + the observed state, which makes me feel this hook is doing too much, IMHO.
Ideally, what I have in mind is to have a similar interface as in the the nativeuseCallback
. But what can make this hook so useful is to convert the event object to an observable. Then we can manage DOMEvent as a stream easily.
If we need to performance side effects, like to update state, we should usetap
operator instead. For example:
constdeps=/* some dependencies, could be state, or props */const[state,setState]=useState();constonClick=useEventCallback((event$,deps$)=>event$.pipe(filter((e)=>e.type==='click'),map(({ target})=>target.value),tap((v)=>setState(v))// * re-render explicitly requested by the user.withLatestFrom(deps$),/* do something with dependencies */),[deps]// optional)
Thedeps
would be any props or local state dependencies that is reactively emitted and passed as the second props to the callback. And we can think this like an epic inredux-observable
. This way we decouple the state management part out of this hook. And this hook is never going to trigger re-render by itself, it will have to be introduced by the user.
Implementation
Here is the suggested implementation:
constcreateEvent$=<T>()=>{returnnewSubject<T>();};constcreateDeps$=<Dextendsunknown[]>(deps:D)=>{returndeps.length ?newBehaviorSubject(deps) :undefined;};exportconstuseEventCallback=<E=any,Dextendsunknown[]=never>(callback:(event$:Observable<E>,deps$:Observable<D>)=>Observable<unknown>,deps:D=[]asnever)=>{constevent$=useMemo(()=>createEvent$<E>(),[]);constdeps$=useMemo(()=>createDeps$<D>(deps),[]);useEffect(()=>{constsubscriber=callback(event$,deps$asObservable<D>).subscribe();return()=>{subscriber.unsubscribe();event$.complete();if(deps$){deps$.complete();}};},[]);useEffect(()=>{if(deps$){deps$.next(deps);}},deps);returnuseCallback((e:E)=>event$.next(e),[]);};
And perhaps, it should be called asuseEvent$Callback
to make it more obvious what this hook is doing.