Part 3 - Loading Older Messages
In this part, we will automatically load older messages when the user scrolls near the top of the list. We will do that by adding an event handler to the onScroll
component property.
Cursor Pagination
Our ChatChannel
class uses the so called cursor-based pagination, which is suitable for live data. Rather than using offsets, cursor-based pagination uses a pointer to a specific item in a list of results. This pointer is called a cursor. In our case, we're going to use the first loaded message as a cursor to load older messages. Add the following ref to the Home
component:
const firstMessageId = React.useRef<number | null>(null)
Then, we will track the first message id in the initial data load. Add the following code to the useEffect
hook:
if (!channel.loaded) {
channel
.getMessages({ limit: 20 })
.then((messages) => {
if (messages !== null) {
firstMessageId.current = messages[0].id
messageListRef.current?.data.append(messages)
}
})
.catch((error) => {
console.error(error)
})
}
onScroll
Event Handler
The onScroll
event handler receives a ListScrollLoaction
object as a parameter. The object contains several fields that denote the current state of the list. In this case, we are going to look at the listOffset
- the distance from the top of the list to the top of the viewport. The value is 0
when the list is scrolled all the way to the top, and -N
when the list is scrolled N
pixels down. We will start loading the newer messages when the scroll position is near the top (> -100
). Our handler should look like this:
const [loadingNewer, setLoadingNewer] = React.useState(false)
// ...
const onScroll = React.useCallback(
(location: ListScrollLocation) => {
// offset is 0 at the top, -totalScrollSize + viewportHeight at the bottom
if (location.listOffset > -100 && !loadingNewer && firstMessageId.current) {
setLoadingNewer(true)
channel
.getMessages({ limit: 20, before: firstMessageId.current })
.then((messages) => {
if (messages !== null) {
firstMessageId.current = messages[0].id
messageListRef.current?.data.prepend(messages)
setLoadingNewer(false)
}
})
.catch((error) => {
console.error(error)
})
}
},
[channel, loadingNewer]
)
The implementation above uses the imperative data
API of the component to prepend a set of messages at the top of the list. Calling the prepend
method automatically adjusts the scroll position to keep the list in the same visible position.
Let's add the event handler to the VirtuosoMessageList
component:
<VirtuosoMessageList<ChatMessage, MessageListContext>
onScroll={onScroll}
context={{ loadingNewer, channel }}
Notice that we're also going to add the loadingNewer
flag into the context prop, so we need to update our interface as well:
interface MessageListContext {
loadingNewer: boolean
channel: ChatChannel
}
Loading Indicator
The code above introduced an additional state flag - loadingNewer
. This flag prevents the component from loading messages when the previous request is still in progress. We're going to make an additional use of this flag by displaying a loading indicator at the top of the list when the component is loading older messages. We are going to make a custom Header component for that purpose. Declare the Header component next to your EmptyPlaceholder
:
const Header: VirtuosoProps['Header'] = ({ context }) => {
return <div style={{ height: 30 }}>{context.loadingNewer ? 'Loading...' : ''}</div>
}
Then, add the component to the VirtuosoMessageList
props:
<VirtuosoMessageList<ChatMessage, MessageListContext>
Header={Header}
If everything works as expected, you should see 'Loading...' when you scroll near the top of the list, and older messages should be loaded.