I am trying to render a view in a React Native Android app when a HTTP call has completed.
My render method:
render: function() {
if (this.state.obj.name) {
return this.renderPage();
} else {
return (<Loading />);
}
}
< Loading /> contains simple View and Text elements which display "Loading".
I call the network request in componentDidMount():
componentDidMount: function() {
var self = this;
SomeService.getObjById(1).then(function(data) {
var obj = JSON.parse(data._bodyText);
self.setState({
obj: obj
});
});
},
However, when I update the state and instantiate 'obj' (which now has a name prop) render is recalled, it returns render page but the view does not update.
My renderPage method works when it is light like:
renderPage: function() {
return (
<View><Text>Hello</Text></View>
)
}
But when it has more than say 6 lines of code it seems it doesn't update.
Any idea why event though the conditional returns different components when the call is finished, the actual view does not update?
I solved this by making the request before the change of view.
This still didnt work but I added a setTimout(function(){}, 0) around the navigator.push() and this worked, as it pushed the 'push' to the end of the call stack.
Not amazing, but works
Related
I have custom objects for holding child objects full of data. The child objects are initiated with null values for all their properties, so the objects can be referenced and their properties filled from remote sources. This creates a lazy-loading setup.
This code is going to be extremely trimmed down, but everything relevant should be here:
class Collection extends Object {
constructor(){
this.loaded = false;
var allLoaders = [];
var loaderPropmises = [];
var resolver;
const $this = this;
var trackLoaders = function(){
$this.loaded = false;
loaderPromises.push(Promise.all(allLoaders).then(() => {
//... irrelevant logic in here to ensure only the latest promise sets loaded to true
$this.loaded = true; //This is getting called where I expect
resolver();
}));
}
//hook for outside things to watch the promise if they want
this.loader = new Promise((resolve) => {
//this only gets resolved once, which is fine
resolver = resolve;
});
//... bunch of code around adding child objects, but the important part:
this.add(child){
this[child.id] = child;
this.allLoaders.push(child.loader);
trackLoaders();
}
}
}
The child then looks like:
class Child extends Object {
constructor(){
this.loaded = false;
var resolver;
const $this = this;
this.loader = new Promise((resolve) => {
resolver = resolve;
}).then((){
$this.loaded = true;
});
this.populate(data){
//bunch of stuff to set data to properties on this object
resolver();
}
}
}
In Vuex 4 I have these Collections as properties on an "AppData" object in the store:
const store = createStore({
state: function(){
AppData: {}
},
mutations: {
setupCollection(state, name){
if (!Object.hasOwnProperty.call(state.AppData, name){
state.AppData[name] = new Collection();
}
}
},
actions: {
//this is called on each row of data returned from an Axios call
add (context, {name, data}){
context.state.AppData[name][data.id].populate(data);
}
}
});
The idea is that whenever a Child is added to a Collection, the collection loaded property will be false until all the Child loader promises resolve. This all executes perfectly... Except that the loaded bools aren't reactive.
Right now, I have a Promise.all in each component's Created function that flags the component as "loaded" once all the objects needed for the component have had their "loader" promises resolved. This absolutely works, but isn't ideal as different data will be available at different times, and there are sometimes hundreds or more of these classes on screen at once. What I'm trying to accomplish is:
<div v-if="!myCollection.loaded">
Loading...
</div>
<div v-else>
Show the data I want here {{myCollection.property}}
</div>
So I have two thoughts on overcoming this, either of which would be great:
VueJS3 no longer has a need for Vue.set(), because Proxies. How would I make the loaded bools here reactive then? Or more specifically, what am I doing that prevents this from working?
Alternatively, is there a practical way to use the loader promise directly in a template?
It looks like Vue's ref is what I needed:
this.loaded = ref(false);
This works, at least on the Child class. I have some sort of circular referencing issue going on and haven't been able to test on the Collection class yes, but it should work the same.
I want to avoid typing the same lines of code. Currently, I have an app that is supposed to make an API call, like so.
render: function(){
var processappkey = localStorage.getItem('yourid');
var weather = new XMLHttpRequest();
var deesfault = "Houston, Texas";
weather.open("GET", "http://apidatafromsomewebsiteq="+deesfault+"&units=imperial&appid="+processappkey, false);
weather.send(null);
var r = JSON.parse(weather.response);
var check = r.main.temp;
var theunicorn = r.weather[0].icon;
return (<p>{theunicorn}</p>)
}
I would like to split this up to something like this:
somecontainer: function(){
var processappkey = localStorage.getItem('yourid');
var weather = new XMLHttpRequest();
var deesfault = "Houston, Texas";
weather.open("GET", "http://apidatafromsomewebsiteq="+deesfault+"&units=imperial&appid="+processappkey, false);
weather.send(null);
var r = JSON.parse(weather.response);
var check = r.main.temp;
var theunicorn = r.weather[0].icon;
},
render: function() {
{this.somecontainer()}
return (
<p>{theunicorn}</p>
)
}
I will be calling the API from different areas in my app. Not to mention including a setInverval, which will have me repeating the code again.
As a matter of fact, while I am at it I would also like to know how to go about something like this.
render: function() {
this.somecontainer();
setInterval(function() {
this.somecontainer();
}, 5000);
}
However, that is a different question, and I'll be happy for insight on the first issue.
Good question, pretty easy answer. Just have a function that goes and gets the data you want, and returns the result via a callback function. This utility function would sit in another file somewhere and you can import it and call it from any component. Then, take the data that the function returns and put it in your components state.
You should almost certainly not be calling an API in the render method. React can run the render() method a lot depending on your app. If you want it to fire when the component first loads, use componentDidMount (this will only fire on the client, handy if you're using server-side rendering).
let counter = 0;
// separate utility
function goGetAUnicorn(callback) {
// replicate async for demonstration...
setTimeout(() => {
callback(`I am unicorn picture #${counter++}`);
}, 100)
}
class Unicorn extends React.Component {
constructor(props) {
super(props);
this.state = {
unicornPicture: '',
};
}
componentDidMount() {
// runs once, client side only
goGetAUnicorn(unicornPicture => {
this.setState({unicornPicture});
});
// to simulate reusing the same function elsewhere at some other time
setInterval(() => {
goGetAUnicorn(unicornPicture => {
this.setState({unicornPicture});
});
}, 1000)
}
render() {
return (
<div>Here is your unicorn: {this.state.unicornPicture}</div>
);
}
}
ReactDOM.render(<Unicorn />, document.getElementById('app'));
I'm using setTimeout just to indicate that you must wait for the response before carrying on. I'd actually use a promise, not a callback, but they both work.
Here's a jsbin to play with: https://jsbin.com/lohojo/1/edit?html,js,output
I have a template, chartEditPreview, that runs a D3.js chart-drawing function once the rendered callback fires. It looks like this:
Template.chartEditPreview.rendered = function() {
drawChart(".chart-container", this.data);
}
Where .chart-container is the target div and this.data is the data for the object in the DB currently being accessed. Problem is, this.data often returns null, seemingly at random. It looks like this has something to do with how the publish/subscribe pattern works — Iron Router (which I'm using) lets the templates render and then hot-pushes the data into those templates.
My question is (hopefully) pretty simple: how can I make sure this.data is actually full of DB data before drawChart is run? Should I be doing this in some other way, instead of calling it on the rendered callback?
I'm thinking of storing the DB data in a Session variable during the routing and calling that from rendered, but it seems like an extra step, and I'm not certain it'll fix this problem. The chart's also not rendered only once on the page — it's interactive, so it needs to be redrawn every time the database object is updated via one of the inputs on screen.
Any help would be much appreciated. Thanks!
For reference, here's what my routes.js looks like:
Router.route('/chart/edit/:_id', {
name: 'chart.edit',
layoutTemplate: 'applicationLayout',
yieldRegions: {
'chartEditType': { to: 'type' },
'chartEditPreview': { to: 'preview' },
'chartEditOutput': { to: 'output' },
'chartEditAside': { to: 'aside' },
'chartEditEmbed': { to: 'embed' }
},
data: function () {
return Charts.findOne({_id: this.params._id});
},
waitOn: function () {
return Meteor.subscribe('chart', this.params._id);
}
});
And my publications.js:
Meteor.publish("chart", function (id) {
return Charts.find({ _id: id });
});
This is a common problem with Meteor. While the subscription might be ready (you should check for it like Ethaan shows) , that does not mean the find() function actually had time to return something.
Usually I solve it with some defensive code, i.e:
Template.chartEditPreview.rendered = function () {
if(this.data)
drawChart(".chart-container", this.data);
// else do nothing until the next deps change
}
Of course this is not as clean as it should be, but as far as I know the only way to solve problems like this properly.
Updated answer
In this case we need a dependency to trigger rerun on data change. Iron router solves this for us:
Template.chartEditPreview.rendered = function () {
var data = Router.current() && Router.current().data();
if(data)
drawChart(".chart-container", data);
// else do nothing until the next deps change
}
add this.ready() into the data:function
data: function () {
if(this.ready()){
return Charts.findOne({_id: this.params._id});
}else{
this.render('loading')
}
},
Something using data and waitOn could be a little bit tricky
Template.chartEditPreview.rendered = function() {
Meteor.setTimeout(function(){
drawChart(".chart-container", this.data);
},1000)
}
I have list of menu options, and each menu item has it's own Ractive instance with different template but same shared data. When each selection is changed I am calling teardown() on rendered view instance and render(domElement) on current selection's Ractive instance.
An example Instance is like below, and all follow the same structure.
var View = new Ractive({
template: '#contacts',
data: {
name: 'Contacts',
contacts : dummyData // array data
}
});
And I render them like below
var isRendered = false;
channel.subscribe("menu", function(msg) {
if(msg === "contacts") {
contentHolder.innerHTML = "";
View.render(contentHolder);
isRendered = true;
} else {
if(isRendered) {
View.teardown();
isRendered = false;
console.log(View.get('contacts')); // Here I can see the data.
}
}
});
In first render() call view is rendered as expected, but after calling teardown(), again if I call render() it does not render contacts list data and only displays name property, but was rendered on initial call.
Please help me to fix this.
Just for reference, the question was answered on GitHub
teardown() is a non-reversible call that completely destroys the ractive instance. What you want is detach() function, which will remove the ractive instance from the DOM but not destroy it. You can use it later by calling insert().
In a Kendo app using the Kendo MVVM framework: I have a "global" viewModel which is information that is common to all parts of the app - e.g. the UserState, which has a property isLoggedIn.
Many different Views and ViewModels access the userState object (from what I can see, 1 View is bound to 1 ViewModel in Kendo).
For example, my home page might show the Login button if they are not authenticated. Then all the other screens behave differently once you are logged in, so each ViewModel needs to reference the UserState object. However, if any of them change it then all other Views should update as I used a Kendo Observable object. This does not seem to work.
I set up a simple example here to illustrate the problem: http://jsfiddle.net/rodneyjoyce/uz7ph/11
var app = new kendo.mobile.Application();
userState = (function ()
{
var userStateViewModel = kendo.observable({
isLoggedIn: false
});
function loginUser()
{
userStateViewModel.set("isLoggedIn", true);
alert('Logged in');
};
return {
userStateViewModel: userStateViewModel,
loginUser: loginUser
}
})();
var viewModel1 = kendo.observable({
label: 'ViewModel1',
isLoggedInVM1: function() {
return userState.userStateViewModel.get("isLoggedIn");
},
logIn: function ()
{
//when calling LoginUser from here, the binding is not updated, even though the value is changed (true)
userState.loginUser();
alert('After Login viewModel1.isLoggedInVM1() = ' + viewModel1.isLoggedInVM1() + ' but the binding has not updated');
}
});
alert('Value onLoad = ' + viewModel1.isLoggedInVM1());
//If you uncomment this and call LoginUser from here then afterwards the binding changes to true, but not if you call it from within ViewModel1
//userState.loginUser();
kendo.bind($("#testForm"), viewModel1);
When I call userState.loginUser() to change the value of isLoggedIn in userStateViewModel it does not update. Run and click on the button to see the problem - the binding does not reflect the updated value (but the alert box does). Any help appreciated, thank you.
Note: This is en extension of an earlier question which got me a bit further.
The problem is that userState is a simple object, not an ObservableObject. Because of this, the change event of the userStateViewmodel observable does not trigger the change event for viewmodel1 and the view doesn't update.
You can remedy this by making userState a property of viewModel1, so it is wrapped in an observable (or you could wrap your return object in the IIFE in an observable):
var viewModel1 = kendo.observable({
label: 'ViewModel1',
userState: userState,
isLoggedInVM1: function() {
return userState.userStateViewModel.get("isLoggedIn");
},
logIn: function ()
{
userState.loginUser();
}
});
Take a look at this demo; try commenting the userState property and you'll see the difference.