Stateful components loosing state on query refetch - apollo

Is it correct that stateful components loose their state on a Query component refetch? Do I have to use Apollo client state for all components inside Query components?
Here is a little demo: https://codesandbox.io/s/qxx6jk3yz9 (increase count and then refetch - count will be reset)
Here is the main part of the demo's code:
<ApolloProvider client={client}>
<div className="App">
<Query query={GET_ALL_FILMS}>
{({ data, loading, refetch }) => {
if (loading) return "Loading...";
return <Counter refetch={refetch} />;
}}
</Query>
</div>
</ApolloProvider>

You have a conditional inside the Query component's render function, namely
if (loading) return "Loading...";
The loading state is updated not just on the initial load, but anytime refetch is called. That means when you hit refetch, only Loading... is rendered and the entire Counter component is unmounted. When the loading state changes back to false, the Counter component is rendered again, but because this is a new component, it starts off with a fresh state.
If you comment out the conditional, you'll see the App behave as you expected.
There's a variety of ways you could approach this problem. Here's three:
Manage the state through Apollo, Redux or some other state management library
Lift the state up so that it's stored in the Query component's parent component, and then pass the state down to your Counter component, along with a function to mutate it.
Instead of not rendering the Counter component, just hide it, for example by setting display to none.

Related

Mutation process in Apollo

This is my code so far:
<Mutation mutation={addUserQuery}>
{
(addUser, data)=>{
console.log(data)
return (
<div className="form">
<form onSubmit={(e)=>{
e.preventDefault();
console.log(e);
addUser({variables: {username: "AuraDivitiae",
firstname: "Michael",
lastname: "Lee"}})
}}>
<button type="submit">Add User</button>
</form>
</div>
)
}
}
</Mutation>
What does Apollo do when a mutation component mounts?
I feel like I don't really understand the processes running inside Apollo.
Does Apollo subscribe to the result of the mutation query?
Does it then update the cache on returning?
Is Data then stored in some components state?
I feel like the documentation doesn't provide enough information sometimes...
<Mutation/> component is ... a normal react component - it has own state, lifecycles, it's using apollo client (and its cache), keeps data.
It's probably a bit confusing that being in render we have rerenderings not caused by setState of our component.
If <Mutation/> is a component then de facto your inner content is rendered by render function of <Mutation/>, not in our component (it only renders <Mutation/> component). This is an additional depth level in components tree structure (with own lifecycles).

Ember View does not updating as of the change that happen to the model immediately

When I am making an AJAX request to update some values the data or model is changing. But the updated model is not reflecting immediately. It is reflecting only after clicking refresh. I want to only modify a view of a div in the page. I have tried many things but not successful. Can anyone suggest me the way to solve the issue?
The git repo:
https://github.com/SyamPhanindraChavva/trell-app-front-end
templates/note.hbs
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<p><a class="dropdown-item" {{action 'updateNote' "doing" note.id}}>DOING</a></p>
<p><a class="dropdown-item"{{action 'updateNote' "done" note.id}}>DONE</a></p>
<p><a class="dropdown-item"{{action 'deleteNote' note.id}}>DELETE</a></p>
</div>
controllers/notes.js
status2:Ember.inject.service('work-status'),
actions: {
updateNote(upstatus,id){
let status1 = Ember.get(this,'status2');
status1.makeputrequest(upstatus,id);
this.toggleProperty('parameter');
}
}
services/work-status.js
makeputrequest(status1,id1)
{
$.when($.ajax ({
// get(this,'layout.referrer.owner.router').transitionTo('notes');
type: 'PUT',
url:'http://localhost:3000/notes/'+id1,
data: {note:{status:status1}},
})).then(function(result){
console.log(result);
}).catch(function(err){
console.log("you have error")
});
},
The thing I am trying to make is whenever I change the status of a record the corresponding record in the UI must also go to the corresponding table or container in UI.
Because you're using ajax calls directly in ember, you'll be responsible for setting the new properties once it's complete.
In services/work-status.js, you'll want to return the promises generated by the ajax calls.
In controllers/notes.js, chain .then to the returned promise and call and set the new property.
EG - controllers/notes.js
updateNote(upstatus,id){
let status1 = Ember.get(this,'status2');
status1.makeputrequest(upstatus,id).then((){
this.get('model').findBy('id',id).set('status',upstatus);
});
},
With that said, this isn't the correct route to take. You're mixing ember-data with plain ajax calls which is making the logic of your codebase more difficult than necessary.
If you modify your action to take the entire note as an argument, change the status, and just save the model, you wouldn't need to use the service.
// templates/note.hbs
<p><a class="dropdown-item" {{action 'updateNote' note "doing"}}>DOING</a></p>
// controllers/note.js
updateNote(note,upstatus) {
note.set('status',upstatus)
note.save();
}
Ajax is only meant if you are not trying to change something on the records. However, if you would like to change a value of your records you should be using ember-data

Ember - Transition to same route with same model

I have a left tree list, and each item in the list opens same route with different model. What i want is, if i click on the same item again and again, i want the route to load again. I know ember doesn't normally function this way. Are there any tweaks to achieve this?
Update:
Left tree is parent route.On clicking items in left tree, child route is loaded in its outlet.
My left tree will be structured like this,
item1(link to bodyRoute1 with model1)
item2(link to bodyRoute1 with model2)
item3(link to bodyRoute1 with model3)
item4(link to bodyRoute2 with model1)
...etc
You could use refresh() route method api link, for example:
// route
actions: {
refreshRoute: function() {
this.refresh();
}
}
//template
<ul>
{{#each items as |item|}}
<li {{action 'refreshRoute'}}>{{item}}</li>
{{/each}}
</ul>
Update
One of the controller property need to be updated from server
So you could use afterModel model hook.
From guides:
The most common reason for this is that if you're transitioning into a route with a dynamic URL segment via {{link-to}} or transitionTo (as opposed to a transition caused by a URL change), the model for the route you're transitioning into will have already been specified (e.g. {{#link-to 'article' article}} or this.transitionTo('article', article)), in which case the model hook won't get called.
In these cases, you'll need to make use of either the beforeModel or afterModel hook to house any logic while the router is still gathering all of the route's models to perform a transition.
I got it working by doing like this,
I maintain the currentRouteName and currentModelId(An id to uniquely identify the model) in an object and it is injected in all routes. It gets updated on any transition.
All the transitions from left tree go through a common function, And in that function i check if the forwarding route,modelId is the same as current route,modelId. If so, i change the value of another globally injected property refreshSameRoute. This value is observed in the child routes and if it changes this.refresh() is called.
Still searching for a better method.
The methods for transitions, like transitionToRoute and replaceRoute, returns some params, including the targetName. When the query params are the same, the targetName returns undefined. You can check that and do a route refreshing.
const queryParams = {<your params>};
const routeName = 'my.route';
const transition = this.transitionToRoute(routeName, { queryParams });
if (transition.targetName !== routeName) {
const route = getOwner(this).lookup(`route:${routeName}`);
route.refresh();
}

how to inject a store into a component (when using localstorage adapter)

Ember docs say to define a store like this
MyApp.Store = DS.Store.extend();
If you are looking up records in components, this doc says you can inject the store into the component like this
// inject the store into all components
App.inject('component', 'store', 'store:main');
However, I am using the local storage adapter which I define like this
App.ApplicationAdapter = DS.LSAdapter.extend({
namespace: 'my-namespace'
});
Therefore, I don't know how to inject this into the component (where I need to look up a record) following the above instructions.
Following the instructions of this SO answer, I tried to inject the store into a component by passing it in like store=store and/or store=controller.store
<li> {{my-component id=item.customid data=item.stats notes=item.notes store=store}} </li>
or
<li> {{my-component id=item.customid data=item.stats notes=item.notes store=controller.store}} </li>
The goal was then to be able to do this in an action in the componeent
var todo = this.get('store');
console.log(todo, "the new store");
todo.set('notes', bufferedTitle);
console.log(todo, "todo with notes set");
todo.save();
However, todo.save(); always triggers
Uncaught TypeError: undefined is not a function
Notice that I logged the store? this is what it shows
Class {_backburner: Backburner, typeMaps: Object, recordArrayManager: Class, _pendingSave: Array[0], _pendingFetch: ember$data$lib$system$map$$Map…}
If i inspect it(by opening the tree, which isn't shown here), it does indeed show that notes were set via todo.set('notes', bufferedTitle); however, it doesn't have any of the other attributes of my model that I defined for the index route, and this object doesn't have a 'save' method. Therefore, it doesn't seem to be the actual store, but rather just some backburner object.
I got the same results trying this SO answer where it says to get the store of the targetObject
var todo = this.get('targetObject.store');
Note, I also tried this, i.e. setting the store to be the store of the item.
<li> {{my-component id=item.customid data=item.stats notes=item.notes store=item.store}} </li>
It should be noted that if I set the store in the component, I can print the store on the page by doing {{store}} which gives me
<DS.Store:ember480>
but I can't do var todo = this.get('store'); in the action that handles the click even in the application code.
Question, using the localStorage adapter, how am I able to look up a record in a component (with the aim of then being able to alter the record and then save it again)
Note, if it's important, I define a model for the (index) route like this
App.Index = DS.Model.extend({
title: DS.attr('string'),
version (unfortunately I don't know what version of Ember data or the adapter I'm using)
Ember Inspector
1.7.0
Ember
1.9.1
Ember Data
<%= versionStamp %>
Handlebars
2.0.0
jQuery
1.10.2
Update in response to request for more info
The code that sets up the problem is very simple.
here's the router (with a bad name for the resource :)
App.Router.map(function(){
this.resource('index', { path: '/'});
}
Here's the route that gets the record to use in the Index route
App.IndexRoute = Ember.Route.extend({
model: function{
var resource = this.store.find('index');
return resource;
}
});
I have an Index Controller which does nothing in particular for the component (unless I should be defining methods on the Controller that get triggered by component events)
In the html, I do this with handlebars to pass data to the component
{{#each item in items}}
<li> {{my-component id=item.customid data=item.stats notes=item.notes store=store}}
{{/each}}
Then, in components/my-component, I have a label that when clicked is supposed to trigger an action that will let me edit one of the attributes on the model
<label> {{action "editTodo" on="doubleClick">{{notes}}</label>
that click triggers this code in App.MyComponent, which triggers the error that prompted this question
var todo = this.get('store')
todo.set('notes', bufferedTitle);
todo.save()
IMHO injecting store into components is not the best idea... By design, components should be isolated and shouldn't have any knowledge about the store.
In the doc you've given, it's written: In general, looking up models directly in a component is an anti-pattern, and you should prefer to pass in any model you need in the template that included the component.
However, if you really need it for some reason, then why not just to pass the variable store to the component?
{{my-component store=store}}
Then, you can pass the store from your controller only in the components where you really need that.
Injecting the store in all your components will most likely lead you to the bad design (although it seems tempting at first).
Here's an updated answer for Ember 2:
Ember Data's store is now a Service, and we can easily inject it into all Components via an Initializer, e.g. app/initializers/inject-store-into-components:
export function initialize(application) {
application.inject('component', 'store', 'service:store');
}
export default {
name: 'inject-store-into-components',
initialize,
}
Then, in your Components, you can access the store with this.get('store'). The obviates the need to directly pass the store as an argument to Components, which requires a lot of boilerplate in your templates.
Whilst the accepted answer is sensible for simple applications it is perfectly acceptable to inject a store into a component if that component doesn't have a relationship with the url, like side bar content or a configurable widget on a dashboard.
In this situation you can use an initializer to inject the store into your component.
However, initializers can be a pain to mimic in testing. I have high hopes that the excellent Ember.inject API that is testing friendly will extend beyond services and accommodate stores. (Or that stores will simply become services).
According to this docThe preferred way to inject a store into a component is by setting a store variable to the record, for example
{{#each item in arrangedContent}}
<li> {{my-component store=item}} </li>
{{/each}}
Then in application code, you can do
var store = this.get('store');
store.set('todo', bufferedTitle);

Template #each & rendering

Why does the template get rendered the number of times that correlates with the Each in my template.
<template name="carousel">
<div class="pikachoose">
<ul class="carousel" >
{{#each article}}
<li><img src="{{image}}" width="500" height="250" alt="picture"/><span>{{caption}}</span></li>
{{/each}}
</ul>
</div>
</template>
Template.carousel.article = function () {
return News.find({},{limit: 3});
}
Template.carousel.rendered = function() {
//$(".pika-stage").remove();
alert($(".carousel").html());
//$(".carousel").PikaChoose({animationFinished: updateNewsPreview});
}
In this case it would alert 3 times.
That's the way Meteor handles data updates. Your article data function returns a cursor that is to be used in the template. Initially, the cursor is empty, and data is pulled from the server, one article at a time. Each time an article is fetched, the contents of cursor are changed (it now has one more article), and therefore the reactive article method causes template to rerender.
If you need to be sure that your code runs only once, there are several possibilities depending on what you need.
The easiest one is just to use created instead of rendered.
If you modify DOM elements, you can also mark elements you modify so that you won't process them twice:
Template.carousel.rendered = function() {
_.each(this.findAll('.class'), function(element){
if($(element).data('modified')) return;
$(element).data('modified', true);
...
});
};
You can disable reactivity for the cursor, though it's a sad solution:
Articles.find(..., {reactive: false});
The most invasive, but also the most versatile is to observe when the data is fully loaded:
Deps.autorun(function() {
Meteor.subscribe('articles', {
ready: function() {
...
},
});
});
The issue may have to do with the use of the .rendered callback. Each time the loop runs the DOM is updated so the callback will run again.
When I had this problem in the past, I found it helpful to use Meteor event handlers whenever possible, to eliminate load order issues like this one. In this case, maybe you could try a timeout, so that that the .remove() and .PikaChoose() calls only run after the DOM has been quiet for a certain interval. Hope that works for you.