Skip to content
/ spokes Public

Pub/sub to coordinate events such as webpage analytics with SPAs (React, GTM, Segment)

Notifications You must be signed in to change notification settings

joelvh/spokes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

79 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Spokes.js

Spokes.js facilitates coordinating lifecycle events between webpage lifecycle and the lifecycles of single-page applications (SPAs) or third party analytics platforms (e.g. Google Tag Manager's data layer).

  • Pub/sub events allow for the basis of coordinating events and sharing data between components. Events are published to topics that can be subscribed to.
  • State change events are published to indicate when global state data is updated.
  • Lifecycle events can be defined for each set of events that you want to track and coordinate. By default, webpage (Page) lifecycle events are automatically processed on page load and pub/sub events are published to notify about Loaded and QueryStringParsed to hook into.
  • Promises allow resolving lifecycle events, even if they've already ocurred, in order to retrieve the related data as needed. Note, however, that lifecycle events arre also published and can be subscribed to as-they-happen with pub/sub.

Install

Run npm install spokes to add this package to your project (package.json).

Development

  • Run yarn install to get all dependencies installed.

Publish to NPM

  • Commit all changes
  • Run yarn publish to specify a new version and automatically create a commit, tag and publish (npm publish will increment the patch without prompting for a version)

Spokes + React

Share global state and pubsub events between your webpage and your React app. Use the HOC (higher order component) and hooks from spokes-react to share a Spokes instance.

TODO

  • Polyfills for ES6 and other browser things that may not be supported by all (e.g. Promise)
  • Add GTM Data Layer integration
  • Add common analytics event integration (e.g. track and identify)
  • Evaluate if we use URLSearchParams (and related polyfill) rather than the ValueStack internals
  • Evaluate if we use Map (and related polyfill) rather than the List internals

Getting Started

Setup is pretty straight forward. You need an instance of Spokes that is shared by all components that need to communicate with each other.

import Spokes from 'Spokes';
import Page from 'Spokes/lifecycle/Page';
import window from 'Spokes/dom/window';
import document from 'Spokes/dom/document';

// Create a global instance to use by your SPA or analytics data layer
const spokes = new Spokes();

// Register the Page lifecycle, which hooks into pub/sub and the ability for
// components to resolve the result of specific page events (`Loaded` and `QueryStringParsed`)
spokes.registerLifecycle('Page', lifecycle => new Page(document).load(lifecycle));

// You could make this a global variable
window._spokes = spokes;

Publish/Subscribe Events

You can easily coordinate events and data between components with publish/subscribe. Events are published to topics, and you can subscribe to topics. Within the payload value are topic, key and value.

// Subscribe to the `Page` lifecycle topic and filter on the load event that
// Spokes emits automatically. Note that the last argument is a
// `Subscription` instance, which allows you to call
// `subscription.unsubscribe()` to unsubscribe.
spokes.subscribe('Page', ({ key, value }, subscription) => {
  if (key == 'Loaded') console.log('Page:Loaded fired', value);
});

// You can also subscribe and receive the last event published to the
// topic in case you missed it, using  the `withLastEvent` option.
spokes.subscribe('Page', ({ key, value }) => {
  if (key == 'Loaded') console.log('Page:Loaded fired', value);
}, { withLastEvent: true });

// Alternatively, subscribe to all topics. In this case,
// the original event is nested inside the payload's `value` property.
spokes.subscribeAll((payload) => {
  const { topic, key, value } = payload.value;
  console.log(topic, key, value);
});

You can publish events to topics to notify other components of what's occurred.

// You can publish to a topic with  an  event name and a data value.
spokes.publish('Some Topic', 'UserRegistered', { name: 'John Doe', email: 'john@doe.com', phone: '123-456-7890' });

Global State & State Changes

It's simple to manage global state by utilizing a key/value store. Any key that is added or value that is updated emits an event to the StateChanged topic.

// Subscribe to changes to global state and filter on `UserProfile`.
spokes.subscribe('StateChanges', ({ key, value }) => {
  if (key == 'UserProfile') console.log('User info changed', value);
});

// Update the global state, which will publish an event to the
// `StateChanged` topic.
spokes.setState('UserProfile', { name: 'John Doe', email: 'john@doe.com' });

// Get the latest value.
const userProfile = spokes.getState('UserProfile');

Lifecycle Events

Lifecycle events differ slightly from publish/subscribe events because they are triggered once. These events are registered and then resolved using a Promise. No matter when you "subscribe" to the event, you'll receive a value whether the value was resolved previously or has yet to be resolved. Note, however, that these events are also published with pub/sub when resolved the first time, for any pub/sub subscribers of the event. (Pub/sub is the underlying event handling that drives Spokes.js.)

Each lifecycle has it's own pub/sub topic. Built-in lifecycle Page topic has events Loaded and QueryStringParsed. Lifecycles are registered by name (e.g. Page) and events like these are registered to that lifecycle. The default lifecycle for the page is encapsulated in new Page() and registered with a Lifecycle instance.

// As seen above, we can register a lifecycle like this, by
// giving it a name and providing a factory method to setup a
// `Lifecycle` instance by registering events (e.g. `Loaded`).
spokes.registerLifecycle('Page', lifecycle => new Page(document).load(lifecycle));

// Within that `registerLifecycle` method, the following types of things are done:

// You may want to know when the page is loaded. This can be called
// at any time and the value will be passed to the callback when it's
// available. This is different from subscribing to pub/sub because
// if you subscribe to a pub/sub topic too late, you may miss events.
// Note: `when` returns a Promise, for which you can call `then` or  `catch`.
spokes.when('Page', 'Loaded').then(document => console.log('Page:Loaded resolved'));

// Here's an example of registering a lifecycle event, such as telling other components
// when your SPA is loaded and  ready.
spokes.registerLifecycle('SPA', lifecycle => {
  lifecycle.registerEvent('Initialized', (resolve, reject) => {
    // Do whatever needs to be done to finish loading your SPA,
    // then trigger `resolve` to pass back a payload to subscribers.
    // If there is a failure, call `reject`.
    if (success) {
      resolve(/*...*/);
    } else {
      reject(/*...*/);
    }
  });
});

// Another ecommerce-related lifecycle event might be `CheckoutCompleted`,
// if it doesn't happen more than once per page load.
spokes.registerLifecycle('Ecommerce', lifecycle => {
  lifecycle.registerEvent('CheckoutCompleted', (resolve, reject) => { /*...*/ });
});