Lazy data loading (infinite scroll)
Enabling lazy loading of Grid records makes it possible to load the dataset in chunks when they are scrolled into view, instead of loading the complete dataset at once.
Begin by setting the lazyLoad config on the Store to true to enable this
behaviour. You will also need something that initiate the first load, either use
autoLoad or call the load function manually.
The Store will now report to the Grid that there is more records in the store than the ones that has been loaded. When the Grid scrolls close to those records, the Store will initiate a new load request and respond to the Grid with a promise that resolves to the loaded record. While the promise is unresolved, the row will be rendered with a "skeleton" placeholder, indicating that the row is being loaded.
Using an AjaxStore
If your store is an AjaxStore you only need to configure the readUrl, and then you are done. At least on the frontend. For backend implementation please read that chapter a bit further down this page.
new Grid({
store: {
// This will create an AjaxStore
readUrl: 'backend/read',
// This will activate the lazy load functionality
lazyLoad: true,
// This will load the Store initially upon creation
autoLoad: true
}
});
Using a regular Store
If you are using a regular Store, then you will need to implement the requestData function with your own data loading logic. This function will be called each time the Store gets requests for records that has not yet been loaded.
requestData will be called with a single object argument containing
a startIndex and a count value. For a
detailed explanation of startIndex and count, read the "Record indexes and count" chapter below.
class MyStore extends Store {
static configurable = {
lazyLoad: true,
autoLoad: true
};
async requestData({
startIndex,
count
}) {
const response = await fetchData({
startIndex,
count
});
// The requestData function is expected to return an object
// with a data property, which value contains all the records
return {
data: response.data,
// And optionally, but recommended, is to provide a total
// count of all available records
total: response.totalCount
}
}
}
new Grid({
store: new MyStore()
});
Using a Tree store
If the Store is configured with tree set to true there are some very important
differences to know about:
- For the lazy load functionality to recognize that a store is expecting tree data, the
tree config must be set to
true. The autoTree config is not supported. - The isFullyLoaded field can be set from the backend, indicating that a
TreeNode should not initiate any more load requests. If omitted, it will be set to
trueautomatically when a load request returns a smaller number of children than asked for.
When using a Tree store, a load request will be made in the following scenarios:
- The initial load request, either by autoLoad or a separate load call.
- When a TreeNode is rendered and:
- it has expanded set to
trueand isFullyLoaded set tofalse - is the last child of a parent which has isFullyLoaded set to
false
- it has expanded set to
- A TreeNode with expanded set to
falseis manually or programmatically expanded
It is allowed, and in most cases recommended, to include nested data in the server responses. Please read more in the upcoming chapters, which some have a section named "Tree store differences".
Record indexes and Chunks
The Store will initiate a request for data when a record index is requested but not already loaded. This request will include more records, as it would be poor performance to load one record at a time (configurable).
startIndex will be calculated by subtracting chunkSize from the requested record's
index in the Store.
count will be calculated by adding chunkSize to the requested record's index in
the Store and subtracting the startIndex.
As the default chunkSize is set to 100, a normal request will be for a total of 200 records. But, the range can be
smaller if parts of the range is already loaded.
The lazy loading functionality is completely dependent on record indexes. That makes the sorting of the records very important. Please read the upcoming three chapters to learn more.
Tree store differences
For a Tree store, each level of children will be loaded independently. In addition to the params mentioned above, a
parentId param will also be included in the request. For the top level nodes, the parentId will be set to root.
Also, the default chunkSize is set to 50.
Sorting and filtering
Local sorting and filtering is not supported when the store is lazy loaded. Remote sorting and filtering, however, is supported. If you use an AjaxStore configured with remote sorting or filtering (which is done by setting filterParamName or sortParamName), the sorter and/or filter information will be available in the fetch request parameters.
If you do not use an AjaxStore, you should configure the Store with remoteSort
or remoteFilter. This will add a sorters and/or a filters param to the
requestData call.
class MyStore extends Store {
static configurable = {
remoteSort:true,
remoteFilter:true,
lazyLoad: true,
autoLoad: true
};
async requestData({
startIndex,
count,
sorters,
filters
}) {
const response = await fetchData({
startIndex,
count,
sorters,
filters
});
return {
data: response.data,
total: response.totalCount // This should be the total count after filtering
}
}
}
new Grid({
store: new MyStore()
});
A filter or a sort action on the Store will clear all loaded records and a new load request will be made. This is
because a sort or a filter action will change the record's indexes, which makes the loaded dataset invalid.
Adding and removing
Adding and removing records also affects the indexes of the complete dataset. It is important that the backend always is
synced with the Store, which is why an add or remove action that has not yet been synced to the server, will
suspend all further load actions until the sync is completed.
When adding a record, it will be added to the end of the complete dataset. In this case it is important that a total
count is provided in the load request's response from the backend. Otherwise, the loaded dataset will be invalid, and a
complete refresh is required.
When inserting a record, you must be sure that the insertion index is exactly the same as the record will have when it
is requested in a load action. Otherwise, the loaded dataset will be invalid, and a complete refresh is required.
Backend
Regardless of using AjaxStore or another solution, your backend must respond to the request params correctly. The
backend should respond to the startIndex and count param by performing a range extraction that starts with the
record on the startIndex and takes the number of records provided in count.
How you extract your data range is different in different programming languages. Sometimes you can use a startIndex
and a count, and other times you would need to use an endIndex instead of the count. That could look something
like this:
(python)
# Let us say that startIndex = 0 and count = 200
endIndex = startIndex + count # endIndex would be 100
records = allRecords[startIndex:endIndex] # records would be [0, ..., 99]
Here is an (untested) example of a simple endpoint implementation using express.js with a MySQL server:
app.get('/read', async(req, res) => {
const
{
startIndex,
count
} = req.query, // Params provided by the client
total = await new Promise(resolve => {
connection.query('SELECT COUNT(*) from Records', (error, result) => resolve(result));
}),
data = await new Promise(resolve => {
connection.query(`SELECT * from Records ORDER BY id LIMIT ${count} OFFSET ${startIndex}`,
(error, results) => resolve(results));
});
// Return the expected JSON response
res.json({
success: true,
total,
data
});
});
Tree store differences
For Tree data, the requests will be more frequent and a bit different. Each parent will load its own children, which means that for every parent, there will be at least one request made (depending on child count and chunkSize).
The initial load request will have parentId set to root. This request should be responded to with the records that
have no parent, and is within the startIndex and count of the top tree level. When these "roots" are rendered into
the Grid, they will in turn create additional requests to load "children" of their own. To avoid a lot of initial
requests, it is recommended to include nested children in the initial root request.
If the load request gets fewer rows than asked for, it will be interpreted as that parent is fully loaded and the
isFullyLoaded field will be set to true. To be able to return fewer root
rows than asked for, add a isFullyLoaded : false to the response object.
And to avoid additional child requests for parents with all their children included, the same
isFullyLoaded can be set to true on the actual data row object.
Here is what an initial response can look like, with the goal of having only one initial load request:
{
"success": true,
"total": 100000,
"isFullyLoaded" : false,
"data": [
{"id": 1, "parentId": null, "expanded": true, "isFullyLoaded": true, children": [
{"id": 10, "parentId": 1, "expanded": true, "isFullyLoaded: true, children": [
{"id": 100, "parentId": 10},
{"id": 101, "parentId": 10},
...16 more
]},
{"id": 11, "parentId": 1, "expanded": true, "isFullyLoaded": true, "children": [
{"id": 110, "parentId": 11},
{"id": 111, "parentId": 11},
...11 more
]}
]},
{"id": 2, "parentId": null, "expanded": true, "isFullyLoaded": true, "children": [
{"id": 20, "parentId": 2, "expanded": true, "isFullyLoaded": true, "children": [
{"id": 200, "parentId": 20},
{"id": 201, "parentId": 20},
...21 more
]},
{ "id": 21, "parentId": 2, "expanded": true, "children": [
{"id": 210, "parentId": 21},
{"id": 211, "parentId": 21},
...4 more
]}
]},
{"id": 3, "parentId": null, "expanded": false},
{"id": 5, "parentId": null, "expanded": false},
{"id": 6, "parentId": null, "expanded": true },
{"id": 7, "parentId": null, "expanded": true }
]
}
It is currently not supported to include nested children in the data array. The children must be nested inside their
parent's children array.
Not supported
There are a number of Grid features, functions, and configs that are either not supported at all or only work in a limited way. Such information is available in the corresponding documentation. Please note that support will continuously be added where it makes sense to be added. Please let us know of the functionality that lacks support and is most important to you.
LazyLoading is currently not supported in Calendar, Gantt nor TaskBoard.