In the previous chapter of this tutorial, we brought our Create Post feature to life. This involved building out components to request camera permissions, capture or select images, preview the photos, and finalize posts with captions. To ensure better performance, we also introduced an image resizing utility function, which streamlined handling larger files.
Now, with the ability to create posts in place, it’s time to give users a way to view their content. In this chapter, we’ll focus on creating the foundation of the feed, where posts will be displayed. This involves designing the UI for how a single post will look, complete with buttons for likes, comments, bookmarks, and user avatars. By the end of this section, users will be able to scroll through posts they’ve created in a visually appealing and functional feed interface.
It’s important to note that while we’ll add these interactive buttons to the post UI, they won’t be wired up to any functionality just yet. That will come in later chapters when we implement features like likes, comments, and bookmarks. For now, the focus is on laying the groundwork and getting the visual elements in place.
Accumulated reading time so far: 30 minutes.
Part 6 completed repo on GitHub for reference
Updating the Home Screen
Before updating the Home
screen with the feed functionality, it’s important to preserve any existing functionality. If your current Home
screen includes buttons for signing in and out of the application, move that functionality to the Profile
screen (app/(tabs)/profile/index.tsx
). This ensures that the sign-in and sign-out features remain accessible as we transition the Home
screen into a feed.
To do this:
Open the
app/(tabs)/index.tsx
file and locate the existing code for the sign-in and sign-out buttons.Cut this code and paste it into
app/(tabs)/profile/index.tsx
.
Once you’ve moved this functionality, you’re ready to replace the code in the Home
screen with the following:
import React from "react";
import { FeedProvider } from "replyke-expo";
import Feed from "../../components/home/Feed";
const Home = () => {
return (
<FeedProvider sortBy="hot">
<Feed />
</FeedProvider>
);
};
export default Home;
What This New Code Does
FeedProvider: This is a context provider that will give all of its child components access to feed data and functionality. We’ve set the
sortBy
prop to "hot," which means the "hottest" posts will appear higher in our feed. The scoring system for "hot" is handled automatically by Replyke, so you don’t need to worry about implementing it yourself.Feed: This component will handle the UI of the feed. It will use a
FlatList
to render individual posts, which we will implement in the next steps.
By moving the sign-in and sign-out functionality to the Profile
screen and updating the Home
screen, you’ve set the stage for building the feed UI. Let’s proceed to implementing the Feed
component next!
Creating the Feed Component
Next, let’s create the Feed
component. Start by creating a new folder named home
inside the app/components
directory. Within this folder, create a new file called Feed.tsx
.
Here is the content for the Feed.tsx
file:
import React, { useCallback, useState } from "react";
import { EntityProvider, useFeed } from "replyke-expo";
import {
RefreshControl,
View,
Text,
FlatList,
ActivityIndicator,
} from "react-native";
import { SinglePost } from "../shared/SinglePost";
function Feed() {
const { entities, loadMore, resetEntities, loading } = useFeed();
const [listHeight, setListHeight] = useState(0);
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(async () => {
setRefreshing(true);
await resetEntities?.();
setRefreshing(false);
}, [resetEntities]);
const initialLoading = loading && (!entities || entities.length === 0);
if (initialLoading)
return (
<View className="flex-1 items-center justify-center">
<ActivityIndicator size="large" />
</View>
);
return (
<View
className="flex-1"
onLayout={(event) => {
if (event.nativeEvent.layout.height > listHeight)
setListHeight(event.nativeEvent.layout.height);
}}
>
<FlatList
data={entities!}
renderItem={({ item: entity }) => (
<EntityProvider entity={entity}>
<SinglePost listHeight={listHeight} />
</EntityProvider>
)}
keyExtractor={(item) => item.id}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
onEndReached={loadMore}
onEndReachedThreshold={2}
ListFooterComponent={null}
ListEmptyComponent={
loading ? null : (
<View
className="flex-1 bg-white justify-center"
style={{
height: listHeight,
}}
>
<Text className="text-center text-xl font-medium text-gray-400">
No Results
</Text>
<Text className="text-center text-lg text-gray-400 mt-2">
Try expanding your search
</Text>
</View>
)
}
pagingEnabled
decelerationRate="fast"
showsVerticalScrollIndicator={false}
snapToAlignment="start"
scrollEventThrottle={16}
bounces={false}
/>
</View>
);
}
export default Feed;
What’s Happening in the Feed Component
useFeed Hook: This custom hook from
replyke-expo
fetches and manages feed data, including loading states, entities (posts), and pagination functionality.-
State Management: We use
useState
to manage:-
listHeight
: Keeps track of the height of the list to ensure proper rendering. -
refreshing
: Tracks the pull-to-refresh state.
-
Pull-to-Refresh: The
onRefresh
function resets the feed data by callingresetEntities
. This provides a smooth user experience for refreshing the feed.-
FlatList Parameters:
-
data
: This is the array of posts (entities) to display in the feed. -
renderItem
: For each item (post) in thedata
, this renders aSinglePost
component wrapped in anEntityProvider
, which will provide the post the necessary context. -
keyExtractor
: Generates a unique key for each item based on itsid
property. -
refreshControl
: Enables pull-to-refresh functionality with aRefreshControl
component. -
onEndReached
: Triggered when the user scrolls near the end of the list, callingloadMore
to fetch additional posts. -
onEndReachedThreshold
: Determines how close the user needs to scroll to the bottom foronEndReached
to trigger (2 means the function triggers a bit earlier). -
ListEmptyComponent
: Displays a message when there are no posts to show and no loading is happening. -
pagingEnabled
: Ensures that scrolling snaps to items. -
decelerationRate
andsnapToAlignment
: Help create a smooth, fast-scrolling experience. -
showsVerticalScrollIndicator
: Hides the scroll indicator. -
scrollEventThrottle
: Controls the frequency of scroll events for better performance. -
bounces
: Disables the bounce effect when scrolling past the list’s edges.
-
-
Loading States:
- Displays an
ActivityIndicator
while the initial data is loading.
- Displays an
This setup ensures the feed dynamically handles pagination, refresh, and empty states, creating a seamless user experience. Next, we’ll implement the SinglePost
component to complete the post display!
Implementing the SinglePost Component
Folder and File Structure
Let’s organize our SinglePost
component for reuse across multiple screens (e.g., Home, User Profile, Single Post). Create the following structure inside app/components
:
app/
components/
shared/
SinglePost/
SinglePost.tsx
PostActions.tsx
index.ts
We’re placing SinglePost
in the shared
folder because it will be reused in multiple screens. Creating a dedicated folder for SinglePost
keeps related parts neatly organized, as the component will grow in complexity over time.
SinglePost.tsx
Create the SinglePost.tsx
file with the following code:
import { Image, Text, View } from "react-native";
import React from "react";
import { useEntity } from "replyke-expo";
import PostActions from "./PostActions";
const SinglePost = ({ listHeight }: { listHeight: number }) => {
const { entity } = useEntity();
if (!entity) return null; // This ensures type safety, as the EntityProvider guarantees entity existence.
return (
<View className="relative flex-1" style={{ height: listHeight }}>
<Image
source={{ uri: entity.media[0].publicPath }}
className="flex-1"
resizeMode="cover"
/>
{/* Caption */}
<View className="px-6 py-4 gap-2 absolute left-0 bottom-0 right-0 bg-black/30">
<Text className="text-white">{entity.title}</Text>
</View>
{/* Actions */}
<PostActions />
</View>
);
};
export default SinglePost;
Explanation:
Entity Context: The
useEntity
hook provides the current post data (entity). If no entity is available, the component renders nothing. This ensures type safety, as theEntityProvider
directly passes a definedentity
.Image Display: The post’s primary media (image) is displayed full-screen.
Caption: The title of the post appears as an overlay at the bottom of the image.
PostActions: Adds buttons for interacting with the post, such as likes, comments, and bookmarks.
PostActions.tsx
Create the PostActions.tsx
file with the following code:
import React from "react";
import { View, TouchableOpacity, Text, Pressable } from "react-native";
import { useEntity, UserAvatar } from "replyke-expo";
import FontAwesome from "@expo/vector-icons/FontAwesome";
import AntDesign from "@expo/vector-icons/AntDesign";
import Ionicons from "@expo/vector-icons/Ionicons";
function PostActions() {
const { entity } = useEntity();
if (!entity) return null;
return (
<View
className="absolute right-4 bottom-20 z-50 items-center gap-6 bg-black/30 p-3 pb-4 rounded-2xl"
style={{ columnGap: 12 }}
>
{/* User avatar and follow button */}
<Pressable>
<UserAvatar user={entity.user} size={46} borderRadius={8} />
</Pressable>
{/* LIKE BUTTON */}
<TouchableOpacity className="items-center gap-1.5">
<AntDesign name="heart" size={36} color="white" />
<Text className="text-white overflow-visible">
{entity?.upvotes.length}
</Text>
</TouchableOpacity>
{/* OPEN COMMENT SECTION */}
<TouchableOpacity className="items-center gap-1.5">
<Ionicons name="chatbubble" size={36} color="#ffffff" />
{(entity.repliesCount || 0) > 0 && (
<Text className="text-[#9ca3af]">{entity.repliesCount}</Text>
)}
</TouchableOpacity>
{/* BOOKMARK BUTTON */}
<TouchableOpacity>
<FontAwesome name="bookmark" size={32} color="#ffffff99" />
</TouchableOpacity>
</View>
);
}
export default PostActions;
Explanation:
User Avatar: Displays the post creator’s avatar. In the future, tapping this can navigate to the user’s profile.
Like Button: Displays the number of likes/upvotes the post has received. This button will later support liking/unliking posts.
Comment Button: Shows the number of comments (if any). This button will later open the comment section when clicked.
Bookmark Button: Allows saving the post. This button will later open the collections sheet.
index.ts
Create an index.ts
file with the following:
export { default as SinglePost } from "./SinglePost";
This allows other components to import SinglePost
easily from the folder.
Wrapping Up
In this chapter, we’ve set the stage for the feed by creating the Feed
and SinglePost
components. These components handle displaying posts in a visually appealing and interactive layout, including features like likes, comments, and bookmarks. While we’ve built the foundation, we’re not done yet! In the next chapter, we’ll wire up these features to make them fully functional, allowing users to engage with the content seamlessly.
Stay Updated
Don’t forget to join the Discord server where I post free boilerplate code repos for different types of social networks. Updates about the next article and additional resources will also be shared there. Lastly, follow me for updates here and on X/Twitter & BlueSky.
Top comments (0)