Search and Filter
Search doesn’t have to hit the server. For a list already loaded in memory, filtering client-side is instant and requires no extra API calls. You’ll add a search input above the post feed that narrows the list as the user types.
Client-side vs server-side search
Section titled “Client-side vs server-side search”| Approach | When to use |
|---|---|
| Client-side filter | Small to medium lists already in memory |
| Server-side search | Large datasets, full-text search, or when data can’t be loaded all at once |
Bulletin loads all posts at once, so client-side filtering is the right choice. For thousands of posts, you’d add query params to GET /posts?search=keyword and search in the database.
Adding search state to PostFeed
Section titled “Adding search state to PostFeed”// src/components/PostFeed.tsx — add searchimport { useState, useEffect, useMemo } from 'react'import PostItem from './PostItem'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) const [search, setSearch] = useState('')
useEffect(() => { getPosts() .then(setPosts) .catch(() => setError('Failed to load posts.')) .finally(() => setLoading(false)) }, [])
const filteredPosts = useMemo(() => { const q = search.toLowerCase().trim() if (!q) return posts return posts.filter(p => p.title.toLowerCase().includes(q)) }, [posts, search])
if (loading) return <p>Loading...</p> if (error) return <p>{error}</p>
return ( <div> <input type="search" placeholder="Search posts..." value={search} onChange={e => setSearch(e.target.value)} aria-label="Search posts" /> {filteredPosts.length === 0 ? <p>No posts match your search.</p> : ( <ul> {filteredPosts.map(post => ( <PostItem key={post.id} post={post} /> ))} </ul> ) } </div> )}Why useMemo
Section titled “Why useMemo”const filteredPosts = useMemo(() => { return posts.filter(...)}, [posts, search])useMemo caches the result of the filter function and only recomputes it when posts or search changes. Without it, the filter runs on every render — even renders caused by unrelated state changes (like an upvote count updating). For a short list it barely matters, but useMemo makes the intent explicit: this value is derived from these two inputs and nothing else.
Sorting posts
Section titled “Sorting posts”You can add a sort dropdown without a server call:
const [sortBy, setSortBy] = useState<'new' | 'top'>('new')
const filteredPosts = useMemo(() => { const q = search.toLowerCase().trim() let result = q ? posts.filter(p => p.title.toLowerCase().includes(q)) : [...posts]
if (sortBy === 'top') { result = result.sort((a, b) => b.upvotes - a.upvotes) } return result}, [posts, search, sortBy])
// in JSX:<select value={sortBy} onChange={e => setSortBy(e.target.value as 'new' | 'top')}> <option value="new">Newest</option> <option value="top">Top</option></select>The API returns posts newest-first by default. Sorting by top is a client-side reorder — no new request needed.
Controlled inputs recap
Section titled “Controlled inputs recap”<input value={search} onChange={e => setSearch(e.target.value)} />This is a controlled input: React owns the value. The input shows exactly what search holds and calls setSearch on every keystroke. Never use defaultValue with a controlled input — pick one approach and stick with it.
Exercise
Section titled “Exercise”Open src/components/PostFeed.tsx in your Bulletin project.
- Add
searchstate and a search input above the post list. - Compute
filteredPostswithuseMemo— filter by title, case-insensitive. - Render
filteredPostsinstead ofpostsin the list. - Show “No posts match your search.” when
filteredPostsis empty. - Stretch: Add a sort dropdown (Newest / Top) and include
sortByin theuseMemodependency array. - Test: type in the search box and confirm the list narrows live. Clear the input and confirm all posts return.
- Client-side filtering is instant for lists already in memory — no extra API call.
useMemocaches the filtered result and only recomputes when inputs change.- Controlled inputs (
value+onChange) keep search state in React. - Sort can also be client-side when all data is loaded — add it to the same
useMemo.