Forward and backward pagination with relay js - javascript

Relay cursor connection specification says:
hasNextPage will be false if the client is not paginating with first,
or if the client is paginating with first, and the server has
determined that the client has reached the end of the set of edges
defined by their cursors.
What I understand from that spec is that Relay can only paginate in one
direction; forwards or backwards. That seems reasonable for an infinite
scroll pagination implementation.
But how would one implement a sequential pages navigation similar to the stackoverflow questions page which
can navigate both ways?
Is Relay.js suitable for this kind pagination? I can't rely on hasNextPage and hasPreviousPage field which never has true value unless I paginate with first and last which is also very discouraged.

What I understand from that spec is that relay can only paginate with single direction, forwards or backwards.
Correct. The original design was exactly that, an infinite scroll pagination. It was never intended to be used for bi-directional pagination.
But how about sequential pages like stackoverflow questions page?
This is quite tricky do to with the default implementation of Relay as it stands today because, as stated above, it was't designed for bi-directional navigation.
That said, you can still achieve this, though not through optimal means.
Method 1 - Passing page info through Relay
This is probably the easiest method to implement, though the less optimal one. It involves passing the page info to Relay as a GraphQL argument. This requires a new Relay query each time you navigate to a new page.
Example
Assume you have a view displaying a list of members. You fetch a list of member nodes with memberList. If you only want to display 10 members per "page" and we're on page 3, you could do:
app: () => Relay.QL`
fragment on App {
memberList(page: 3, limit: 10) {
...
}
}`
}
In your backend your SQL query would now look something like:
SELECT id, username, email FROM members LIMIT 20, 10;
Where 20 is the starting record: (page-1)*10 and 10 is the limit (how many items after the start). So the above will fetch the records 21-30.
Note that you would need initial values for page and limit. So your final query would be:
prepareVariables() {
return {
page: 1,
limit: 10,
};
},
fragments: {
app: () => Relay.QL`
fragment on App {
memberList(page: $page, limit: $limit) {
...
}
}`
}
}
And when navigating to a new page we need to update the values:
_goToPage = (pg) => {
this.props.relay.setVariables({
page: pg,
});
}
Method 2 - Manually managing cursors
This is a slightly hacky implementation in that it essentially gives you a bi-directional navigation, even though is not natively supported. This approach is slightly more optimal to the first one, but sacrifices the ability to jump to a page directly.
Note that we cannot use both first and last:
Including a value for both first and last is strongly discouraged, as it is likely to lead to confusing queries and results.
Hence we would construct our query normally. Something like:
app: () => Relay.QL`
fragment on App {
memberList(first: 10, after: $cursor) {
...
}
}`
}
where the initial value for $cursor is null.
As you navigate to the next page and set the value for $cursor to be the cursor of the last member node, you also save the value in a local stack variable.
This way, whenever you navigate back you would simply pop the last cursor from the stack and use that for your after argument. It requires some extra logic in your application, but this is definitely doable.

Based on Method 2 given by #Chris,
This is my solution in VueJS, it's roughly the same for ReactJS as well.
loadPage(type) {
const { endCursor } = this.pageInfo
let cursor = null
if(type === 'previous') {
this.cursorStack = this.cursorStack.slice(0, this.cursorStack.length - 1)
cursor = this.cursorStack[this.cursorStack.length - 1]
}
else {
cursor = endCursor
this.cursorStack = [...this.cursorStack, cursor]
}
this.fetchMore({
variables: { cursor: cursor },
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult) return previousResult
return fetchMoreResult
}
})
},
In template:
<button :disabled="!cursorStack.length" #click="loadPage('previous')" />
<button :disabled="!pageInfo.hasNextPage" #click="loadPage('next')" />

Related

Is it possible to reset specific item in Apollo Client Cache?

I have a query which uses fetchMore and the relayPagination which works fine for lazy loading when using the variables page and perPage, the issue now is I'm trying to refresh the query whenever i update the other variables for filtering like date and type which values can be either debited or credited. It fetches the data, but then appends the incoming data to apollo cache instead of replacing the old data with the new one.
for example in this sample typePolicy
PaginatedBooks: {
fields: {
allBooks: {
merge: relayPagination()
}
}
}
AllProducts: {
// Singleton types that have no identifying field can use an empty
// array for their keyFields.
keyFields: [],
},
I only want to reset PaginatedBooks
I've tried use fetchPolicy of no-cache but this stops pagination and fetchMore from working as i can't merging existing and incoming data. I opted to use client.resetStore(): https://www.apollographql.com/docs/react/api/core/ApolloClient/#ApolloClient.resetStore
but this also refetches other active queries and causes the ui to flicker. So far looking through the documentation and github repo I can't seem to find anything or anyone who has tried to do something similar, so I'm hoping I can get some insight and probably be offered a better solution. Thanks in advance

How can I repeatedly access a Vue prop without tanking performance?

I'm working on a Vue/Vuetify app that has a performance problem. I've created a custom component that wraps around a standard Vuetify v-data-table component. It works fine for small amounts of data, but giving it moderate to large amounts of data causes Firefox to hang and Chrome to crash.
Here's a slightly simplified version of my code:
<script>
import ...
export default {
props: {
theValues: Array,
// other props
},
computed: {
theKeys: function() {
return this.schema.map(col => col.col);
},
schema: function() {
return this.theValues[0].schema;
},
dataForDataTable: function() {
console.time('test');
let result = [];
for (let i = 0; i < theValues[0].data.length; i++) {
let resultObj = {};
for (let j = 0; j < theKeys.length; j++) {
// The "real" logic; this causes the browser to hang/crash
// resultObj[theKeys[j]] = theValues[0].data[i][j];
// Test operation to diagnose the problem
resultObj[theKeys[j]] = Math.floor(Math.random() * Math.floor(99999));
}
result.push(resultObj);
}
console.timeEnd('test');
// For ~30k rows, timer reports that:
// Real values can take over 250,000 ms
// Randomly generated fake values take only 7 ms
return result;
},
// other computed
},
// other Vue stuff
</script>
And here's an example of what theValues actually looks like:
[
{
data: [
[25389, 24890, 49021, ...] <-- 30,000 elements
],
schema: [
{
col: "id_number",
type: "integer"
}
]
}
]
The only meaningful difference I see between the fast code and the slow code is that the slow code accesses the prop theValues on each iteration whereas the fast code doesn't touch any complicated part of Vue. (It does use theKeys, but the performance doesn't change even if I create a local deep copy of theKeys inside the function.)
Based on this, it seems like the problem is not that the data table component can't handle the amount of data I'm sending, or that the nested loops are inherently too inefficient. My best guess is that reading from the prop so much is somehow slowing Vue itself down, but I'm not 100% sure of that.
But I do ultimately need to get the information from the prop into the table. What can I do to make this load at a reasonable speed?
The performance problem is actually a symptom of your loop code rather than Vue. The most expensive data access is in your inner loop in dataForDataTable():
for (i...) {
for (j...) {
theValues[0].data[i][j] // ~50 ms average (expensive)
}
}
// => long hang for 32K items
An optimization would be to cache the array outside your loop, which dramatically improves the loop execution time and resolves the hang:
const myData = theValues[0].data
for (i...) {
for (j...) {
myData[i][j] // ~0.00145 ms average
}
}
// => ~39 ms for 32K items
demo 1
Note the same result can be computed without loops, using JavaScript APIs. This affords readability and reduced lines of code at a slight performance cost (~1ms). Specifically, use Array.prototype.map to map each value from data to an object property, obtained by Array.prototype.reduce on theKeys:
theValues[0].data
.map(values => theKeys.reduce((obj,key,i) => {
obj[key] = values[i]
return obj
}, {}))
// => ~40 ms for 32K items
demo 2
Times above measured on 2016 MacBook Pro - 2.7GHz i7, Chrome 87. Codesandbox demos might show a vast variance from the above.
Tip 1
Original text:
Accessing the prop (data) should not be an issue. Yes, data are reactive but reading it should be very efficient (Vue is just "making notes" that you are using that data for rendering)
Well it seems I was clearly wrong here...
Your component is getting data by prop but it is very probable that the data is reactive in parent component (coming from Vuex or stored in parent's data). Problem with Vue 2 reactivity system is it is based on Object.defineProperty and this system does not allow to intercept indexed array access (Vue is not able to detect code like arr[1] as an template dependency). To workaround this, if object property (theValues[0].data in your code) is accessed, it checks whether the value is array and if yes it iterates the whole array (plus all nested arrays) to mark the items as dependencies - you can read more in depth explanation here
One solution to this problem is to create local variable let data = theValues[0].data as tony19 suggests. Now the .data Vue getter is not called every time and the performance is fixed...
But if your data is immutable (never change), just use Object.freeze() and Vue will not try to detect changes of such data. This will not only make your code faster but also saves a ton of memory in case of large lists of objects
Note that this problem is fixed in Vue 3 as it uses very different reactivity system based on ES6 proxies...
Tip 2
Although Vue computed properties are highly optimized, there is still some code running every time you access the property (checking whether underlying data is dirty and computed prop needs reevaluate) and this work adds up if you use it in a tight loop as in your case...
Try to make local copy of the theKeys computed prop before executing the loop (shallow copy is enough, no need for a deep copy)
See this really good video from Vue core member
Of course the same issue applies to accessing the dataForDataTable computed prop from the template. I encourage You to try to use watcher instead of computed to implement same logic as dataForDataTable and store it's result in data to see if it makes any difference...

Pagination in React with fetch()

I am fetching objects from an API simply with
getData() {
fetch('API_URL').then(res => res.json()).then(data => {
this.setState({ jobs: data.jobs });
}).catch(console.log);
}
componentDidMount() {
this.getData();
}
But I want to be able to click a button to load more objects.
I guess that I should create the API such that it only prints e.g. 10 objects at a time and keeps a variable "pageNumber" and if I click the "load more" button, it should fetch from next page and append the new objects.
Is this the right approach? Or can I simply load thousands of objects when mounting the component and use React to limit how many are seen? I guess this would be a very inefficient way to fetch the data, but I am not really sure how well React handles this.
Can I, for instance, in my API just keep print X number of objects and in my fetch request decide how many objects are loaded? So when I have pressed "load more" 2 times, the API endpoint will return 30 objects instead of only 10 - even though the first 20 have already been fetched before?
I have tried searching for pagination in React, but I only get a lot of pagination libraries. I just want to understand the very basic initial fetching and the fetching following clicking load more.
Edit
But if I have an API endpoint which returns something like
{
page: 1,
objectsPerPage: 10,
numPages: 30,
objects: [
...
]
}
and I am initially retrieving the objects on page 1, and every time I click "Load more", I increase the page number and append the objects on the next page (with this.setState({ jobs: this.state.jobs.concat(data.jobs) }); where data.jobs is the list of objects on the next page, then I would be afraid that new objects are created in the database, so which objects belong to which page is completely screwed up and not all or some duplicates are shown.
Yes, it is the right approach to have a pageNumber on the API, so you only look for the registers you don't have.
On the other size if your data is not too big you can make the fake pagination having all the objects in memory and only showing the ones that you are interested in.
I don't recommend to increase the number of objects you are looking for because you are not getting the advantage of the ones you have already fetched and everytime you increase the number, the request will last more and more.

Using Apollo, how can I distinguish newly created objects?

My use case is the following:
I have a list of comments that I fetch using a GraphQL query. When the user writes a new comment, it gets submitted using a GraphQL mutation. Then I'm using updateQueries to append the new comment to the list.
In the UI, I want to highlight the newly created comments. I tried to add a property isNew: true on the new comment in mutationResult, but Apollo removes the property before saving it to the store (I assume that's because the isNew field isn't requested in the gql query).
Is there any way to achieve this?
Depends on what do you mean by "newly created objects". If it is authentication based application with users that can login, you can compare the create_date of comment with some last_online date of user. If the user is not forced to create an account, you can store such an information in local storage or cookies (when he/she last time visited the website).
On the other hand, if you think about real-time update of comments list, I would recommend you take a look at graphql-subscriptions with use of websockets. It provides you with reactivity in your user interface with use of pub-sub mechanism. Simple use case - whenever new comment is added to a post, every user/viewer is notified about that, the comment can be appended to the comments list and highlighted in a way you want it.
In order to achieve this, you could create a subscription called newCommentAdded, which client would subscribe to and every time a new comment is being created, the server side of the application would notify (publish) about that.
Simple implementation of such a case could look like that
const Subscription = new GraphQLObjectType({
name: 'Subscription',
fields: {
newCommentAdded: {
type: Comment, // this would be your GraphQLObject type for Comment
resolve: (root, args, context) => {
return root.comment;
}
}
}
});
// then create graphql schema with use of above defined subscription
const graphQLSchema = new GraphQLSchema({
query: Query, // your query object
mutation: Mutation, // your mutation object
subscription: Subscription
});
The above part is only the graphql-js part, however it is necessary to create a SubscriptionManager which uses the PubSub mechanism.
import { SubscriptionManager, PubSub } from 'graphql-subscriptions';
const pubSub = new PubSub();
const subscriptionManagerOptions = {
schema: graphQLSchema,
setupFunctions: {
newCommentAdded: (options, args) => {
newCommentAdded: {
filter: ( payload ) => {
// return true -> means that the subscrition will be published to the client side in every single case you call the 'publish' method
// here you can provide some conditions when to publish the result, like IDs of currently logged in user to whom you would publish the newly created comment
return true;
}
}
},
pubsub: pubSub
});
const subscriptionManager = new SubscriptionManager(subscriptionManagerOptions);
export { subscriptionManager, pubSub };
And the final step is to publish newly created comment to the client side when it is necessary, via above created SubscriptionManager instance. You could do that in the mutation method creating new comment, or wherever you need
// here newComment is your comment instance
subscriptionManager.publish( 'newCommentAdded', { comment: newComment } );
In order to make the pub-sub mechanism with use of websockets, it is necessary to create such a server alongside your main server. You can use the subscriptions-transport-ws module.
The biggest advantage of such a solution is that it provides reactivity in your application (real-time changes applied to comments list below post etc.). I hope that this might be a good choice for your use case.
I could see this being done a couple of ways. You are right that Apollo will strip the isNew value because it is not a part of your schema and is not listed in the queries selection set. I like to separate the concerns of the server data that is managed by apollo and the front-end application state that lends itself to using redux/flux or even more simply by managing it in your component's state.
Apollo gives you the option to supply your own redux store. You can allow apollo to manage its data fetching logic and then manage your own front-end state alongside it. Here is a write up discussing how you can do this: http://dev.apollodata.com/react/redux.html.
If you are using React, you might be able to use component lifecycle hooks to detect when new comments appear. This might be a bit of a hack but you could use componentWillReceiveProps to compare the new list of comments with the old list of comments, identify which are new, store that in the component state, and then invalidate them after a period of time using setTimeout.
componentWillReceiveProps(newProps) {
// Compute a diff.
const oldCommentIds = new Set(this.props.data.allComments.map(comment => comment.id));
const nextCommentIds = new Set(newProps.data.allComments.map(comment => comment.id));
const newCommentIds = new Set(
[...nextCommentIds].filter(commentId => !oldCommentIds.has(commentId))
);
this.setState({
newCommentIds
});
// invalidate after 1 second
const that = this;
setTimeout(() => {
that.setState({
newCommentIds: new Set()
})
}, 1000);
}
// Then somewhere in your render function have something like this.
render() {
...
{
this.props.data.allComments.map(comment => {
const isNew = this.state.newCommentIds.has(comment.id);
return <CommentComponent isNew={isNew} comment={comment} />
})
}
...
}
The code above was right off the cuff so you might need to play around a bit. Hope this helps :)

In Flux architecture, how do you manage Store lifecycle?

I'm reading about Flux but the example Todo app is too simplistic for me to understand some key points.
Imagine a single-page app like Facebook that has user profile pages. On each user profile page, we want to show some user info and their last posts, with infinite scroll. We can navigate from one user profile to another one.
In Flux architecture, how would this correspond to Stores and Dispatchers?
Would we use one PostStore per user, or would we have some kind of a global store? What about dispatchers, would we create a new Dispatcher for each “user page”, or would we use a singleton? Finally, what part of the architecture is responsible for managing the lifecycle of “page-specific” Stores in response to route change?
Moreover, a single pseudo-page may have several lists of data of the same type. For example, on a profile page, I want to show both Followers and Follows. How can a singleton UserStore work in this case? Would UserPageStore manage followedBy: UserStore and follows: UserStore?
In a Flux app there should only be one Dispatcher. All data flows through this central hub. Having a singleton Dispatcher allows it to manage all Stores. This becomes important when you need Store #1 update itself, and then have Store #2 update itself based on both the Action and on the state of Store #1. Flux assumes this situation is an eventuality in a large application. Ideally this situation would not need to happen, and developers should strive to avoid this complexity, if possible. But the singleton Dispatcher is ready to handle it when the time comes.
Stores are singletons as well. They should remain as independent and decoupled as possible -- a self-contained universe that one can query from a Controller-View. The only road into the Store is through the callback it registers with the Dispatcher. The only road out is through getter functions. Stores also publish an event when their state has changed, so Controller-Views can know when to query for the new state, using the getters.
In your example app, there would be a single PostStore. This same store could manage the posts on a "page" (pseudo-page) that is more like FB's Newsfeed, where posts appear from different users. Its logical domain is the list of posts, and it can handle any list of posts. When we move from pseudo-page to pseudo-page, we want to reinitialize the state of the store to reflect the new state. We might also want to cache the previous state in localStorage as an optimization for moving back and forth between pseudo-pages, but my inclination would be to set up a PageStore that waits for all other stores, manages the relationship with localStorage for all the stores on the pseudo-page, and then updates its own state. Note that this PageStore would store nothing about the posts -- that's the domain of the PostStore. It would simply know whether a particular pseudo-page has been cached or not, because pseudo-pages are its domain.
The PostStore would have an initialize() method. This method would always clear the old state, even if this is the first initialization, and then create the state based on the data it received through the Action, via the Dispatcher. Moving from one pseudo-page to another would probably involve a PAGE_UPDATE action, which would trigger the invocation of initialize(). There are details to work out around retrieving data from the local cache, retrieving data from the server, optimistic rendering and XHR error states, but this is the general idea.
If a particular pseudo-page does not need all the Stores in the application, I'm not entirely sure there is any reason to destroy the unused ones, other than memory constraints. But stores don't typically consume a great deal of memory. You just need to make sure to remove the event listeners in the Controller-Views you are destroying. This is done in React's componentWillUnmount() method.
(Note: I have used ES6 syntax using JSX Harmony option.)
As an exercise, I wrote a sample Flux app that allows to browse Github users and repos.
It is based on fisherwebdev's answer but also reflects an approach I use for normalizing API responses.
I made it to document a few approaches I have tried while learning Flux.
I tried to keep it close to real world (pagination, no fake localStorage APIs).
There are a few bits here I was especially interested in:
It uses Flux architecture and react-router;
It can show user page with partial known info and load details on the go;
It supports pagination both for users and repos;
It parses Github's nested JSON responses with normalizr;
Content Stores don't need to contain a giant switch with actions;
“Back” is immediate (because all data is in Stores).
How I Classify Stores
I tried to avoid some of the duplication I've seen in other Flux example, specifically in Stores.
I found it useful to logically divide Stores into three categories:
Content Stores hold all app entities. Everything that has an ID needs its own Content Store. Components that render individual items ask Content Stores for the fresh data.
Content Stores harvest their objects from all server actions. For example, UserStore looks into action.response.entities.users if it exists regardless of which action fired. There is no need for a switch. Normalizr makes it easy to flatten any API reponses to this format.
// Content Stores keep their data like this
{
7: {
id: 7,
name: 'Dan'
},
...
}
List Stores keep track of IDs of entities that appear in some global list (e.g. “feed”, “your notifications”). In this project, I don't have such Stores, but I thought I'd mention them anyway. They handle pagination.
They normally respond to just a few actions (e.g. REQUEST_FEED, REQUEST_FEED_SUCCESS, REQUEST_FEED_ERROR).
// Paginated Stores keep their data like this
[7, 10, 5, ...]
Indexed List Stores are like List Stores but they define one-to-many relationship. For example, “user's subscribers”, “repository's stargazers”, “user's repositories”. They also handle pagination.
They also normally respond to just a few actions (e.g. REQUEST_USER_REPOS, REQUEST_USER_REPOS_SUCCESS, REQUEST_USER_REPOS_ERROR).
In most social apps, you'll have lots of these and you want to be able to quickly create one more of them.
// Indexed Paginated Stores keep their data like this
{
2: [7, 10, 5, ...],
6: [7, 1, 2, ...],
...
}
Note: these are not actual classes or something; it's just how I like to think about Stores.
I made a few helpers though.
StoreUtils
createStore
This method gives you the most basic Store:
createStore(spec) {
var store = merge(EventEmitter.prototype, merge(spec, {
emitChange() {
this.emit(CHANGE_EVENT);
},
addChangeListener(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
}));
_.each(store, function (val, key) {
if (_.isFunction(val)) {
store[key] = store[key].bind(store);
}
});
store.setMaxListeners(0);
return store;
}
I use it to create all Stores.
isInBag, mergeIntoBag
Small helpers useful for Content Stores.
isInBag(bag, id, fields) {
var item = bag[id];
if (!bag[id]) {
return false;
}
if (fields) {
return fields.every(field => item.hasOwnProperty(field));
} else {
return true;
}
},
mergeIntoBag(bag, entities, transform) {
if (!transform) {
transform = (x) => x;
}
for (var key in entities) {
if (!entities.hasOwnProperty(key)) {
continue;
}
if (!bag.hasOwnProperty(key)) {
bag[key] = transform(entities[key]);
} else if (!shallowEqual(bag[key], entities[key])) {
bag[key] = transform(merge(bag[key], entities[key]));
}
}
}
PaginatedList
Stores pagination state and enforces certain assertions (can't fetch page while fetching, etc).
class PaginatedList {
constructor(ids) {
this._ids = ids || [];
this._pageCount = 0;
this._nextPageUrl = null;
this._isExpectingPage = false;
}
getIds() {
return this._ids;
}
getPageCount() {
return this._pageCount;
}
isExpectingPage() {
return this._isExpectingPage;
}
getNextPageUrl() {
return this._nextPageUrl;
}
isLastPage() {
return this.getNextPageUrl() === null && this.getPageCount() > 0;
}
prepend(id) {
this._ids = _.union([id], this._ids);
}
remove(id) {
this._ids = _.without(this._ids, id);
}
expectPage() {
invariant(!this._isExpectingPage, 'Cannot call expectPage twice without prior cancelPage or receivePage call.');
this._isExpectingPage = true;
}
cancelPage() {
invariant(this._isExpectingPage, 'Cannot call cancelPage without prior expectPage call.');
this._isExpectingPage = false;
}
receivePage(newIds, nextPageUrl) {
invariant(this._isExpectingPage, 'Cannot call receivePage without prior expectPage call.');
if (newIds.length) {
this._ids = _.union(this._ids, newIds);
}
this._isExpectingPage = false;
this._nextPageUrl = nextPageUrl || null;
this._pageCount++;
}
}
PaginatedStoreUtils
createListStore, createIndexedListStore, createListActionHandler
Makes creation of Indexed List Stores as simple as possible by providing boilerplate methods and action handling:
var PROXIED_PAGINATED_LIST_METHODS = [
'getIds', 'getPageCount', 'getNextPageUrl',
'isExpectingPage', 'isLastPage'
];
function createListStoreSpec({ getList, callListMethod }) {
var spec = {
getList: getList
};
PROXIED_PAGINATED_LIST_METHODS.forEach(method => {
spec[method] = function (...args) {
return callListMethod(method, args);
};
});
return spec;
}
/**
* Creates a simple paginated store that represents a global list (e.g. feed).
*/
function createListStore(spec) {
var list = new PaginatedList();
function getList() {
return list;
}
function callListMethod(method, args) {
return list[method].call(list, args);
}
return createStore(
merge(spec, createListStoreSpec({
getList: getList,
callListMethod: callListMethod
}))
);
}
/**
* Creates an indexed paginated store that represents a one-many relationship
* (e.g. user's posts). Expects foreign key ID to be passed as first parameter
* to store methods.
*/
function createIndexedListStore(spec) {
var lists = {};
function getList(id) {
if (!lists[id]) {
lists[id] = new PaginatedList();
}
return lists[id];
}
function callListMethod(method, args) {
var id = args.shift();
if (typeof id === 'undefined') {
throw new Error('Indexed pagination store methods expect ID as first parameter.');
}
var list = getList(id);
return list[method].call(list, args);
}
return createStore(
merge(spec, createListStoreSpec({
getList: getList,
callListMethod: callListMethod
}))
);
}
/**
* Creates a handler that responds to list store pagination actions.
*/
function createListActionHandler(actions) {
var {
request: requestAction,
error: errorAction,
success: successAction,
preload: preloadAction
} = actions;
invariant(requestAction, 'Pass a valid request action.');
invariant(errorAction, 'Pass a valid error action.');
invariant(successAction, 'Pass a valid success action.');
return function (action, list, emitChange) {
switch (action.type) {
case requestAction:
list.expectPage();
emitChange();
break;
case errorAction:
list.cancelPage();
emitChange();
break;
case successAction:
list.receivePage(
action.response.result,
action.response.nextPageUrl
);
emitChange();
break;
}
};
}
var PaginatedStoreUtils = {
createListStore: createListStore,
createIndexedListStore: createIndexedListStore,
createListActionHandler: createListActionHandler
};
createStoreMixin
A mixin that allows components to tune in to Stores they're interested in, e.g. mixins: [createStoreMixin(UserStore)].
function createStoreMixin(...stores) {
var StoreMixin = {
getInitialState() {
return this.getStateFromStores(this.props);
},
componentDidMount() {
stores.forEach(store =>
store.addChangeListener(this.handleStoresChanged)
);
this.setState(this.getStateFromStores(this.props));
},
componentWillUnmount() {
stores.forEach(store =>
store.removeChangeListener(this.handleStoresChanged)
);
},
handleStoresChanged() {
if (this.isMounted()) {
this.setState(this.getStateFromStores(this.props));
}
}
};
return StoreMixin;
}
So in Reflux the concept of the Dispatcher is removed and you only need to think in terms of data flow through actions and stores. I.e.
Actions <-- Store { <-- Another Store } <-- Components
Each arrow here models how the data flow is listened to, which in turn means that the data flows in the opposite direction. The actual figure for data flow is this:
Actions --> Stores --> Components
^ | |
+----------+------------+
In your use case, if I understood correctly, we need a openUserProfile action that initiates the user profile loading and switching the page and also some posts loading actions that will load posts when the user profile page is opened and during the infinite scroll event. So I'd imagine we have the following data stores in the application:
A page data store that handles switching pages
A user profile data store that loads the user profile when the page is opened
A posts list data store that loads and handles the visible posts
In Reflux you'd set it up like this:
The actions
// Set up the two actions we need for this use case.
var Actions = Reflux.createActions(['openUserProfile', 'loadUserProfile', 'loadInitialPosts', 'loadMorePosts']);
The page store
var currentPageStore = Reflux.createStore({
init: function() {
this.listenTo(openUserProfile, this.openUserProfileCallback);
},
// We are assuming that the action is invoked with a profileid
openUserProfileCallback: function(userProfileId) {
// Trigger to the page handling component to open the user profile
this.trigger('user profile');
// Invoke the following action with the loaded the user profile
Actions.loadUserProfile(userProfileId);
}
});
The user profile store
var currentUserProfileStore = Reflux.createStore({
init: function() {
this.listenTo(Actions.loadUserProfile, this.switchToUser);
},
switchToUser: function(userProfileId) {
// Do some ajaxy stuff then with the loaded user profile
// trigger the stores internal change event with it
this.trigger(userProfile);
}
});
The posts store
var currentPostsStore = Reflux.createStore({
init: function() {
// for initial posts loading by listening to when the
// user profile store changes
this.listenTo(currentUserProfileStore, this.loadInitialPostsFor);
// for infinite posts loading
this.listenTo(Actions.loadMorePosts, this.loadMorePosts);
},
loadInitialPostsFor: function(userProfile) {
this.currentUserProfile = userProfile;
// Do some ajax stuff here to fetch the initial posts then send
// them through the change event
this.trigger(postData, 'initial');
},
loadMorePosts: function() {
// Do some ajaxy stuff to fetch more posts then send them through
// the change event
this.trigger(postData, 'more');
}
});
The components
I'm assuming you have a component for the whole page view, the user profile page and the posts list. The following needs to be wired up:
The buttons that opens up the user profile need to invoke the Action.openUserProfile with the correct id during it's click event.
The page component should be listening to the currentPageStore so it knows which page to switch to.
The user profile page component needs to listen to the currentUserProfileStore so it knows what user profile data to show
The posts list needs to listen to the currentPostsStore to receive the loaded posts
The infinite scroll event needs to call the Action.loadMorePosts.
And that should be pretty much it.

Categories