Skip to content

Fetching and Displaying the Post Feed

The homepage of Bulletin is a feed of posts — titles, authors, vote counts, and comment counts. This lesson wires up the GET /posts endpoint and builds the component that renders the results.

Add a getPosts function to your API client:

src/api/postsApi.ts
import apiClient from './apiClient'
export interface Post {
id: number
title: string
body: string
userId: number
username: string
upvotes: number
commentCount: number
createdAt: string
}
export async function getPosts(): Promise<Post[]> {
const res = await apiClient.get<Post[]>('/posts')
return res.data
}

The shape mirrors what the Express endpoint returns. Typing it here means TypeScript catches mismatches throughout the component tree.

src/components/PostFeed.tsx
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { getPosts, Post } from '../api/postsApi'
export default function PostFeed() {
const [posts, setPosts] = useState<Post[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
getPosts()
.then(setPosts)
.catch(() => setError('Failed to load posts.'))
.finally(() => setLoading(false))
}, [])
if (loading) return <p>Loading...</p>
if (error) return <p>{error}</p>
if (posts.length === 0) return <p>No posts yet. Be the first!</p>
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<Link to={`/posts/${post.id}`}>
<strong>{post.title}</strong>
</Link>
<span> by {post.username}</span>
<span> · {post.upvotes} votes · {post.commentCount} comments</span>
</li>
))}
</ul>
)
}

The three-state pattern — loading, error, data — is the same one from Module 08. Loading shows a spinner or placeholder; error shows a message; data renders the list.

src/pages/Home.tsx
import PostFeed from '../components/PostFeed'
export default function Home() {
return (
<main>
<h1>Bulletin</h1>
<PostFeed />
</main>
)
}

Register the route in your router:

// src/App.tsx (routes excerpt)
<Route path="/" element={<Home />} />

What useEffect with an empty dependency array means

Section titled “What useEffect with an empty dependency array means”
useEffect(() => {
// runs once — after the first render
}, [])

The empty array tells React: run this effect once, when the component mounts. It’s the right tool for fetching data that the page needs on load. You don’t want to re-fetch on every render — only when the component first appears.

Open src/components/PostFeed.tsx in your Bulletin project.

  1. Add the getPosts function to src/api/postsApi.ts with the Post interface as shown above.
  2. Create PostFeed.tsx using the three-state pattern (loading, error, data).
  3. Create src/pages/Home.tsx and render <PostFeed /> inside it.
  4. Register the / route in App.tsx.
  5. Start the dev server (npm run dev), start the backend, and confirm the feed displays real posts from the database. If the database is empty, seed it with one or two posts via the API (use Thunder Client or your terminal).

Hold onto PostFeed.tsx — you’ll add an upvote button to it in Lesson 05.

  • getPosts() calls GET /posts and returns a typed array.
  • useEffect(() => { ... }, []) fetches once on mount.
  • The three-state pattern handles loading, error, and success cleanly.
  • Each post links to /posts/:id — you’ll build that page in Lesson 03.