Microcosm

Effects

  1. Overview
  2. Creating Effects
  3. A quick example
  4. API

Overview

Not all actions result in updates to application state. For example:

Effect handlers fire immediately after Domain handlers and are only called once per action state. This means that a repo’s state is up to date with the latest state transitions by the time they execute.

Creating Effects

There are two ways to create an effect: as a class, and as a plain object. The usage is roughly the same for both versions, the class form can additionally take advantage of having a constructor.

Effects as classes

class Effect {
  setup (repo, options) {
    // Run startup behavior
  }
  teardown (repo) {
    // Clean up any setup behavior
  }
  handleAction (repo, payload) {
    // Respond once to an action
  }
  register () {
    return {
      [action] : this.handleAction
    }
  }
}

repo.addEffect(Effect)

Effects as plain objects

const Effect = {
  setup (repo, options) {
    // Run starting behavior
  },
  teardown (repo) {
    // Clean up
  },
  handleAction (repo, payload) {
    // Respond once to an action
  },
  register () {
    return {
      [action] : this.handleAction
    }
  }
}

repo.addEffect(Effect)

Microcosm calls Object.create on the simple object form, preventing any assignments within the Effect from polluting other instances. In this way, they are somewhat similar to the class form.

A quick example - query strings

URL persistence is important for shareability and wayfinding. However, a full routing solution isn’t always practical. On some projects we simply want to push search parameters or other meta data into a query string.

This is a perfect use case for an effect:

// /src/effects/location.js

import url from 'url'
import {patchQuery} from '../actions/query'

class Location {

  updateQuery (repo) {
    const { origin, hash } = window.location

    const location = url.format({
      host  : origin,
      query : repo.state.query,
      hash  : hash
    })

    window.history.pushState(null, null, location)
  }

  register () {
    return {
      [patchQuery] : this.updateQuery
    }
  }
}

export default Location

API

setup(repo, options)

Setup runs right after an effect is added to a Microcosm. It receives that repo and any options passed as the second argument.

teardown(repo)

Runs whenever Microcosm::teardown is invoked. Useful for cleaning up work done in setup().

register()

Returns an object mapping actions to methods on the effect. This is the communication point between a effect and the rest of the system.

// /src/effects/planets.js

import { addPlanet } from '../actions/planets'

class Planets {
  //...
  register () {
    return {
      [addPlanet]: this.alert
    }
  }

  alert (repo, planet) {
    alert('A planet was added! ' + planet.name)
  }
}

repo.addEffect(Planets)
repo.push(addPlanet, { name: 'earth' }) // this will add Earth

Multiple handlers for the same action

You can assign multiple handlers to an action by passing an array:

// /src/effects/planets.js

import { addPlanet } from '../actions/planets'

class Planets {
  //...
  register () {
    return {
      [addPlanet.error]: [this.alert, this.trackError]
    }
  }

  alert (repo, error) {
    alert('There was an issue!)
  }
  
  trackError (repo, error) {
    myLogService.trackError(error)
  }
}

repo.addEffect(Planets)
repo.push(addPlanet, { name: 'earth' })

Effect.defaults

Specifies default options a Effect is instantiated with. This provides a concise way to configure sensible defaults for setup options:

class AutoSave {
  static defaults = {
    saveInterval: 5000
  }

  setup (repo, { saveInterval }) {
    console.log(saveInterval) // 5000
  }
}

let repo = new Microcosm()

repo.addEffect(AutoSave) // default saveInterval is 5000

When instantiated, default options are determined in the following order:

  1. Microcosm defaults
  2. Microcosm instantiation options
  3. Effect defaults
  4. Instantiation options
  5. Options passed to repo.addEffect.