Skip to content

Instantly share code, notes, and snippets.

@nhunzaker
Last active October 5, 2017 20:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nhunzaker/fd3d6e827df49e21852e4699d72a59d8 to your computer and use it in GitHub Desktop.
Save nhunzaker/fd3d6e827df49e21852e4699d72a59d8 to your computer and use it in GitHub Desktop.

Next Microcosm

I've had a couple of things on my mind with regard to the next version of Microcosm.

A client-side version of GraphQL

I want to be able to build out a Microcosm state tree using GraphQL. A single document would automatically produce a fast, well documented, easily configurable Microcosm setup. For example:

type Author {
  id: ID
  name: String
}

type Post {
  id: ID
  title: String
  author: Author
}

type Comment {
  id: ID
  post: Post
  content: String
}

type UI {
  menuOpen: Boolean
}

type Query {
  author(id: ID): Author
  authors: [Author]
  comment(id: ID): Comment
  comments: [Comment]
  post(id: ID): Post
  posts: [Post]
  ui: UI
}

type Mutation {
  addAuthor(name: String): Author
  updateAuthor(id: ID, name: String): Author
  deleteAuthor(id: ID, name: String): Author
  addPost(title: String, author: ID): Post
  updatePost(id: ID, title: String, author: ID): Post
  deletePost(id: ID, title: String, author: ID): Post
  addComment(content: String, post: ID): Comment
  updateComment(id: ID, content: String, post: ID): Comment
  deleteComment(id: ID): Comment
}

This would produce a domain for Authors, Posts, Comments, a general UI domain, and a declaration of general Queries.

There are a lot of concepts in here that I think are worth thinking through

Fetching vs Mutating Data

In GraphQL there are 2 primary operators: queries and mutations. Queries are responsible for pulling down information from an API. Mutations are responsible for changing data.

Queries In Microcosm

Queries are distinct in that they are cached. Microcosm has no concept of a cache. I propose we make a distinction between actions that fetch data and actions that modify data. Additionally, we can provide dramatic performance gaurantees by caching not just networking requests, but expensive data calculation. I would love for the presentation layer of Microcosm apps to simply ask for data and maybe decorate on some display specific data. Increasing the boundary between data computation (which is easy to test) and the presentation layer (which is hard to test) will make our apps simpler.

In GraphQL, Queries are computed via resolvers. I believe that a Domain should be a set of these resolvers. We could still use the registration method to subscribe to mutations:

class Author {
  all(repo, params, cache) {
    return params.id in cache ? cache[params.id] : http('/authors')
  }

  find(repo, params, cache) {
    return cache ? cache : http('/authors/' + params.id)
  }
}

class Post {
  all(repo, params, cache) {
    return cache ? cache : http('/posts')
  }

  find(repo, params, cache) {
    return cache ? cache : http('/posts/' + params.id)
  }

  author(repo, params) {
    return repo.request('authors.find', { id: post.author })
  }
}

class Query {
  post(repo, params) {
    return repo.request('post.all', params)
  }

  posts(repo, params) {
    return repo.request('post.find', params)
  }

  authors(repo, params) {
    return repo.request('authors.all', params)
  }

  author(repo, params) {
    return repo.request('authors.find', params)
  }
}

repo.request(Post.post)

Using GraphQL, repo.request would get called a bunch:

query Posts {
  posts {
    id
    title
    author
  }
}
A cache

Domains would essentially turn into a data warehouse. repo.request would call a domain handler and the resulting value would be assigned to the cache. This is as far as I have this figured out

Mutations

Mutations would behave just like repo.push. Nothing changes, unless we determine that we should invalidate cache somehow.

getModel is out. GraphQL is in

Presenter:getModel works well if you're managing a big javascript object, but it has a couple of issues

  1. It is dynamic. Every time new props/state are sent into a Presenter, the model recalculates. GraphQL queries are static, passing in variables to configure specific sections of the query. This is very easy to optimize.
  2. Fetching data is hard. You must figure out in the presenter if you need to fetch new data based on props/state/model changes. We have two lifecycle methods (update and modelDidChange) that deal with this. I think this is a code smell. It is better for a Presenter to simply ask for data and have the underlying data layer determine if it really needs to fetch data.
  3. getModel bindings are hard to reuse. The current presenter essentially reduces down state into a subset. This is hard to compose and reuse. GraphQL queries can have secondary and teriary queries, which are easy to compose. For example: A post has an author, and an auther might have other posts you want to show in only a specific area of an app. Access to resources does not change, but the fields inside vary drastically between view components. GraphQL queries provide an easier approach: Just define the fields on a resource and allow the presentation layer developer to mix and match as they please.

What this might look like:

I'm proposing we get rid of this.model all together, assigning to this.state:

class Blog extends GraphPresenter {
  model = gql`{
    posts {
      id
      title
      author {
        name
      }
    }
  }

  renderItem(item) {
    return (
      <li key={item.id}>
        {item.title} by {item.author.name}
      </li>
    )
  }

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

    return <ul>{posts.map(this.renderItem, this)}</ul>
  }
}

I'm also debating if we even want this, or should explore the new render prop pattern that is emerging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment