Using react hooks and EventEmitter

Time:2021-1-27

Using react hooks and EventEmitter

Our blog is at: https://blog.rxliuli.com/p/43… Welcome to play!

scene

EventEmitter is very suitable for component communication without modifying the component state structure. However, its life cycle is not managed by react, so it is troublesome to manually add / clean up listening events. Moreover, if an EventEmitter is not used, it will be a bit troublesome to be initialized.

objective

So the purpose of using react hooks combined with event emitter is

  • Add high-level components and inject EM objects into all sub components through react context
  • Add custom hooks, get the emitter object from react context, and expose the appropriate function.
  • Automatically clean up the emitter object and the emitter listener.

realization

Implement the basic event emitter

First of all, we need to implement a basic event emitter, which we have implemented before, so we will take it directly.

type EventType = string | number

export type BaseEvents = Record<EventType, any[]>

/**
 *Event bus
 *In fact, it is a simple implementation of publish subscribe mode
 *The type definition is restricted by {@ link} https://github.com/andywer/typed-emitter/blob/master/index.d.ts }But you just need to declare the parameter, and you don't need to return the value (it should be {@ code void})
 */
export class EventEmitter<Events extends BaseEvents> {
  private readonly events = new Map<keyof Events, Function[]>()

  /**
   *Add an event listener
   *@ param type listening type
   *@ param callback processing callback
   * @returns {@code this}
   */
  add<E extends keyof Events>(type: E, callback: (...args: Events[E]) => void) {
    const callbacks = this.events.get(type) || []
    callbacks.push(callback)
    this.events.set(type, callbacks)
    return this
  }
  /**
   *Remove an event listener
   *@ param type listening type
   *@ param callback processing callback
   * @returns {@code this}
   */
  remove<E extends keyof Events>(
    type: E,
    callback: (...args: Events[E]) => void,
  ) {
    const callbacks = this.events.get(type) || []
    this.events.set(
      type,
      callbacks.filter((fn: any) => fn !== callback),
    )
    return this
  }
  /**
   *Remove a class of event listeners
   *@ param type listening type
   * @returns {@code this}
   */
  removeByType<E extends keyof Events>(type: E) {
    this.events.delete(type)
    return this
  }
  /**
   *Trigger a kind of event listener
   *@ param type listening type
   *@ param args the parameters needed to process the callback
   * @returns {@code this}
   */
  emit<E extends keyof Events>(type: E, ...args: Events[E]) {
    const callbacks = this.events.get(type) || []
    callbacks.forEach((fn) => {
      fn(...args)
    })
    return this
  }

  /**
   *Get a kind of event listener
   *@ param type listening type
   *@ returns is a read-only array. If it cannot be found, it will return an empty array {@ code []}
   */
  listeners<E extends keyof Events>(type: E) {
    return Object.freeze(this.events.get(type) || [])
  }
}

Implement a package component with context

The purpose of the package component is to provide a package component directly and provide the default value of the provider without the user’s direct contact with the emitter object.

import * as React from 'react'
import { createContext } from 'react'
import { EventEmitter } from './util/EventEmitter'

type PropsType = {}

export const EventEmitterRCContext = createContext<EventEmitter<any>>(
  null as any,
)

const EventEmitterRC: React.FC<PropsType> = (props) => {
  return (
    <EventEmitterRCContext.Provider value={new EventEmitter()}>
      {props.children}
    </EventEmitterRCContext.Provider>
  )
}

export default EventEmitterRC

Exposing the emitter API with hooks

There are only two main APIs we need to expose

  • useListener: add a listener, and use hooks to automatically clean up the listener function when the component is unloaded
  • emit: trigger the listener and call it directly
import { DependencyList, useCallback, useContext, useEffect } from 'react'
import { EventEmitterRCContext } from '../EventEmitterRC'
import { BaseEvents } from '../util/EventEmitter'

function useEmit<Events extends BaseEvents>() {
  const em = useContext(EventEmitterRCContext)
  return useCallback(
    <E extends keyof Events>(type: E, ...args: Events[E]) => {
      console.log('emitter emit: ', type, args)
      em.emit(type, ...args)
    },
    [em],
  )
}

export function useEventEmitter<Events extends BaseEvents>() {
  const emit = useEmit()
  return {
    useListener: <E extends keyof Events>(
      type: E,
      listener: (...args: Events[E]) => void,
      deps: DependencyList = [],
    ) => {
      const em = useContext(EventEmitterRCContext)
      useEffect(() => {
        console.log('emitter add: ', type, listener)
        em.add(type, listener)
        return () => {
          console.log('emitter remove: ', type, listener)
          em.remove(type, listener)
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, [listener, type, ...deps])
    },
    emit,
  }
}

use

It’s very easy to use. Wrap aEventEmitterRCComponent, and then you can use ituseEventEmitterIt’s too late.

The following is a simple todo example, which uses twitter to realize the communication between todo form and todo list.

The directory structure is as follows

  • todo

    • component

      • TodoForm.tsx
      • TodoList.tsx
    • modal

      • TodoEntity.ts
      • TodoEvents.ts
    • Todo.tsx

Todo parent component, usingEventEmitterRCPackage subassemblies

const Todo: React.FC<PropsType> = () => {
  return (
    <EventEmitterRC>
      <TodoForm />
      <TodoList />
    </EventEmitterRC>
  )
}

Use in form componentsuseEventEmitterHooks getemitMethod, and then trigger todo when you add it.

const TodoForm: React.FC<PropsType> = () => {
  const { emit } = useEventEmitter<TodoEvents>()

  const [title, setTitle] = useState('')

  function handleAddTodo(e: FormEvent<HTMLFormElement>) {
    e.preventDefault()
    emit('addTodo', {
      title,
    })
    setTitle('')
  }

  return (
    <form onSubmit={handleAddTodo}>
      <div>
        < label htmlfor = {title '} > Title: < / label >
        <input
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          id={'title'}
        />
        < button type = {'submit'} > Add < / button >
      </div>
    </form>
  )
}

Use in list componentsuseEventEmitterHooks getuseListenerHooks, and then listens for events that add todo.

const TodoList: React.FC<PropsType> = () => {
  const [list, setList] = useState<TodoEntity[]>([])
  const { useListener } = useEventEmitter<TodoEvents>()
  useListener(
    'addTodo',
    (todo) => {
      setList([...list, todo])
    },
    [list],
  )
  const em = { useListener }
  useEffect(() => {
    console.log('em: ', em)
  }, [em])
  return (
    <ul>
      {list.map((todo, i) => (
        <li key={i}>{todo.title}</li>
      ))}
    </ul>
  )
}

Here are some typescript types

export interface TodoEntity {
  title: string
}
import { BaseEvents } from '../../../components/emitter'
import { TodoEntity } from './TodoEntity'

export interface TodoEvents extends BaseEvents {
  addTodo: [TodoEntity]
}

reference resources

  • Building event emitter using react hooks
  • NodeJS EventEmitter API