ruby 60 lines · 9 steps

Building a paginated activity feed in Rails

A single class merges several ActiveRecord sources into one sorted, paginated, renderable stream.

Explained by highlit
1class ActivityFeed
2 include Enumerable
3 
4 Entry = Struct.new(:record, :type, :occurred_at, keyword_init: true) do
5 def to_partial_path
6 "activities/#{type}"
7 end
8 end
9 
10 DEFAULT_PER_PAGE = 25
11 
12 def initialize(user, page: 1, per_page: DEFAULT_PER_PAGE)
13 @user = user
14 @page = [page.to_i, 1].max
15 @per_page = per_page.to_i.clamp(1, 100)
16 end
17 
18 def each(&block)
19 entries.each(&block)
20 end
21 
22 def entries
23 @entries ||= merged.slice(offset, @per_page) || []
24 end
25 
26 def next_page?
27 merged.size > offset + @per_page
28 end
29 
30 def next_page
31 next_page? ? @page + 1 : nil
32 end
33 
34 private
35 
36 def offset
37 (@page - 1) * @per_page
38 end
39 
40 def merged
41 @merged ||= sources
42 .flat_map { |type, scope| wrap(scope, type) }
43 .sort_by { |entry| -entry.occurred_at.to_i }
44 end
45 
46 def sources
47 limit = offset + @per_page
48 {
49 comment: @user.comments.recent.limit(limit),
50 post: @user.posts.published.recent.limit(limit),
51 follow: @user.received_follows.recent.limit(limit)
52 }
53 end
54 
55 def wrap(scope, type)
56 scope.map do |record|
57 Entry.new(record: record, type: type, occurred_at: record.created_at)
58 end
59 end
60end
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Including Enumerable and defining each gives your object the full collection API for free.
  2. 2Normalizing heterogeneous records into a common Struct lets you merge and sort them uniformly.
  3. 3Memoization plus bounded input sizes keeps a multi-source feed cheap to build and safe to paginate.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Building a paginated activity feed in Rails — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code