Quickstart
A quick dive into getting started with Lore
A quick dive into getting started with Lore
In this step we'll update our strategy for displaying new tweets so that they appear immediately.
You can view the finished code for this step by checking out the
optimistic.3
branch of the completed project.
While new tweets now show up at the top of the Feed, they only show up after the server confirms they exist. This means there's a delay between when a tweet is created and when it appears in the Feed.
While that's not necessarily a bad experience, it's not the one we want for this application.
We can solve this by using Lore's built-in support for optimistic updates to get tweets to appear immediately.
To start, open the Feed
component and update the selectOther()
callback to look like this:
// src/components/Feed.js
render() {
...
return (
<div className="feed">
...
<InfiniteScrollingList
...
selectOther={(getState) => {
return getState('tweet.all', {
where: function(tweet) {
const isOptimistic = !tweet.id;
const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
return isOptimistic || isNew;
},
sortBy: function(model) {
return -moment(model.data.createdAt).unix();
}
});
}}
/>
</div>
);
}
In the code above, we've updated our where()
filter to now include tweets that don't yet exist, and we did this by checking whether the tweet
has an id
.
Since the API server is responsible for assigning an id
to resources, then we know that any tweets we have that don't have an id
are optimistic; they represent data that is currently in the process of being created.
If you now refresh the browser and try to create a new tweet, the application will crash, and if you check the console, you'll see both a warning and an error:
Warning: Each child in an array or iterator should have a unique "key" prop.
...
Uncaught Error: Invalid call to 'getState('user.byId')'. Missing required attribute 'id'.
We'll fix the warning now, and solve the error in the next step.
The warning is happening because up until now, we've been using the id
of our models as the key when rendering a list, which you can see in this code from the Feed
component:
// src/components/Feed.js
<InfiniteScrollingList
...
row={(tweet) => {
return (
<Tweet key={tweet.id} tweet={tweet} />
);
}}
/>
The problem is that optimistic data doesn't have an id, which means we're providing undefined
as a value for the key. And React doesn't like that, so it issues a warning.
To solve this, we need to provide an alternate key, and we can do that by using the cid
for the model.
The
cid
property stands for "client id", and it exists for the sole purpose of supporting optimistic updates. Every model in Lore is assigned one, and the value is unique to that model. While there may be times a model won't have anid
, it will always have acid
.You can read more about the
cid
property here.
Update the key to look like this:
<InfiniteScrollingList
...
row={(tweet) => {
return (
<Tweet key={tweet.id || tweet.cid} tweet={tweet} />
);
}}
/>
With that change in place, the warning should go away. In the next step we'll solve the error, and restore functionality to the application.
If everything went well, your application should now look like this (exactly the same).
Below is a list of files modified during this step.
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import _ from 'lodash';
import moment from 'moment';
import InfiniteScrollingList from './InfiniteScrollingList';
import Tweet from './Tweet';
export default createReactClass({
displayName: 'Feed',
getInitialState() {
return {
timestamp: new Date().toISOString()
};
},
render() {
const { timestamp } = this.state;
return (
<div className="feed">
<h2 className="title">
Feed
</h2>
<InfiniteScrollingList
select={(getState) => {
return getState('tweet.find', {
where: {
where: {
createdAt: {
'<=': timestamp
}
}
},
pagination: {
sort: 'createdAt DESC',
page: 1
}
});
}}
row={(tweet) => {
return (
<Tweet key={tweet.id || tweet.cid} tweet={tweet} />
);
}}
refresh={(page, getState) => {
return getState('tweet.find', page.query);
}}
selectNextPage={(lastPage, getState) => {
const lastPageNumber = lastPage.query.pagination.page;
return getState('tweet.find', _.defaultsDeep({
pagination: {
page: lastPageNumber + 1
}
}, lastPage.query));
}}
selectOther={(getState) => {
return getState('tweet.all', {
where: function(tweet) {
const isOptimistic = !tweet.id;
const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
return isOptimistic || isNew;
},
sortBy: function(model) {
return -moment(model.data.createdAt).unix();
}
});
}}
/>
</div>
);
}
});
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import moment from 'moment';
import InfiniteScrollingList from './InfiniteScrollingList';
import Tweet from './Tweet';
class Feed extends React.Component {
constructor(props) {
super(props);
this.state = {
timestamp: new Date().toISOString()
};
}
render() {
const { timestamp } = this.state;
return (
<div className="feed">
<h2 className="title">
Feed
</h2>
<InfiniteScrollingList
select={(getState) => {
return getState('tweet.find', {
where: {
where: {
createdAt: {
'<=': timestamp
}
}
},
pagination: {
sort: 'createdAt DESC',
page: 1
}
});
}}
row={(tweet) => {
return (
<Tweet key={tweet.id || tweet.cid} tweet={tweet} />
);
}}
refresh={(page, getState) => {
return getState('tweet.find', page.query);
}}
selectNextPage={(lastPage, getState) => {
const lastPageNumber = lastPage.query.pagination.page;
return getState('tweet.find', _.defaultsDeep({
pagination: {
page: lastPageNumber + 1
}
}, lastPage.query));
}}
selectOther={(getState) => {
return getState('tweet.all', {
where: function(tweet) {
const isOptimistic = !tweet.id;
const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
return isOptimistic || isNew;
},
sortBy: function(model) {
return -moment(model.data.createdAt).unix();
}
});
}}
/>
</div>
);
}
}
export default Feed;
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import moment from 'moment';
import InfiniteScrollingList from './InfiniteScrollingList';
import Tweet from './Tweet';
class Feed extends React.Component {
constructor(props) {
super(props);
this.state = {
timestamp: new Date().toISOString()
};
}
render() {
const { timestamp } = this.state;
return (
<div className="feed">
<h2 className="title">
Feed
</h2>
<InfiniteScrollingList
select={(getState) => {
return getState('tweet.find', {
where: {
where: {
createdAt: {
'<=': timestamp
}
}
},
pagination: {
sort: 'createdAt DESC',
page: 1
}
});
}}
row={(tweet) => {
return (
<Tweet key={tweet.id || tweet.cid} tweet={tweet} />
);
}}
refresh={(page, getState) => {
return getState('tweet.find', page.query);
}}
selectNextPage={(lastPage, getState) => {
const lastPageNumber = lastPage.query.pagination.page;
return getState('tweet.find', _.defaultsDeep({
pagination: {
page: lastPageNumber + 1
}
}, lastPage.query));
}}
selectOther={(getState) => {
return getState('tweet.all', {
where: function(tweet) {
const isOptimistic = !tweet.id;
const isNew = moment(tweet.data.createdAt).diff(timestamp) > 0;
return isOptimistic || isNew;
},
sortBy: function(model) {
return -moment(model.data.createdAt).unix();
}
});
}}
/>
</div>
);
}
}
export default Feed;
In the next section we'll fix the error and restore functionality to the application.