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 unloadedemit
: 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 aEventEmitterRC
Component, and then you can use ituseEventEmitter
It’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, usingEventEmitterRC
Package subassemblies
const Todo: React.FC<PropsType> = () => {
return (
<EventEmitterRC>
<TodoForm />
<TodoList />
</EventEmitterRC>
)
}
Use in form componentsuseEventEmitter
Hooks getemit
Method, 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 componentsuseEventEmitter
Hooks getuseListener
Hooks, 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