Skip to content

Adding Comments

Comments appear below a post’s body. Logged-in users see a form; anonymous users see a prompt to log in. Submitting the form calls POST /posts/:id/comments and adds the new comment to the local state without re-fetching the whole post.

src/api/commentsApi.ts
import apiClient from './apiClient'
import { Comment } from './postsApi'
export async function createComment(
postId: number,
body: string
): Promise<Comment> {
const res = await apiClient.post<Comment>(`/posts/${postId}/comments`, { body })
return res.data
}

The server returns the new comment object including its generated id, username, and createdAt.

src/components/CommentForm.tsx
import { useState, FormEvent } from 'react'
import { createComment } from '../api/commentsApi'
import { Comment } from '../api/postsApi'
interface Props {
postId: number
onCommentAdded: (comment: Comment) => void
}
export default function CommentForm({ postId, onCommentAdded }: Props) {
const [body, setBody] = useState('')
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
async function handleSubmit(e: FormEvent) {
e.preventDefault()
if (!body.trim()) return
setSubmitting(true)
setError(null)
try {
const comment = await createComment(postId, body)
onCommentAdded(comment)
setBody('')
} catch {
setError('Failed to post comment.')
} finally {
setSubmitting(false)
}
}
return (
<form onSubmit={handleSubmit}>
{error && <p style={{ color: 'red' }}>{error}</p>}
<textarea
value={body}
onChange={e => setBody(e.target.value)}
placeholder="Add a comment..."
rows={3}
required
/>
<button type="submit" disabled={submitting}>
{submitting ? 'Posting...' : 'Comment'}
</button>
</form>
)
}

onCommentAdded is a callback prop — it lets PostDetail decide what to do with the new comment rather than CommentForm managing it. This keeps the form focused on submission only.

// src/pages/PostDetail.tsx — comments section update
import { useAuth } from '../context/AuthContext'
import CommentForm from '../components/CommentForm'
// update post state type:
const [post, setPost] = useState<PostDetailType | null>(null)
function handleCommentAdded(comment: Comment) {
setPost(prev => {
if (!prev) return prev
return { ...prev, comments: [...prev.comments, comment] }
})
}
// in JSX, comments section:
<section>
<h2>Comments ({post.comments.length})</h2>
<ul>
{post.comments.map(comment => (
<li key={comment.id}>
<strong>{comment.username}</strong>: {comment.body}
</li>
))}
</ul>
{isAuthenticated
? <CommentForm postId={post.id} onCommentAdded={handleCommentAdded} />
: <p><Link to="/login">Log in</Link> to comment.</p>
}
</section>

handleCommentAdded uses the functional form of setPost to safely append the new comment to the existing list, then clears the form via the setBody('') call inside CommentForm.

Callback props for child-to-parent communication

Section titled “Callback props for child-to-parent communication”

React data flows down (parent → child via props), but sometimes a child needs to tell a parent something happened. Callback props are the standard pattern:

Parent defines: function handleCommentAdded(comment) { ... }
Parent passes: <CommentForm onCommentAdded={handleCommentAdded} />
Child calls: onCommentAdded(comment) // triggers parent's function

No global state needed — just a function passed down and called back up.

Open src/pages/PostDetail.tsx in your Bulletin project.

  1. Create src/api/commentsApi.ts with createComment.
  2. Create src/components/CommentForm.tsx with the onCommentAdded callback prop pattern.
  3. Update PostDetail.tsx: add handleCommentAdded, render <CommentForm> for logged-in users and a “log in to comment” message for anonymous users.
  4. Submit a comment on a post and confirm it appears in the list immediately (without a page reload). Check the database or call GET /posts/:id to confirm it persisted.
  • createComment posts to /posts/:id/comments and returns the new comment object.
  • CommentForm takes a callback prop (onCommentAdded) — the parent manages the list state.
  • Append the new comment with { ...prev, comments: [...prev.comments, comment] } — no re-fetch needed.
  • Show the form to authenticated users; show a login link to anonymous users.