Mobx-state-tree: Investigate integrated graphQL support

Created on 24 Jan 2019  路  12Comments  路  Source: mobxjs/mobx-state-tree

Empty currently, but a placeholder issue to gather ideas, requirements, formulate a plan, etc :). (probably this should become a separate library or middleware)

So please chime in with ideas, (pieces) of existing projects, etc!

cc @luisherranz @capaj @kitze

brainstorminwild idea

Most helpful comment

One issue I've been looking into is how to handle partial data.

My use case is that I want to use mobx-state-tree instead of apollo for handling state from a grahpql api. Apollo is great, but mst would have some advantages over it imo:

  • Objects stored in Apollo have an implicit life span, which is bad for complex, long-running apps that need to do frequent cleanup of cached data.
  • Having to define which queries to subscribe to and refetch on a mutation can be annoying. This could be handled by mst automatically by simply updating the changed state.
  • In general being more in control and explicit with how state is handled.

Most of the things needed for this to happen are already in place. Add __typename to all models in your queries and use a function to attach data to the correct store according to the type. If everything lives in the same tree you can use identifiers and references and this mostly just works (the company I work for use something like this in production right now).

Mst mostly expects your models to be hydrated with complete data, which doesn't match well with how graphql works. These are some of the challenges we face with our implementation:

  • How do we know if a model is incomplete and needs to fetch more data, or to only fetch what is missing if we want to cache old data?
  • How do we use a partial model in the parts of our app that don't need or want access to the complete underlying model?
  • How do we define views and actions that act on incomplete data?

I recently watched Rich Hickeys "Maybe Not"-talk, where he (among other things) discuss how Clojure Spec ran into a similar issue. The solution he brings up in his talk is an api for _selecting_ certain properties from a specification.
Maybe something like that could be useful for mst as well?

This is how it might look:

import { types } from 'mst';

const User = types
    .model('User', {
        id: types.identifier,
        firstName: types.string,
        lastName: types.string,
        username: types.string,
        adress: types.string,
        phoneNumber: types.string
    })
    .views(self => ({
         get fullName() { return self.firstName + ' ' + self.lastName; }
    })
    .actions(self => ({
         setUsername(username) { self.username = username; }
    }));

const UserBrief = User.select(self => {
    return {
        props: {
           id: self.id,
           username: self.username
        },
        actions: {
           setUserName: self.setUsername,
           onIncompleteDataAccess() {
               // a special method that maybe throws or does a request for more data when the 
               // model doesn't have access to the data that the consumer is asking for
               // 
               // so if you tried to access `fullName` or `phoneNumber` on a UserBrief this method 
               // would be called.
           }
       }
    }
})

const UserStore = types.model({
    users: types.map(types.union(User, UserBrief))
});

All 12 comments

I would love to have something! That would be awesome! I have a project where I would require an integration between mbox-state-tree and GraphQL.

A middleware sounds to be a nice foundation. I will start the integration on my own as I need it for my project. I might leave some feedback

Hi @mweststrate

I spent the afternoon to build a converter. It's not perfect, but a good start.

https://github.com/birkir/graphql-mst

I'm planning on adding some relay connection utils too, or maybe in another package.

@birkir thanks!

@mweststrate thanks for tagging. I wanted to write a bit about graphtype here. I work on sort of a query builder for graphQL. The good thing about it is that it knows what types you are querying at runtime. It can add __typename property on each model.
So it should be trivial to map the graphQL response to MST types essentially giving you free deserialization of your API models.

It's still very much WIP, but if anyone has suggestions how to improve it/feature requests, create them in https://github.com/capaj/graphtype/issues

I've updated my library to use the same codegen utility function (and added support for more types etc.)

re: queries

This is what most people who use MST and GraphQL do today (with various abstraction of course)

const Todo = types.model('Todo', {
  id: types.identifier,
  text: types.string,
  completed: types.boolean,
});

interface GetAllTodosQueryData {
  allTodos: Array<{
    id: number;
    text: string;
    completed: boolean;
  }>
}

client.query<GetAllTodosQueryData>({
  query: gql`
    query allTodos($filter: TodoFilter) {
      allTodos(filter: $filter) {
        id
        text
        completed
      }
    }
  `,
  variables: {
    filter: 'SHOW_ALL',
  },
}).
.then(result => {
  result.data.allTodos.forEach(todoData => {
    const item = Todo.create(result.data);
    todoStore.addItem(item);
  })
});

By using graphql-mst we can move some of the model definitions to shared typings (server, client) by using graphql.

enum TodoFilter {
  SHOW_ALL
  SHOW_ACTIVE
  SHOW_COMPLETED
}

type Todo {
  text: String!
  completed: Boolean
  id: ID!
}

type Query {
  allTodos(filter: TodoFilter): [Todo]
}
const { Todo, TodoFilter, Query: { allTodos } } = generateFromSchema(schema);

client.query<typeof allTodos>({
  query: gql`
    query allTodos($filter: TodoFilter) {
      allTodos(filter: $filter) {
        id
        text
        completed
      }
    }
  `,
  variables: {
    filter: TodoFilter.SHOW_ALL,
  }
}).
.then(result => {
  result.data.allTodos.forEach(todoData => {
    const item = Todo.create(result.data);
    todoStore.addItem(item);
  })
});

And if we could make queries, automatically query-able, by using something like graphtype to compile them.

enum TodoFilter {
  SHOW_ALL
  SHOW_ACTIVE
  SHOW_COMPLETED
}

type Todo {
  text: String!
  completed: Boolean
  id: ID!
}

type Query {
  allTodos(filter: TodoFilter): [Todo]
}
const {
  Todo,
  TodoFilter,
  Query: { allTodos }
} = generateFromSchema(schema);

graphqlMst
  .query(allTodos, {
    filter: TodoFilter.SHOW_ALL
  })
  .then(allTodos => {
    allTodos.forEach(todo => {
      todoStore.addItem(todo); // 'todo' is already an 'Todo' instance
    });
  });

The issue with this is that you can no longer control what fields you actually want to query, unless a query builder would be provided instead of using the model reference.

One issue I've been looking into is how to handle partial data.

My use case is that I want to use mobx-state-tree instead of apollo for handling state from a grahpql api. Apollo is great, but mst would have some advantages over it imo:

  • Objects stored in Apollo have an implicit life span, which is bad for complex, long-running apps that need to do frequent cleanup of cached data.
  • Having to define which queries to subscribe to and refetch on a mutation can be annoying. This could be handled by mst automatically by simply updating the changed state.
  • In general being more in control and explicit with how state is handled.

Most of the things needed for this to happen are already in place. Add __typename to all models in your queries and use a function to attach data to the correct store according to the type. If everything lives in the same tree you can use identifiers and references and this mostly just works (the company I work for use something like this in production right now).

Mst mostly expects your models to be hydrated with complete data, which doesn't match well with how graphql works. These are some of the challenges we face with our implementation:

  • How do we know if a model is incomplete and needs to fetch more data, or to only fetch what is missing if we want to cache old data?
  • How do we use a partial model in the parts of our app that don't need or want access to the complete underlying model?
  • How do we define views and actions that act on incomplete data?

I recently watched Rich Hickeys "Maybe Not"-talk, where he (among other things) discuss how Clojure Spec ran into a similar issue. The solution he brings up in his talk is an api for _selecting_ certain properties from a specification.
Maybe something like that could be useful for mst as well?

This is how it might look:

import { types } from 'mst';

const User = types
    .model('User', {
        id: types.identifier,
        firstName: types.string,
        lastName: types.string,
        username: types.string,
        adress: types.string,
        phoneNumber: types.string
    })
    .views(self => ({
         get fullName() { return self.firstName + ' ' + self.lastName; }
    })
    .actions(self => ({
         setUsername(username) { self.username = username; }
    }));

const UserBrief = User.select(self => {
    return {
        props: {
           id: self.id,
           username: self.username
        },
        actions: {
           setUserName: self.setUsername,
           onIncompleteDataAccess() {
               // a special method that maybe throws or does a request for more data when the 
               // model doesn't have access to the data that the consumer is asking for
               // 
               // so if you tried to access `fullName` or `phoneNumber` on a UserBrief this method 
               // would be called.
           }
       }
    }
})

const UserStore = types.model({
    users: types.map(types.union(User, UserBrief))
});

So the UserBrief is like an MST analogy to graphql fragment on User objectType. But unlike fragments this can carry it's own runtime methods around. Pretty neat.

the company I work for use something like this in production right now).

Good to hear!

We currently use MST with graphql. All of integrations it's just one function, which transforms server response to model snapshot format. i.e.:

fragment Issue on Issue {
    id
    start_date
    end_date
    executor_id: executor { id }
    chat_id: chat { id }
}

query loadIssues {
    data: issues(filter: {active: true}) {
        edges {
            node {
                ...Issue
            }
        }
    }
}
const ItemData = types.model({
    id: types.identifier,
    start_date: types.maybeNull(types.string),
    end_date: types.maybeNull(types.string),
    chat: types.reference(Chat),
    executor: types.maybeNull(types.reference(User)),
})
import { FetchResult } from 'apollo-link'

export function transform(node: any) {
    if (typeof node !== 'object' || !node)
        return node
    if ('edges' in node)
        return (node.edges || []).map((edge: any) => transform(edge.node))
    if (node.constructor === Array)
        return node.map((el: any) => transform(el))
    const newNode: Record<string, any> = {}
    for (let [key, value] of Object.entries(node)) {
        const pageInfo = value && (value as any).pageInfo
        if (pageInfo && value && (value as any).edges)
            newNode[key + '_page_info'] = pageInfo
        const page_info = value && (value as any).page_info
        if (page_info && value && (value as any).edges)
            newNode['page_info'] = page_info
        if (!key.endsWith('_raw'))
            value = transform(value)
        else
            key = key.replace('_raw', '')
        if (key.endsWith('_id')) {
            if (value)
                value = (value as any).id
            key = key.replace('_id', '')
        }
        if (key.endsWith('_ids') && value) {
            if (value)
                value = (value as Array<any>).map(x => x.id)
            key = key.replace('_ids', '')
        }
        newNode[key] = value
    }
    return newNode
}

export default function prepare(val: FetchResult) {
    if (val.errors) {
        const error = val.errors[0]
        error.name = '[GraphQL error]'
        throw error
    }

    let data = transform(val.data)
    if (Object.keys(data).length === 1 && data.data)
        data = data.data
    return data
}

This function handles various use cases, for example - transforms relay connections to simple arrays with page info as sibling, or rename fields.
May be it will helpful for somebody.

Closing this issue as mst-gql is kinda serious now, so all further questions could be handled there: https://github.com/mobxjs/mst-gql/

@Amareis I'm curious if this fit's your use case as well!

@kimgronqvist mst-gql has now a query select builder, as described here: https://github.com/mobxjs/mst-gql#customizing-the-query-result. Hope it solves your problems!

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs or questions.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

A-gambit picture A-gambit  路  3Comments

tahv0 picture tahv0  路  3Comments

donatoaz picture donatoaz  路  3Comments

FredyC picture FredyC  路  3Comments

mreed picture mreed  路  3Comments