Skip to main content

Publish-Subscribe Event System

Reactodia uses a lightweight EventEmitter-like publish-subscribe mechanism to connect different components and observe changes to the state.

Observing events

An observable instance in the library typically expose an events property implementing an Events interface. Listeners can be attached directly to the Events or subscribed via EventObserver to make it easy to unsubscribe:

const {model} = Reactodia.useWorkspace();

const onChangeSelection = () => {
console.log('New selection:', model.selection);
};
// Subscribe to the selection change event
model.events.on('changeSelection', onChangeSelection);
// Unsubscribe, must pass the same callback
model.events.off('changeSelection', onChangeSelection);

const observer = new Reactodia.EventObserver();
// Subscribe to the language change event
observer.listen(model.events, 'changeLanguage', ({previous}) => {
console.log(`Changed language from ${previous} to ${model.language}`);
});
// Unsubscribe from all events added via listen() by the observer
observer.stopListening();

It is possible to listen for all events on an instance by using Events.onAny() or EventObserver.listenAny():

function Component() {
const {model} = React.useWorkspace();
React.useEffect(() => {
const element = model.getElement(elementId);
const observer = new Reactodia.EventObserver();
observer.listenAny(element, ({data}) => {
if (data.requestedFocus || data.requestedRedraw) {
console.log('Element requested something');
}
});
return () => observer.stopListening();
}, [elementId]);
// ...
}

Using React hooks to listen to events

In case of change-like events it is recommended to use useObservedProperty() to observe current value:

const {editor} = Reactodia.useWorkspace();
// Subscribe to editor.authoringState changes
const authoringState = Reactodia.useObservedProperty(
editor.events, 'changeAuthoringState', () => editor.authoringState
);

Alternatively it is possible to use a combination of useEventStore() and either a React built-in useSyncExternalStore() or a compatibility shim Reactodia.useSyncStore() for more control over subscription:

import { useSyncExternalStore } from 'react';

function Component() {
const {editor} = Reactodia.useWorkspace();
const eventStore = Reactodia.useEventStore(
editor.events, 'changeAuthoringState'
);
const debouncedStore = Reactodia.useFrameDebouncedStore(eventStore);
const authoringState = useSyncExternalStore(
debouncedStore, () => editor.authoringState
);
// ...
}

In the above example, Reactodia.useFrameDebouncedStore() hook is used to debounce React component updates due to triggered events from the event store to only once each rendered frame based on requestAnimationFrame().

Making an observable

To create an observable instance it would be enough to implement the Events interface. An easiest way to do it would be to use EventSource:

// Declare event types
interface MyObservableEvents {
changeTitle: Reactodia.PropertyChange<MyObservableThing, string>;
notification: {
readonly status: 'normal' | 'error';
readonly message: string;
};
}

class MyObservableThing {
// Create an event source
private readonly source = new EventSource<MyObservableEvents>();
readonly events: Events<MyObservableEvents> = this.source;

// ...

setTitle(title: string) {
const previous = this._title;
if (previous !== title) {
this._title = title;
// Trigger change event
this.source.trigger('changeTitle', {source: this, previous});
}
}

private handleNotification(status: 'normal' | 'error', message: string): void {
// Trigger another event
this.source.trigger('notification', {status, message});
}
}
tip

EventSource implements EventTrigger interface which can be used as a separate type, e.g. a combination of Events<T> & EventTrigger<T> can be used as an "event bus" to trigger and listen for events at the same time.