I've had a couple of things on my mind with regard to the next version of Microcosm.
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
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 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
}
}
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 would behave just like repo.push
. Nothing changes, unless we determine that we should invalidate cache somehow.
Presenter:getModel
works well if you're managing a big javascript object, but it has a couple of issues
- 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.
- 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
andmodelDidChange
) 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. - 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.
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.