Skip to content

Upvotes on the Detail Page

The feed has an upvote button — the detail page should too. Rather than duplicating the optimistic update logic, you’ll extract it into a custom hook that both components share.

src/hooks/useUpvote.ts
import { useState } from 'react'
import { upvotePost } from '../api/postsApi'
export function useUpvote(initialCount: number, postId: number) {
const [upvotes, setUpvotes] = useState(initialCount)
async function handleUpvote() {
setUpvotes(prev => prev + 1)
try {
const { upvotes: serverCount } = await upvotePost(postId)
setUpvotes(serverCount)
} catch {
setUpvotes(prev => prev - 1)
}
}
return { upvotes, handleUpvote }
}

The hook encapsulates the three-step optimistic pattern. Any component can use it by passing the initial count and post ID.

// src/pages/PostDetail.tsx — excerpt
import { useUpvote } from '../hooks/useUpvote'
import { useAuth } from '../context/AuthContext'
export default function PostDetail() {
// ... existing state
const { isAuthenticated } = useAuth()
// Initialize after post loads:
const { upvotes, handleUpvote } = useUpvote(post?.upvotes ?? 0, Number(id))
// in JSX:
return (
<main>
<h1>{post.title}</h1>
<p>by {post.username}</p>
{isAuthenticated
? <button onClick={handleUpvote}>{upvotes}</button>
: <span>{upvotes} votes</span>
}
{/* ... rest of page */}
</main>
)
}

Refactor PostFeed to use useUpvote per post. Since each post is its own list item, create a small PostItem component that uses the hook:

src/components/PostItem.tsx
import { Link } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import { useUpvote } from '../hooks/useUpvote'
import { Post } from '../api/postsApi'
interface Props {
post: Post
}
export default function PostItem({ post }: Props) {
const { isAuthenticated } = useAuth()
const { upvotes, handleUpvote } = useUpvote(post.upvotes, post.id)
return (
<li>
<Link to={`/posts/${post.id}`}>
<strong>{post.title}</strong>
</Link>
<span> by </span>
<Link to={`/users/${post.userId}`}>{post.username}</Link>
{isAuthenticated
? <button onClick={handleUpvote}>{upvotes}</button>
: <span> · {upvotes} votes</span>
}
<span> · {post.commentCount} comments</span>
</li>
)
}

Then simplify PostFeed:

// src/components/PostFeed.tsx — simplified
import PostItem from './PostItem'
// in JSX:
<ul>
{posts.map(post => (
<PostItem key={post.id} post={post} />
))}
</ul>

A custom hook is just a function whose name starts with use and calls other hooks inside it. The naming convention is what matters — React enforces the rules of hooks (no conditional calls, top-level only) based on the use prefix.

Custom hooks are for sharing stateful logic, not for sharing UI. When you find yourself copying the same useState/useEffect/callback pattern into multiple components, extract it.

Open src/hooks/useUpvote.ts in your Bulletin project (create the file).

  1. Create src/hooks/useUpvote.ts with the useUpvote hook as shown.
  2. Create src/components/PostItem.tsx using the hook.
  3. Update PostFeed.tsx to render <PostItem> instead of inline <li> elements.
  4. Add the upvote button to PostDetail.tsx using useUpvote.
  5. Test both: upvote a post from the feed, then navigate to its detail page and confirm the count is consistent (the server is the source of truth).
  • Custom hooks extract shared stateful logic — name them with use prefix.
  • useUpvote encapsulates the three-step optimistic pattern: increment, confirm, rollback.
  • Extract PostItem from PostFeed when a list item needs its own hook state.
  • The feed and detail page both use useUpvote — the logic lives in one place.