Imperative React Patterns II - Implementing useListener & useDispatcher with RxJS

July 02, 2023

A cozy image from Midjourney
A cozy image from Midjourney.

As RxJS provides rich tooling for event-driven implementations, I've decided to give it a try by implementing the previous example of action handling hooks (useListener and useDispatcher):

export const createAction = () => {
  const subject = new Subject()

  const useListener = (callback, operator) => {
    const operatedSubject = useMemo(() => {
      return subject.pipe(operator)
    }, [])

    useEffect(() => {
      const subscription =
        operatedSubject.subscribe(callback)
      return () => subscription.unsubscribe()
    }, [callback])
  }

  const useDispatcher = () => {
    return payload => subject.next(payload)
  }

  return { useListener, useDispatcher }
}

export const createComponentAction = () => {
  const context = createContext()
  const useDispatcher = payload =>
    useContext(context).useDispatcher(payload)
  const useListener = (fn, operator) =>
    useContext(context).useListener(fn, operator)
  const EventProvider = ({ children }) => {
    const createActionRef = useRef(createAction())

    return (
      <context.Provider value={createActionRef.current}>
        {children}
      </context.Provider>
    )
  }

  const withAction = Component => props =>
    (
      <EventProvider>
        <Component {...props} />
      </EventProvider>
    )

  return {
    useDispatcher,
    useListener,
    EventProvider,
    withAction,
  }
}

Since we have access to RxJS operators (such as throttleTime, debounceTime or take etc.), we can do lot more with our actions:

import { debounceTime } from "rxjs"

const AComponentNestedDeepInDomTree = () => {
  useListener(action => {
    console.log("action ", action)
  }, debounceTime(3000))

  return <div>nested</div>
}

(Try it on Sandbox)


Copyright © 2023