Events are the shit

Pascal Schilp - Jul 26 '23 - - Dev Community

Pardon my profanity, there's just no better way to say it. Events are just great. In this blog I'll showcase some cool things that you can achieve with just plain old events. You might not need an expensive or heavy library! Try an event.

Use EventTarget

Did you know you can instantiate EventTargets?

const target = new EventTarget();

target.dispatchEvent(new Event('foo'));
target.addEventListener('foo', (event) => {});
Enter fullscreen mode Exit fullscreen mode

Extend Event!

Did you know you can extend Event?

Instead of creating CustomEvents, you can just extend the Event class, and assign data to it, or even implement other methods on it:

// Create the event:
class MyEvent extends Event {
  constructor(data) {
    super('my-event', { bubbles: true, composed: true });
    this.data = data;
  }
}

const target = new EventTarget();

// Fire the event:
target.dispatchEvent(new MyEvent({foo: 'bar'}));

// Catch the event:
target.addEventListener('my-event', ({data}) => {
  console.log(data); // { foo: 'bar' }
});
Enter fullscreen mode Exit fullscreen mode

Extend EventTarget!

Did you know you can also extend EventTarget?

Here's how you can create a super minimal state manager using events:

class StateEvent extends Event {
  constructor(state = {}) {
    super('state-changed');
    this.state = state;
  }
}

export class State extends EventTarget {
  #state;

  constructor(initialState) {
    super();
    this.#state = initialState;
  }

  setState(state) {
    this.#state = typeof state === 'function' ? state(this.#state) : structuredClone(state);
    this.dispatchEvent(new StateEvent(this.#state));
  }

  getState() {
    return this.#state;
  }
}

export const state = new State({});
Enter fullscreen mode Exit fullscreen mode

And then you can use it like:

state.setState({foo: 'bar'}); // #state === {foo: 'bar'}
state.setState((old) => ({...old, bar: 'baz'})); // #state === {foo: 'bar', bar: 'baz'}

state.addEventListener('state-changed', ({state}) => {
  // Assign state, trigger a render, whatever
});

state.getState(); // {foo: 'bar', bar: 'baz'};
Enter fullscreen mode Exit fullscreen mode

I use this in my @thepassle/app-tools library, and it's often all the state management I need. Super tiny, but powerful state manager.

Events are sync

Did you know events execute synchronously?

const target = new EventTarget();

console.log('first');

target.addEventListener('foo', ({data}) => {
  console.log('second');
});

target.dispatchEvent(new Event('foo'));

console.log('third');
Enter fullscreen mode Exit fullscreen mode

Outputs:

// first
// second
// third
Enter fullscreen mode Exit fullscreen mode

Context-like patterns

It's a common scenario to pass down properties to child components. However, sometimes you end up in a situation known as "prop drilling", where you need to get some property down to a deeply nested child component, and along the way you're passing the property through components that really don't need to know about the property in the first place. In this case, it can sometimes be easier for the child component to request the property from a parent higher up the tree. This is also known as the context pattern. Since events execute synchronously, we can just use the following pattern:

class MyParent extends HTMLElement {
  theme = 'dark';

  constructor() {
    super();
    /**
     * The provider: 
     */
    this.addEventListener('theme-context', (event) => {
      event.theme = this.theme;
    });
  }
}

export class MyChild extends HTMLElement {
  connectedCallback() {
    const event = new Event('theme-context', {
      bubbles: true, 
      composed: true, 
    });
    this.dispatchEvent(event);

    /**
     * Because events execute synchronously, the callback for `'theme-context'`
     * event executes first, and assigns the `theme` to the `event`, which we
     * can then access in the child component
     */
    console.log(event.theme); // 'dark';
  } 
}
Enter fullscreen mode Exit fullscreen mode

Promise-carrying events

Did you know events can also carry promises? A great showcase of this pattern is the Pending Task Protocol by the Web Components Community Group. Now, "Pending Task Protocol" sounds very fancy, but really, it's just an event that carries a promise.

Consider the following example, we create a new PendingTaskEvent class:

class PendingTaskEvent extends Event {
  constructor(complete) {
    super('pending-task', {bubbles: true, composed: true});
    this.complete = complete;
  }
}
Enter fullscreen mode Exit fullscreen mode

And then in a child component, whenever we do some asynchronous work, we can send a new PendingTaskEvent to signal to any parents that a task is pending:

class ChildElement extends HTMLElement {
  async doWork() { /* ... */ }

  startWork() {
    const workComplete = this.doWork();
    this.dispatchEvent(new PendingTaskEvent(workComplete));
  }
}
Enter fullscreen mode Exit fullscreen mode

In our parent component we can then catch the event, and show/hide a loading state:

class ParentElement extends HTMLElement {
  #pendingTaskCount = 0;

  constructor() {
    super();
    this.addEventListener('pending-task', async (e) => {
      e.stopPropagation();
      if (++this.#pendingTaskCount === 1) {
        this.showSpinner();
      }
      await e.complete;
      if (--this.#pendingTaskCount === 0) {
        this.hideSpinner();
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player