Apollo-client: Mobx integration

Created on 3 Aug 2016  路  14Comments  路  Source: apollographql/apollo-client

Hi!

I was wondering is there a => How integrate with Mobx example?

Thanks in advance!

Most helpful comment

@stubailo both work, if it is possible to granular patch the json tree it will just be more efficient, but I think first viable iteration could be to just create fresh observables. fromResource is probably a nice starting point to setup an integration.

I think this should get you going:

import {autorun, observable} from "mobx"
import {fromResource} from "mobx-utils"

// generic utility
function queryToMobxObservable(/*apollo observable*/ observable) {
    let subscription
    return fromResource(
        // MobX starts using the observable 
        (sink) => {
            subscription = queryObservable.subscribe({
                next: ({ data }) => {
                    // todo: recycle MobX observables? (yes, will be renamed at some point ;-))
                    sink(observable(data))
                },
                error: (error) => {
                    console.log('there was an error sending the query', error);
                }
            });
        },
        // Observable no longer in use (might become in use later again)
        () => subscription.unsubscribe()
    )
}

// usage
const myObservable = queryToMobxObservable(client.watchQuery(/* apollo query */))

autorun(() => {
    console.log("Amount of results: " + myObservable.current().length)
}) 

All 14 comments

What kind of integration are you looking for?

Hi @stubailo!

Perhaps an integration example similar to:

http://docs.apollostack.com/apollo-client/redux.html

Thanks!

Right, but I'm wondering what integration means to you - is it having the store data live in mobx? using the query results with mobx as data sources? etc.

In redux it's pretty clear because we internally use reducers etc. But as far as I know MobX doesn't have reducers so I'm wondering what kind of integration you would be looking for.

Right now you can easily just call watchQuery and put the results into MobX yourself, so I'm interested in what kind of improvement you are looking for over that solution.

From my point of view, in order to use Apollo together with Mobx we need a way to transform a query result into an observable state tree. This can be done quite easily by wrapping connect into a function that walks through the json result and creates observables for them. The tricky part is that we need to make sure that we always create/re-use the same observable object instance for each domain object in the result. What I mean by this is if you have two queries both querying Todo with id=17, you need to fetch the same observable object in both cases. That means that you need to have a way to map your query result back to your domain model (and ultimately, to your Mobx stores), which can be achived by always including __typename in your query (use queryTransformer: addTypename). Another complication is when you have parameterized fields in your query, because you cannot map them to a single field on an object, but you need to somehow keep track of the parameters used for that field.

In the end it will be very tighly coupled to your domain model, therefore I am not sure how one could add some generic integration to Apollo.

Reading this again, I realize that watchQuery is just not the right place to hook Mobx in. We would need to access the raw data in the redux store directly and transform this into observables.

Does MobX rely on having nested trees of observables? Is it not enough to have one observable that returns entire new JSON results?

@stubailo both work, if it is possible to granular patch the json tree it will just be more efficient, but I think first viable iteration could be to just create fresh observables. fromResource is probably a nice starting point to setup an integration.

I think this should get you going:

import {autorun, observable} from "mobx"
import {fromResource} from "mobx-utils"

// generic utility
function queryToMobxObservable(/*apollo observable*/ observable) {
    let subscription
    return fromResource(
        // MobX starts using the observable 
        (sink) => {
            subscription = queryObservable.subscribe({
                next: ({ data }) => {
                    // todo: recycle MobX observables? (yes, will be renamed at some point ;-))
                    sink(observable(data))
                },
                error: (error) => {
                    console.log('there was an error sending the query', error);
                }
            });
        },
        // Observable no longer in use (might become in use later again)
        () => subscription.unsubscribe()
    )
}

// usage
const myObservable = queryToMobxObservable(client.watchQuery(/* apollo query */))

autorun(() => {
    console.log("Amount of results: " + myObservable.current().length)
}) 

_Right, but I'm wondering what integration means to you - is it having the store data live in mobx? using the query results with mobx as data sources? etc._

@stubailo To me having an integration would mean we could swap out the redux core for Mobx. This would save some file weight if one was using Mobx and Apollo (do you happen to know the payload size of Redux btw?)

I've found that I really don't need all the hassle of Redux with Apollo Client and for the small amount of "global state" that I need to store, Mobx is more practical. I ended up just making a HoC using props.setGlobalState().

However, I realize switching out your cache layer is not going to be simple so i'm ok with just loading Redux as an Apollo dep.

Was wandering around with mobx-meteor-apollo and found this issue. There is a small misspelling in @mweststrate example, it should be queryObservable as a first argument for function queryToMobxObservable(/*apollo observable*/ queryObservable).

I see here are a lot of people from meteor community, maybe someone could give me a feedback on my integration with react-mobx-meteor-apollo.

In my router /imports/startup/client/routes.jsx I create an apollo client and my app appStore. I'm not sure that it's alright to pass client to the store as this and define it there:

import ApolloClient from 'apollo-client';
import { meteorClientConfig } from 'meteor/apollo';

const apolloClient = new ApolloClient(meteorClientConfig());
const appStore = new AppStore(apolloClient);

const stores = {appStore, uploadStore};
...
<Provider { ...stores }>
   // Mobx provider
</Provider>

Than in my appStore.js

export default class AppStore {
    @observable user = false;

    constructor (apolloClient) {
      const handle = apolloClient.watchQuery({
        query: gql`
          query ($id: String!) {
            user(id: $id) {
              _id
            }
          }
        `,
        variables: {
          id: Meteor.userId()
        },
        forceFetch: false,
        returnPartialData: true,
        pollInterval: 10000, 
      });
      const query = queryToMobxObservable(handle);

      autorun(() => {
        if (query.current()) this.setUser(query.current().user)
      })
    }

    @action setUser = (user) => {
      this.user = user;
    }
}

and queryToMobxObservable

import {observable} from "mobx"
import {fromResource} from "mobx-utils"

export default function (queryObservable) {
    let subscription
    //Mobx util https://github.com/mobxjs/mobx-utils#fromresource
    return fromResource(
        (sink) => {
            subscription = queryObservable.subscribe({
              next: (graphqlRes) => {
                if (graphqlRes.data) {
                  sink(observable(graphqlRes.data));
                }
              },
              error: (error) => {
                console.log('there was an error sending the query', error);
              }
            });
        },
        () => subscription.unsubscribe()
    )
}

any suggestions how to improve all this code above?

@ykshev thanks, great start. How would you do things like fetchMore in this case? In other words, I need to start using some of the methods in react-apollo's graphql's HOC.

Cheers!

@HarrisRobin actually I've moved from mobx fromresource to the plain apollo client usage.

Now I call mobx action onRouter Enter and my action looks like this

@action getData = (collectionId) => {
   this.isLoading = true;
   const options = {
      query: gql`
        query collectionItem($userId: String!, $id: String!, $limit: Int!, $offset: Int!, $isFavorite: Boolean) {
          collection(userId: $userId, id: $id ) {
            _id,
            title,
            isPublic,
            images(limit: $limit, offset: $offset, isFavorite: $isFavorite) {
              _id
              title
              collectionId
              image {
                url
                width
                height
              }
          }
        }
      `,
      variables: {
        userId: Meteor.userId(),
        id: collectionId,
        limit: this.limit,
        offset: this.offset,
        isFavorite: this.isFavoriteFiltered,
      },
      // forceFetch: true
    }

    const self = this,
          apolloClient = this.appStore.apolloClient;

    this.query = apolloClient.watchQuery(options);

    this.subscription = this.query.subscribe({
      next(result, more) {
        if (!result.loading) {
          self.setData(result.data.collection);
        }
      },
      error(err){
        self.appStore.createNotification({
          type: 'error',
          id: Date.now(),
          text: 'Something went wrong on querying. ' + err.message
        })
        if (Meteor.isDevelopment) console.log(err);
      }
    })
}

in this case when you have your query saved, you can use fetchMore in another action as this

  @action loadMore = () => {
    if (this.isLoading) return;
    this.offset = this.offset + defaults.load;
    this.setLoading(true)
    this.query.fetchMore({
      variables: {
        limit: this.limit,
        offset: this.offset,
      },
      updateQuery: (previousResult, { fetchMoreResult, queryVariables }) => {
        const prevEntry = previousResult.collection;
        const newImages = fetchMoreResult.data.collection.images;

        return {
          collection: Object.assign({}, prevEntry, {images: [...prevEntry.images, ...newImages] })
        };
      }
    }).then(() => this.setLoading(false))
  }

and onRouter Leave I call:

  @action onLeave = () => {
    ...
    this.query = null;
    this.subscription = null;
  }

and I'm still not sure, that I'm doing it in the best optimized way, but having a lack of information about all that stuff, I hope it will be usefull for someone 馃枛

@ykshev seems a little weird to do this to me. I think i'm simply going to use redux for this project and have to refactor it all... :P

Thank you for the help though!

Let's continue all store API discussions in #1432.

Just published a small utility mobx-apollo that makes this petty straightforward.

@sonaye Link is 404 atm :(

Was this page helpful?
0 / 5 - 0 ratings