vigetlabs/microcosm

WIP: Microcosm 13.x Design Plan

Closed this issue · 2 comments

Based on a discussion @efatsi and I had at our team offsite, I wanted to better document the plan for Microcosm 13.x. I still need to flesh a bunch of stuff out in this issue, but here's a first pass:

Overall Design

                                          [User]
                                            │
         ┌─────→ [Query] ───────────→ [Application]
         │         │ │                      │ 
         │         │ │                      │
   [subscription] ←┘ └────→ [Fetch]     [Command]
         ↑                    │             │
         │                    └→ [Actions] ←┘
         │                           ↓
         │                ┌┄[STATE MANAGEMENT]┄┐
         │                ╎      [History]     ╎
         │                ╎          ↓         ╎
 [State, Changeset] ←──── ╎       [Repo]       ╎
                          ╎          ↓         ╎
                          ╎      [Domains]     ╎
                          └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘

Domains store changesets

At the lowest level, I'd like to increase the abstraction level of domains. Instead of manipulating data directly, domains will return a changeset:

import { toggleMenu } from '../actions/user'

const Settings = {
  getInitialState() {
    return {  menuOpen: false  }
  },
  toggleMenu(changeset) {
    return changeset.update('menuOpen', open => !open)
  },
  register() {
    return {
      toggleMenu: this.toggleMenu
    }
  }
}

So the signature has changed. The first argument of a Domain will be a changeset object with methods similar to our data helpers: set, merge, update, etc.

This creates a changeset. Under the hood, Microcosm will know precisely that settings.menuOpen changed. When Microcosm sends out an update, consumers can subscribe to precisely the data paths they care about. It also means that users don't have to worry about immutable data updates. We can do that for them.

Queries (and subscriptions)

We've wanted a formal query API in Microcosm for a while. I think GraphQL offers a great way to go about this. The goal is not to support GraphQL backends from the get-go, but it would be a good milestone for later.

import gql from 'graphql-tag'

class Planets extends Presenter {
  query = gql`{
    planets {
      id
      name
      star {
        name
      }
    }
  }` 

  render() {
    const { planets } = this.state.model

    return (
      <ul>{ planets.map(planet => <li>{planet.name}</li>}</ul>
    )
  }
}

From this query we can infer a few things. This presenter cares about the planets and stars data paths. If a domain modifies those paths, we know to update this query. In the future, we could even go so far as to subscribe to changes to a specificplanet.id, planet.name, or star.name update.

Fetching vs Mutating in Domains

Over time I've found that fetching data and modifying client-side state are different flows. When fetching data, we always end up just passing it through to state, or merging the payload into existing records. This results in a lot of unnecessary boilerplate.

Fetching

Fetching data and storing it in a domain should be streamlined:

import http from 'microcosm-http'

const PlanetsDomain = {
  all(args) {
    return http.get('/planets', args)
  }
}

let repo = new Repo()

repo.addDomain('planets', PlanetsDomain)

let subscription = repo.fetch(PlanetsDomain.all, { page: 1 })

repo.fetch returns a subscription to a pool of data, whereas repo.push returns a subscription to the state of an action. If we ask for page 1 of all Planets records, it merges the request payload into a greater pool of all planets, returning a subscription to the records from the AJAX request for page 1.

For example, with repo.fetch(PlanetsDomain.all, { page: 1 }):

  1. Data is fetched
  2. The payload is [{ id: 0, name: 'Mercury' }]
  3. Mercury is merged into all planets
  4. repo.fetch returns a subscription to Mercury.
  5. As other actions change Mercury, the subscription emits a change, telling queries to push new data into presenters

Super exciting to see this getting fleshed out Nate! I know we discussed over Slack for a bit, but I've got a few questions for ya:

  1. I get the 'State Management' part of the chart from Actions down to Domains, but could you briefly explain the other pieces and how they relate to the Presenter? I'm still hazy on [Command] and [Fetch] but it would be good to get a short description of each piece.

  2. Client-side GraphQL querying looks very promising. Straight off the bat I can see how this would significantly increase readability of model subscriptions (especially with nested structures). Just to prod a little further, are there any performance benefits to this as well, or is it to simply improve the ergonomics of getting model data in the Presenter layer?

  3. New signature for Domains are 👌. Just to clarify, in the toggleMenu method in the code sample you provided, is open a variable passed through the Action payload?

  4. I also noticed that you are not using computed property names for domain registration. Is there a reason for that?

  1. A [Command] is basically a function that gets passed into repo.push, it's the job that an action conducts. This concept is very board, [Fetch] represents a new specialized type of command purely for fetching data.
  2. There are significant performance benefits. We can memoize components of a query, sharing computation between different Presenters that need the same question answered. We can also use queries to create much finer-grain subscriptions to data changes.
  3. open is the prior state of menuOpen. I think it would be neat to let a user pass a function to that let's them operate on the prior state.
  4. Ya. In the case of repo.fetch, we're using a method on a Domain as an action. I'm thinking that we could tag that method and automatically register it to the Domain (because the dispatch process can see that the tag is a member of the domain)