Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mobx integration #503

Closed
josoroma-zz opened this issue Aug 3, 2016 · 14 comments
Closed

Mobx integration #503

josoroma-zz opened this issue Aug 3, 2016 · 14 comments

Comments

@josoroma-zz
Copy link

Hi!

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

Thanks in advance!

@stubailo
Copy link
Contributor

stubailo commented Aug 4, 2016

What kind of integration are you looking for?

@josoroma-zz
Copy link
Author

josoroma-zz commented Aug 4, 2016

Hi @stubailo!

Perhaps an integration example similar to:

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

Thanks!

@stubailo
Copy link
Contributor

stubailo commented Aug 4, 2016

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.

@timeyr
Copy link

timeyr commented Aug 15, 2016

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.

@stubailo
Copy link
Contributor

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

@stubailo stubailo added the idea label Aug 19, 2016
@mweststrate
Copy link

mweststrate commented Aug 19, 2016

@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)
}) 

@AdamBrodzinski
Copy link

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.

@ykshev
Copy link

ykshev commented Nov 5, 2016

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?

@harrisrobin
Copy link

@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!

@ykshev
Copy link

ykshev commented Dec 16, 2016

@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 🖖

@harrisrobin
Copy link

@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!

@helfer
Copy link
Contributor

helfer commented May 3, 2017

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

@helfer helfer closed this as completed May 3, 2017
@sonaye
Copy link

sonaye commented Jul 29, 2017

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

@fullofcaffeine
Copy link

@sonaye Link is 404 atm :(

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 1, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants