Build low-latency Vision AI applications using our new open-source Vision AI SDK. ⭐️ on GitHub ->

Build an Instagram-Style For-You Feed in React Native

Social and community platforms like Instagram recommend content to users on their timelines based on interests, past activity, popularity, location, language, and more. Discover how to implement a real-time timeline update system (popularly known as For You) for Android and iOS using React Native.

Amos G.
Amos G.
Published February 9, 2026
Instagram-style feed

Personalized content feeds keep users engaged by surfacing content they're most likely to enjoy. In this tutorial, you’ll build an Instagram-style “For You” feed in React Native Expo that recommends images and videos to users based on their interests and content popularity.

Get a free Stream account and use your API credentials to get started.

What You Will Build

Feed UIs

To keep the app simple, we will create only two screens: Home and For You.

  • Home: Showcases your post, likes, comments, and posts from the people you follow.
  • For You: Displays the content you might be interested in on your timeline. Users can follow and unfollow content on their “for you” timeline.

The final project is a complete application powered by v3 of the Feed SDK by Stream, with main features including realtime timeline feeds for content exploration, comments, reactions, and more.

Here is a demo of what you will create:

Clone and Run the Completed Project

Although the app is simple with only two screens, it still has several components and features ready for you to test out of the box. To download and run the demo successfully, you should ensure that Node.js version 22 or later is installed. Feel free to use Yarn if you prefer.

Run the following commands to clone the project from GitHub and install the required dependencies.

bash
1
2
3
4
5
6
7
8
9
10
11
12
git clone https://github.com/GetStream/stream-tutorial-projects.git cd ReactNative/RN_ForYou_Feeds # Install dependencies yarn install # Install the feeds v3 React Native SDK yarn add @stream-io/feeds-react-native-sdk # Add the React Native Network Info API for Android and iOS yarn add @react-native-community/netinfo

Running the app requires an authenticated user with an API_KEY and a token. For this demo, we will use a hard-coded API credential that works only in Stream’s tutorial environment. Check the project’s root and find user.ts. Its content is the same as the code snippet below.

bash
1
2
3
4
5
6
7
export const API_KEY = '8kyvu2hwe68n'; export const CURRENT_USER = { id: '4aa3cd66-0124-4129-80cd-b86877114d58', name: 'Eggplant Spaghetti', token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3Byb250by5nZXRzdHJlYW0uaW8iLCJzdWIiOiJ1c2VyLzRhYTNjZDY2LTAxMjQtNDEyOS04MGNkLWI4Njg3NzExNGQ1OCIsInVzZXJfaWQiOiI0YWEzY2Q2Ni0wMTI0LTQxMjktODBjZC1iODY4NzcxMTRkNTgiLCJ2YWxpZGl0eV9pbl9zZWNvbmRzIjo2MDQ4MDAsImlhdCI6MTc3MDIwMzM4MCwiZXhwIjoxNzcwODA4MTgwfQ.Oce5b4ZEzXCUOxqINg7muzhOMd42Z-WDPYwC3OumH7c', };

Note: We are using a hard-coded token and API_KEY for testing. They should not be used in a production environment.

Prepare To Run the App on iOS

Xcode and an iOS simulator are required to preview the project. You can also run it on a Physical iOS device, such as an iPhone or iPad.

The demo has been tested with Xcode 26 and should work with Xcode 16. Once you install Xcode and an iOS simulator, you can simply test the demo by running this command.

yarn run ios

Prepare To Run the App on Android

To test the project on Android, you should install Android Studio Otter or a later version and use an emulator. You can also run it on an actual Android device.

Launch and run it on an installed Android emulator using this command.

yarn run android

Configure the App With Stream Activity Feed API

Using the demo user's API_KEY and token credentials from the previous section, we can connect a user to the Stream Feeds docs using the sample code below in your project’s /app/_layout.tsx.

typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import { DarkTheme, DefaultTheme, ThemeProvider, } from "@react-navigation/native"; import { Stack } from "expo-router"; import { StatusBar } from "expo-status-bar"; import "react-native-reanimated"; import { useColorScheme } from "@/hooks/use-color-scheme"; import { StreamFeeds, useCreateFeedsClient, } from "@stream-io/feeds-react-native-sdk"; import { API_KEY, CURRENT_USER } from "@/user"; import { OwnFeedsContextProvider } from "@/contexts/own-feeds-context"; export const unstable_settings = { anchor: "(tabs)", }; export default function RootLayout() { const colorScheme = useColorScheme(); const client = useCreateFeedsClient({ apiKey: API_KEY, tokenOrProvider: CURRENT_USER.token, userData: { id: CURRENT_USER.id, name: CURRENT_USER.name, }, }); if (!client) { return null; } return ( <StreamFeeds client={client}> <OwnFeedsContextProvider> <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme} > <Stack> <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack.Screen name="comments-modal" options={{ presentation: "modal", title: "Comments" }} /> </Stack> <StatusBar style="auto" /> </ThemeProvider> </OwnFeedsContextProvider> </StreamFeeds> ); }

In summary, we import the StreamFeeds top-most context provider and useCreateFeedsClient utility hook to ensure the client instance successfully connects to all components.

Build the Activity Feed UIs

The Activity Feed API uses built-in templates to define feed behavior in your app. These templates are called feed groups.

The following feed groups are available for your app by default.

Get started! Activate your free Stream account today and start prototyping with feeds.
  • User: The feed in which users write and post content.
  • Timeline: A feed populated by the people one follows.
  • For You: A timeline feed that is populated based on content’s popularity.
  • Notification: A feed used to notify users about timely updates.
  • Story: An activity feed with a specified date of expiration.
  • Stories: A timeline feed for following other people’s stories.

To create and ensure the home timeline and “for you” feeds are easily accessible, implement a context provider to initialize the feeds in contexts/own-feeds-context.tsx.

typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import { Feed, useClientConnectedUser, useFeedsClient, } from "@stream-io/feeds-react-native-sdk"; import { createContext, PropsWithChildren, useContext, useEffect, useState, } from "react"; type OwnFeedsContextValue = { ownFeed: Feed | undefined; ownTimeline: Feed | undefined; }; const OwnFeedsContext = createContext<OwnFeedsContextValue>({ ownFeed: undefined, ownTimeline: undefined, }); export const OwnFeedsContextProvider = ({ children }: PropsWithChildren) => { const [ownFeed, setOwnFeed] = useState<Feed>(); const [ownTimeline, setOwnTimeline] = useState<Feed>(); const client = useFeedsClient(); const connectedUser = useClientConnectedUser(); useEffect(() => { if (!connectedUser || !client) return; const feed = client.feed("user", connectedUser.id); setOwnFeed(feed); // Social media apps usually don't add new activities from WebSocket // users need to pull to refresh const timeline = client.feed("timeline", connectedUser.id, { activityAddedEventFilter: ({ activity }) => activity.user?.id === connectedUser.id, }); setOwnTimeline(timeline); Promise.all([ feed.getOrCreate({ watch: true }), timeline.getOrCreate({ watch: true }), ]).then(() => { // You typically create these relationships on your server-side, we do this here for simplicity const alreadyFollows = feed.currentState.own_follows?.find( (follow) => follow.source_feed.feed === timeline.feed, ); if (!alreadyFollows) timeline.follow(feed); }); return () => { setOwnFeed(undefined); setOwnTimeline(undefined); }; }, [connectedUser, client]); return ( <OwnFeedsContext.Provider value={{ ownFeed, ownTimeline }}> {children} </OwnFeedsContext.Provider> ); }; export const useOwnFeedsContext = () => useContext(OwnFeedsContext);

Display Activities With UI Components

You can create and display activities using two components, Activity and ActivityList.

Activity.tsx: An activity to add to a feed.

typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import React from "react"; import { Pressable, StyleSheet } from "react-native"; import { ThemedView } from "@/components/themed-view"; import { ThemedText } from "@/components/themed-text"; import type { ActivityResponse } from "@stream-io/feeds-react-native-sdk"; import { useClientConnectedUser } from "@stream-io/feeds-react-native-sdk"; import { FollowButton } from "@/components/follows/follow-button"; import { Reaction } from "@/components/activity/Reaction"; import { useRouter } from "expo-router"; import { Image } from "expo-image"; type ActivityProps = { activity: ActivityResponse; }; export const Activity = ({ activity }: ActivityProps) => { const router = useRouter(); const name = activity.user?.name || activity.user?.id || "Unknown"; const initial = name.charAt(0).toUpperCase(); const createdAt = activity.created_at instanceof Date ? activity.created_at : new Date(activity.created_at); const createdAtLabel = createdAt.toLocaleString(); const connectedUser = useClientConnectedUser(); const image = activity.attachments?.length > 0 && activity.attachments[0]; return ( <ThemedView style={styles.card}> <ThemedView style={styles.actionsRow}> {activity.current_feed?.feed !== `user:${connectedUser?.id}` && ( <FollowButton feed={activity.current_feed} /> )} </ThemedView> {image ? ( <Image source={{ uri: image.image_url }} style={styles.imagePreview} /> ) : null} <ThemedView style={styles.row}> <ThemedView style={styles.avatarWrapper}> <ThemedView style={styles.avatar}> <ThemedText style={styles.avatarText}>{initial}</ThemedText> </ThemedView> </ThemedView> <ThemedView style={styles.content}> <ThemedView style={styles.headerRow}> <ThemedText style={styles.name} numberOfLines={1}> {name} </ThemedText> <ThemedText style={styles.timestamp} numberOfLines={1}> {createdAtLabel} </ThemedText> </ThemedView> {activity.text ? ( <ThemedText style={styles.text}>{activity.text}</ThemedText> ) : null} </ThemedView> </ThemedView> <ThemedView style={styles.bottomRow}> <Reaction activity={activity} /> <Pressable style={({ pressed }) => [ styles.commentButton, pressed && styles.commentButtonPressed, ]} onPress={() => { router.push({ pathname: "/comments-modal", params: { activityId: activity.id, }, }); }} > <ThemedText style={styles.commentText}>💬</ThemedText> <ThemedText>{activity.comment_count}</ThemedText> </Pressable> </ThemedView> </ThemedView> ); }; const styles = StyleSheet.create({ card: { width: "100%", padding: 12, borderRadius: 12, backgroundColor: "#FFFFFF", borderWidth: 1, borderColor: "#E5E7EB", shadowColor: "#000", shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 3, elevation: 2, }, row: { flexDirection: "row", alignItems: "flex-start", }, avatarWrapper: { marginRight: 12, }, avatar: { width: 40, height: 40, borderRadius: 20, backgroundColor: "#6366F1", alignItems: "center", justifyContent: "center", }, avatarText: { color: "#FFFFFF", fontSize: 18, fontWeight: "600", }, content: { flex: 1, }, headerRow: { flexDirection: "row", alignItems: "center", marginBottom: 4, }, name: { fontSize: 14, fontWeight: "600", marginRight: 8, maxWidth: "50%", }, timestamp: { fontSize: 12, color: "#6B7280", flexShrink: 1, }, text: { fontSize: 14, color: "#111827", }, actionsRow: { flexDirection: "row", alignItems: "center", justifyContent: "flex-end", marginBottom: 4, }, bottomRow: { flexDirection: "row", marginTop: 8, }, imagePreview: { width: "100%", height: 200, marginBottom: 12, borderRadius: 16, }, commentButton: { flexDirection: "row", justifyContent: "center", alignItems: "center", marginLeft: 8, }, commentText: { fontSize: 20, }, commentButtonPressed: { opacity: 0.7, }, });

ActivityList.tsx: A component that displays activities.

typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import React, { useCallback } from "react"; import { View, Text, FlatList, StyleSheet, ActivityIndicator, } from "react-native"; import type { ActivityResponse } from "@stream-io/feeds-react-native-sdk"; import { useFeedActivities } from "@stream-io/feeds-react-native-sdk"; import { Activity } from "@/components/activity/Activity"; const renderItem = ({ item }: { item: ActivityResponse }) => { return <Activity activity={item} />; }; const keyExtractor = (item: ActivityResponse) => item.id; const Separator = () => <View style={styles.separator} />; export const ActivityList = () => { const { activities, loadNextPage, has_next_page, is_loading } = useFeedActivities(); const hasActivities = activities?.length && activities.length > 0; const ListFooterComponent = useCallback( () => is_loading && hasActivities && has_next_page ? ( <ActivityIndicator /> ) : null, [is_loading, has_next_page, hasActivities], ); if (is_loading && (!activities || activities?.length === 0)) { return ( <View style={styles.emptyContainer}> <ActivityIndicator /> </View> ); } if (!activities || activities.length === 0) { return ( <View style={styles.emptyContainer}> <Text style={styles.emptyText}>No posts yet</Text> </View> ); } return ( <FlatList data={activities} keyExtractor={keyExtractor} renderItem={renderItem} contentContainerStyle={styles.listContent} onEndReachedThreshold={0.2} onEndReached={loadNextPage} ItemSeparatorComponent={Separator} ListFooterComponent={ListFooterComponent} /> ); }; const styles = StyleSheet.create({ listContent: { flexGrow: 1, paddingHorizontal: 16, paddingVertical: 12, }, separator: { height: 12, }, footer: { marginTop: 12, alignItems: "center", justifyContent: "center", }, loadMoreButton: { paddingHorizontal: 16, paddingVertical: 10, borderRadius: 999, backgroundColor: "#2563EB", }, loadMoreButtonPressed: { opacity: 0.7, }, loadMoreText: { color: "#FFFFFF", fontWeight: "600", fontSize: 14, }, emptyContainer: { flex: 1, paddingVertical: 32, alignItems: "center", justifyContent: "center", }, emptyText: { fontSize: 14, color: "#6B7280", }, });

How To Post an Activity

After creating and displaying an activity, you need a UI to post it. This can be done using the ActivityComposer component in ActivityComposer.tsx. The component uses a text area, file attachment, and a send button to allow users to compose new messages/posts and send.

Create the For You Activities Page

For You Activities Page

This page allows users to explore content and media by popularity. Refer to the feeds documentation for advanced techniques and algorithms for displaying content on a “for you” timeline page, including popular activity selector and ranking.

The “for you” page contains the following sample code in explore.tsx:

typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { useFeedsClient, useClientConnectedUser, StreamFeed, } from "@stream-io/feeds-react-native-sdk"; import { useEffect, useMemo } from "react"; import { ActivityList } from "@/components/activity/ActivityList"; export default function ExploreScreen() { const client = useFeedsClient(); const currentUser = useClientConnectedUser(); const feed = useMemo(() => { if (!currentUser?.id || !client) { return undefined; } return client.feed("foryou", currentUser.id); }, [client, currentUser?.id]); useEffect(() => { if (feed) { feed.getOrCreate({ limit: 10 }); } }, [feed]); if (!feed) { return null; } return ( <StreamFeed feed={feed}> <ActivityList /> </StreamFeed> ); }

Here is a demo of the “for you” activities page. You can scroll through content by popularity.

Add Support to Follow/Unfollow People

The “for you” activities page lets you follow and unfollow others. To add an integration to follow and unfollow users, you should extend the Activity component to implement a FollowButton component in follow-button.tsx. In its basic form, the component can be used to display information about a user’s activities. Additionally, it can be extended to create a complex homepage timeline layout for feeds.

Add Comments, Reactions, and Media Attachment

Creating a post allows you to add media attachments, such as files, documents, images, and videos, to enrich user engagement and experience.

To support media upload when adding activities/posts and comments, you should create an ImagePicker component in ImagePicker.tsx and extend the Activity component (Activity.tsx) to display it.

In addition to media uploads, comments and reactions are supported to enhance the app’s interactivity. To support comments, you should implement the Comment components, CommentComposer, and CommentList, for creating and listing comments, respectively. To display a button to add a new comment, you should extend the Activity component.

Supporting reactions is similar to the above: you create a Reaction component in Reaction.tsx and extend the Activity component to display it.

Extend What You Built

This article demonstrated how to create a simple but fully interactive “for you” feed similar to Instagram, using the Stream’s Activity Feeds API.

You built a two-page React Native Expo application that uses a popularity algorithm to display content for users with support for comments, reactions, and media attachments. You can tour the v3 Activity Feed API to see it in action.

The Activity Feed API offers several techniques for recommending and ranking content for users on social and community-based applications:

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->