Code Monkey home page Code Monkey logo

Comments (12)

bgentry avatar bgentry commented on May 22, 2024

@viniciussbs hey, thanks for writing this up. Quick question… You said:

Taking a look at #20, it does not solve this problem. I can't create my own observer since managedWatchQuery does not return the observable query.

In #20, the ObservableQuery is available on the result object (returned from apollo.watchQuery) as _apolloObservable. Were you unaware of that, or does that not give you what you need?

from ember-apollo-client.

viniciussbs avatar viniciussbs commented on May 22, 2024

Hi, @bgentry.

I wasn't aware of that. I think I can't achieve part of the results with it. I say "part" because I'll still need to handle the deeply frozen result data. This non-extensible result object breaks Ember. A deep copy of the result object solves the problema, but is not straightforward.

I'll test a solution using the branch from #20. Thanks for the tip.

from ember-apollo-client.

viniciussbs avatar viniciussbs commented on May 22, 2024

Hi, @bgentry. Let me share the results of my tests.

How I solve my problem using #20:

return this.apollo.watchQuery({ query, variables, fetchPolicy }).then((data) => {
  data._apolloObservable.subscribe({
    next: (result) => {
      let model = this.buildModel(copy(result.data, true));
      let currentModel = get(this, 'controller.model');

      if (currentModel) {
        currentModel.setProperties(model);
      }
    },

    error(e) {
      if (get(this, 'controller.model')) {
        this.send('error', e);
      }
    }
  });

  return this.buildModel(data);
});

buildModel() is just a function to extract the nodes from a Relay connection. Application specific.

Some points:

  • I have to check get(this, 'controller.model') because the observer is called before the promise is resolved. I guess I could solve this issue storing _apolloObservable somewhere and subscribing to it inside afterModel().
  • I have to do a deep copy of the result object.
  • I have to handle the error case manually.
  • I guess I would have to put the side effects inside a run call.

How I solve my problem using v0.3.1:

let observable = get(this, 'apollo.client').watchQuery({ query, variables, fetchPolicy });

return new Ember.RSVP.Promise((resolve, reject) => {
  observable.subscribe({
    next: (result) => {
      let model = this.buildModel(Ember.copy(result.data, true));
      let currentModel = get(this, 'controller.model');

      if (currentModel) {
        currentModel.setProperties(model);
      } else {
        resolve(model);
      }
    },

    error(e) {
      if (get(this, 'controller.model')) {
        this.send('error', e);
      } else {
        reject(e);
      }
    }
  });
});

Some points:

  • I have to check get(this, 'controller.model') here too, but now as an expected scenario.
  • I have to do a deep copy of the result object.
  • I have to handle the error case manually.
  • I guess I would have to put the side effects inside a run call.
  • I have to clean the observer and the subscription by myself.
  • It look likes it's more code, but it's just 4 lines longer - the resolve and reject inside else blocks.

How I would like to solve my problem:

model() {
  // vars ...
  
  return this.apollo.watchQuery({ query, variables, fetchPolicy }).then((data) => {
    return this.buildModel(data);
  });
},

// It's called only when it exists.
updateModel(model, data) {
  model.setProperties(this.buildModel(data));
}

Or maybe:

let updateModel = (model, data) => {
  model.setProperties(this.buildModel(data));
};

return this.apollo.watchQuery({ query, variables, fetchPolicy, updateModel }).then((data) => {
  return this.buildModel(data);
});

What do you think?

from ember-apollo-client.

bgentry avatar bgentry commented on May 22, 2024

@viniciussbs I'm a little confused because it is my understanding that refetchQueries should work here for i.e. refetching a list to account for a deleted entry. The Apollo Client docs make me think that if you include a query or a query name in refetchQueries, any subscribers on that query should automatically receive the new result when that updated list is put into the store.

In #20, the function returned by newDataFunc should get called both when the initial data is loaded and when it is refetched. That function maintains a reference to the object (either an Ember.A or Ember.Object) that it returns when it initially resolves the promise. On subsequent calls, that function will update the object using .setProperties() for an Ember.Object or .setObjects() for an Ember.A. If that's not actually happening, then it's either a bug or a big misunderstanding on my part of how Apollo Client works.

One thing I will say about newDataFunc is that it's not updating arrays in an efficient way. This goes both for array result sets and for any nested arrays within a result object. So it might be the case that when you get an updated list, Ember re-renders your whole UI because it thinks the entire list has changed (even if only one item disappeared). If that is indeed happening (as I think you mentioned on Slack), it's something we should be able to fix by making newDataFunc smarter.

Is there another use case you're trying to satisfy here other than deletion from a list?

Some docs that might be useful are the guides for doing this sort of thing react-apollo:

  • http://dev.apollodata.com/react/cache-updates.html - especially the update part as it relates to updating the cache without refetching
  • details on update
  • the fetchMore section on handling pagination. My understanding here is that when you call fetchMore on the observable, the provided updateQuery gets called. In there you specify how to merge and/or replace the new and previous results. Whatever is returned by that function then gets passed into the next() function on the subscriber (newDataFunc in #20).

I will add that I am already using #20 in my own app and refetchQueries seems to be working for me as expected when a new item is added to a list. I don't yet have deletion, though, so can't verify if that's somehow different. I should also note that I'm providing query hashes to refetchQueries, such as { query, variables } rather than just the name of the query, but as I understand it that shouldn't matter.

from ember-apollo-client.

bgentry avatar bgentry commented on May 22, 2024

ah, I'm remembering now that your use case involves doing further manipulation on the result data (such as restructuring the relay association edges into a simple array of related object IDs). Is that the problem here?

I'm wondering if the right way to handle this would be for you to create a model abstraction on top of the result object returned by ember-apollo-client. Essentially you'd do something like MyObject.create({ rawData: apolloResultObject }) and would rely on computed properties within MyObject to do any transformation you need.

Does that address your use case?

from ember-apollo-client.

viniciussbs avatar viniciussbs commented on May 22, 2024

Hi, @bgentry. Sorry for the late update. At least I've tested your suggestions.

@viniciussbs I'm a little confused because it is my understanding that refetchQueries should work here for i.e. refetching a list to account for a deleted entry. The Apollo Client docs make me think that if you include a query or a query name in refetchQueries, any subscribers on that query should automatically receive the new result when that updated list is put into the store.

(...)

ah, I'm remembering now that your use case involves doing further manipulation on the result data (such as restructuring the relay association edges into a simple array of related object IDs). Is that the problem here?

Yep, that's the point. The refetchQueries works, but I need to manipulate the result when there's new data.

In #20, the function returned by newDataFunc should get called both when the initial data is loaded and when it is refetched. That function maintains a reference to the object (either an Ember.A or Ember.Object) that it returns when it initially resolves the promise. On subsequent calls, that function will update the object using .setProperties() for an Ember.Object or .setObjects() for an Ember.A. If that's not actually happening, then it's either a bug or a big misunderstanding on my part of how Apollo Client works.

(...)

I'm wondering if the right way to handle this would be for you to create a model abstraction on top of the result object returned by ember-apollo-client. Essentially you'd do something like MyObject.create({ rawData: apolloResultObject }) and would rely on computed properties within MyObject to do any transformation you need.

Does that address your use case?

Yes, it does. 🤓 I've tested it and it works. It's more like a view class than a model, though.

One thing I will say about newDataFunc is that it's not updating arrays in an efficient way. This goes both for array result sets and for any nested arrays within a result object. So it might be the case that when you get an updated list, Ember re-renders your whole UI because it thinks the entire list has changed (even if only one item disappeared). If that is indeed happening (as I think you mentioned on Slack), it's something we should be able to fix by making newDataFunc smarter.

This is still happening. I can solve this issue using a observer instead of a computed property inside that model/view abstraction that you've suggested.

Thank you for your suggestions.

from ember-apollo-client.

dephora avatar dephora commented on May 22, 2024

@viniciussbs I'm in a similar situation.

I have a user form broken down into several components and those are scattered across a few nested routes within the 'user' route. All update mutations are handled within the component. However, I'm handling the delete mutation in the root user route (for easier transitions once the user is deleted).

Update / cancel button visibility is based on the 'initial' model vs. the modified model. ie. the buttons only show if the state is different. This is trivial with ember-data since it has attributes built into it to handle this. (I should probably spend time digging into Redux to see if I'm over complicating this).

That all being said, I'm using ember-concurrency tasks for everything related to my mutations and queries (not using Ember promises at all).

My solution to updating the model after any sort of mutation is to have the ember concurrency task (successful) fire off a this.refresh() on the root user route. I'm not sure if this is any better or worse than the observer / computed property route. Prior to this I was doing a lot of manual property resetting instead of just refreshing the model after a refetchQueries.

I should note that I'm not doing anything in the before/model/after hooks other than my initial query, so refreshing has no implication there.

Anyhow, my case doesn't seem as complex as yours but thought I'd throw out it there. Also, I know nothing about Relay other than its existence.

from ember-apollo-client.

viniciussbs avatar viniciussbs commented on May 22, 2024

@dephora I've not used ember-concurrency yet, so I can't tell much about it. If you share some piece of code, that would be great.

I'm handling this situation following similar to Blake's suggestion: controller.model holds the object returned by this.get('apollo').query and, every time the query is refetched, controller.model is updated. All manipulation of the returned data is done on after model, or as computed properties or inside components. Keeping controller.model as it was returned, though.

Are you using this.get('apollo').query? It won't work if you are querying calling queryOnce.

from ember-apollo-client.

dephora avatar dephora commented on May 22, 2024

@viniciussbs Blake's suggestion sounds better than the route refresh I'm doing.

I have a full users listing which is 'display' only. The individual user route(s) contains the components which handle the mutations (see section-email.js below)

I'm using query as opposed to queryOnce.

I'm new to ember-concurrency as well (and apollo as noted earlier) so it's very possible there are issues with what I'm doing here but so far everything seems to be working well.

Users Route

// users.js

  model() {
    return get(this, 'loadModelTask').perform();
  },

  setupController(controller, model) {
    this._super(...arguments);
    set(controller, 'usersModel', model);
  },

  actions: {
    refreshCurrentRoute() {
      this.refresh()
    },
    gotoRoute(id) {
      this.transitionTo('main.admin.organization.members.user.account', id)
    }
  },

  loadModelTask: task(function* () {
    let query = get(this, 'query');

    try {
      let modelQuery = yield get(this, 'apollo').query({
        query,
        fetchPolicy: 'cache-first'
      }, 'users');

      return modelQuery;

    } catch (e) {
      // TODO handle
      console.log(e)
      
    }
  }).restartable(),
});

Individual User Email Component

  // section-email.js
  id: alias('userModel.id'),
  email: alias('userModel.email'),
  emailInitial: alias('initialState.email'),

  showFormUpdateBtns: notEqual('email', 'emailInitial'),

  isSaveDisabled: computed('email', function () {
    return Validator.emailFormat(get(this, 'email'));
  }),

  didReceiveAttrs() {
    this._super(...arguments);
    set(this, 'initialState', getProperties(this, 'email'));
  },

  updateMutationTask: task(function* () {
    let mutation = get(this, 'mutation');
    let notify = get(this, 'notify');
    let variables = {
      id: parseInt(get(this, 'id')),
      email: get(this, 'email'),
    };

    try {

      let updateMutation = yield get(this, 'apollo').mutate({
        mutation,
        variables,
      }, 'updateUser');

      notify.success(`Email successfully updated to ${updateMutation.email}.`);

      this.refreshRoute(); // passed in route-action

    } catch (e) {
      // console.info('Error', e);
      // TODO handle
      notify.error(`There was a problem updating the user's email address.`, {
        closeAfter: null
      });
    }
  }).drop(),

  // resetTask currently isn't doing async operations but might in the future
  resetTask: task(function* () {
    let notify = get(this, 'notify');

    yield timeout(1); // this yield is unnecessary - testing

    set(this, 'email', get(this, 'emailInitial'));

    notify.warning(`Email reset: no changes made.`);

  }).drop(),
});

from ember-apollo-client.

viniciussbs avatar viniciussbs commented on May 22, 2024

@dephora you have a route that list users and another route to see an individual user, right? If I'm right, that may be the reason why refetchQueries isn't working.

In order to use refetchQueries, the query to be refetched needs to be active, still in observation. If your index route extends UnsubscribeRoute mixin, than Ember will unsubscribe the query after a route transition. So you won't have the query to be refetched.

You can check active query subscriptions using Apollo Client Developer Tools or calling __APOLLO_CLIENT__.queryManager.observableQueries and __APOLLO_CLIENT__.queryManager.queryDocuments from console.

In my app I always use fetchPolicy: 'network-only' in my index routes. That's why I don't have this problem when I'm coming from another route.

from ember-apollo-client.

dephora avatar dephora commented on May 22, 2024

@viniciussbs Right, sorry for the confusion. I was shifting my users / user routes around and stopped using the refetch on my user route as it wasn't absolutely necessary for how I was handling the updates. I too was using network-only but I'm still experimenting. My users listing isn't a big deal for now so a fetchPolicy: network-only doesn't hurt but I will have an image listing that will be far larger and I haven't yet messed with pagination (or auth, or file uploads).

Anyhow, appreciate the feedback and suggestions. I still need to work in the computed property model.

from ember-apollo-client.

viniciussbs avatar viniciussbs commented on May 22, 2024

Since the solution suggested by @bgentry solves the problem, I'm closing the issue.

from ember-apollo-client.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.