DEV Community

Cover image for How to Build a Social Network in 1 Day: Part 6 - Adding a Feed
Tsabary
Tsabary

Posted on

How to Build a Social Network in 1 Day: Part 6 - Adding a Feed

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:

  1. Open the app/(tabs)/index.tsx file and locate the existing code for the sign-in and sign-out buttons.

  2. 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;
Enter fullscreen mode Exit fullscreen mode

What This New Code Does

  1. 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.

  2. 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;
Enter fullscreen mode Exit fullscreen mode

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 calling resetEntities. 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 the data, this renders a SinglePost component wrapped in an EntityProvider, which will provide the post the necessary context.
    • keyExtractor: Generates a unique key for each item based on its id property.
    • refreshControl: Enables pull-to-refresh functionality with a RefreshControl component.
    • onEndReached: Triggered when the user scrolls near the end of the list, calling loadMore to fetch additional posts.
    • onEndReachedThreshold: Determines how close the user needs to scroll to the bottom for onEndReached 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 and snapToAlignment: 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.

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 the EntityProvider directly passes a defined entity.

  • 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;
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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)