We use cookies to enhance your experience on the site
CodeWorlds

Search and Filtering - Finding Content

Reddit lets you search posts, filter by subreddit, and sort by popularity. Let's implement this!

Client-side Search - Filtering in the Browser 🔍

The simplest approach - filter data on the client side.

Example - Searching posts:

1function PostsList() {
2  const [posts, setPosts] = useState<Post[]>([...]);  // All posts
3  const [searchQuery, setSearchQuery] = useState("");
4
5  // Filtering
6  const filteredPosts = posts.filter(post =>
7    post.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
8    post.content.toLowerCase().includes(searchQuery.toLowerCase())
9  );
10
11  return (
12    <div>
13      <input
14        type="text"
15        value={searchQuery}
16        onChange={(e) => setSearchQuery(e.target.value)}
17        placeholder="Search posts..."
18        className="w-full border rounded px-4 py-2 mb-4"
19      />
20
21      <div className="space-y-4">
22        {filteredPosts.length === 0 ? (
23          <p className="text-gray-500 text-center">No posts found</p>
24        ) : (
25          filteredPosts.map(post => <PostCard key={post.id} post={post} />)
26        )}
27      </div>
28    </div>
29  );
30}

Pros:

  • ✅ Simple
  • ✅ Fast (no API calls)

Cons:

  • ❌ Only works on loaded data
  • ❌ Doesn't scale (1000+ posts = slow)

Multiple Filters - Combining Filters 🎛️

Let's add more filters:

1function PostsList() {
2  const [posts, setPosts] = useState<Post[]>([...]);
3  const [searchQuery, setSearchQuery] = useState("");
4  const [selectedCommunity, setSelectedCommunity] = useState<string | null>(null);
5  const [sortBy, setSortBy] = useState<"hot" | "new" | "top">("hot");
6
7  // Filtering + sorting
8  const filteredPosts = useMemo(() => {
9    let filtered = posts;
10
11    // 1. Search query
12    if (searchQuery) {
13      filtered = filtered.filter(post =>
14        post.title.toLowerCase().includes(searchQuery.toLowerCase())
15      );
16    }
17
18    // 2. Community filter
19    if (selectedCommunity) {
20      filtered = filtered.filter(post => post.community === selectedCommunity);
21    }
22
23    // 3. Sorting
24    if (sortBy === "hot") {
25      filtered = [...filtered].sort((a, b) => b.score - a.score);
26    } else if (sortBy === "new") {
27      filtered = [...filtered].sort((a, b) =>
28        new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
29      );
30    } else if (sortBy === "top") {
31      filtered = [...filtered].sort((a, b) => b.upvotes - a.upvotes);
32    }
33
34    return filtered;
35  }, [posts, searchQuery, selectedCommunity, sortBy]);
36
37  return (
38    <div>
39      {/* Search */}
40      <input
41        type="text"
42        value={searchQuery}
43        onChange={(e) => setSearchQuery(e.target.value)}
44        placeholder="Search posts..."
45        className="w-full border rounded px-4 py-2 mb-4"
46      />
47
48      {/* Filters */}
49      <div className="flex gap-4 mb-4">
50        {/* Community select */}
51        <select
52          value={selectedCommunity || ""}
53          onChange={(e) => setSelectedCommunity(e.target.value || null)}
54          className="border rounded px-3 py-2"
55        >
56          <option value="">All Communities</option>
57          <option value="pirates">r/pirates</option>
58          <option value="treasure">r/treasure</option>
59          <option value="ships">r/ships</option>
60        </select>
61
62        {/* Sort buttons */}
63        <div className="flex gap-2">
64          <button
65            onClick={() => setSortBy("hot")}
66            className={`px-3 py-2 rounded ${sortBy === "hot" ? "bg-blue-600 text-white" : "bg-gray-200"}`}
67          >
68            🔥 Hot
69          </button>
70          <button
71            onClick={() => setSortBy("new")}
72            className={`px-3 py-2 rounded ${sortBy === "new" ? "bg-blue-600 text-white" : "bg-gray-200"}`}
73          >
74            🆕 New
75          </button>
76          <button
77            onClick={() => setSortBy("top")}
78            className={`px-3 py-2 rounded ${sortBy === "top" ? "bg-blue-600 text-white" : "bg-gray-200"}`}
79          >
80Top
81          </button>
82        </div>
83      </div>
84
85      {/* Results */}
86      <div className="space-y-4">
87        {filteredPosts.map(post => <PostCard key={post.id} post={post} />)}
88      </div>
89    </div>
90  );
91}

useMemo caches the filtering - doesn't recalculate on every render!

URL Search Params - Bookmarkable Filters 🔗

Filters in the URL let users bookmark/share results:

1/posts?q=treasure&community=pirates&sort=hot

Next.js useSearchParams:

1'use client';  // Client Component!
2
3import { useSearchParams, useRouter } from 'next/navigation';
4
5function PostsList() {
6  const searchParams = useSearchParams();
7  const router = useRouter();
8
9  const query = searchParams.get('q') || "";
10  const community = searchParams.get('community');
11  const sort = searchParams.get('sort') || "hot";
12
13  const updateFilters = (key: string, value: string | null) => {
14    const params = new URLSearchParams(searchParams);
15    if (value) {
16      params.set(key, value);
17    } else {
18      params.delete(key);
19    }
20    router.push(`?${params.toString()}`);
21  };
22
23  return (
24    <div>
25      <input
26        type="text"
27        value={query}
28        onChange={(e) => updateFilters('q', e.target.value || null)}
29        placeholder="Search posts..."
30      />
31
32      <select
33        value={community || ""}
34        onChange={(e) => updateFilters('community', e.target.value || null)}
35      >
36        <option value="">All Communities</option>
37        <option value="pirates">r/pirates</option>
38      </select>
39
40      {/* ... */}
41    </div>
42  );
43}

Pros:

  • ✅ Bookmarkable
  • ✅ Shareable links
  • ✅ Back button works!

Server-side Search - API Endpoints 🌐

For large datasets, search on the server side:

API Route -
app/api/search/route.ts
:

1import { NextRequest, NextResponse } from 'next/server';
2
3export async function GET(request: NextRequest) {
4  const searchParams = request.nextUrl.searchParams;
5  const query = searchParams.get('q') || "";
6  const community = searchParams.get('community');
7  const sort = searchParams.get('sort') || "hot";
8
9  // Database query (e.g. Prisma)
10  const posts = await prisma.post.findMany({
11    where: {
12      OR: [
13        { title: { contains: query, mode: 'insensitive' } },
14        { content: { contains: query, mode: 'insensitive' } }
15      ],
16      ...(community && { community })
17    },
18    orderBy: sort === "new"
19      ? { createdAt: 'desc' }
20      : { upvotes: 'desc' },
21    take: 50  // Limit
22  });
23
24  return NextResponse.json({ posts });
25}

Client fetch:

1function PostsList() {
2  const [posts, setPosts] = useState<Post[]>([]);
3  const [loading, setLoading] = useState(false);
4  const debouncedQuery = useDebounce(searchQuery, 500);
5
6  useEffect(() => {
7    setLoading(true);
8    fetch(`/api/search?q=${debouncedQuery}&community=${community}&sort=${sort}`)
9      .then(res => res.json())
10      .then(data => setPosts(data.posts))
11      .finally(() => setLoading(false));
12  }, [debouncedQuery, community, sort]);
13
14  if (loading) {
15    return <div>Loading...</div>;
16  }
17
18  return (
19    <div>
20      {posts.map(post => <PostCard key={post.id} post={post} />)}
21    </div>
22  );
23}

Highlight Search Results - Highlighting 💡

Highlight found words:

1function highlightText(text: string, query: string) {
2  if (!query) return text;
3
4  const regex = new RegExp(`(${query})`, 'gi');
5  return text.replace(regex, '<mark>$1</mark>');
6}
7
8function PostCard({ post, searchQuery }: { post: Post; searchQuery: string }) {
9  return (
10    <div>
11      <h2
12        dangerouslySetInnerHTML={{
13          __html: highlightText(post.title, searchQuery)
14        }}
15      />
16    </div>
17  );
18}

Note:

dangerouslySetInnerHTML
only for sanitized content!

Autocomplete - Suggestions 🔮

Show suggestions while typing:

1function SearchWithSuggestions() {
2  const [query, setQuery] = useState("");
3  const [suggestions, setSuggestions] = useState<string[]>([]);
4  const debouncedQuery = useDebounce(query, 300);
5
6  useEffect(() => {
7    if (debouncedQuery) {
8      fetch(`/api/suggestions?q=${debouncedQuery}`)
9        .then(res => res.json())
10        .then(data => setSuggestions(data.suggestions));
11    } else {
12      setSuggestions([]);
13    }
14  }, [debouncedQuery]);
15
16  return (
17    <div className="relative">
18      <input
19        type="text"
20        value={query}
21        onChange={(e) => setQuery(e.target.value)}
22        placeholder="Search..."
23        className="w-full border rounded px-4 py-2"
24      />
25
26      {suggestions.length > 0 && (
27        <div className="absolute top-full left-0 right-0 bg-white border rounded shadow-lg mt-1">
28          {suggestions.map((suggestion, i) => (
29            <button
30              key={i}
31              onClick={() => {
32                setQuery(suggestion);
33                setSuggestions([]);
34              }}
35              className="w-full text-left px-4 py-2 hover:bg-gray-100"
36            >
37              🔍 {suggestion}
38            </button>
39          ))}
40        </div>
41      )}
42    </div>
43  );
44}

Summary 🎓

Client-side filtering -

array.filter()
Multiple filters - search + community + sort ✅ useMemo - caches filtering ✅ URL Search Params - bookmarkable filters ✅ Server-side search - API endpoints for large datasets ✅ Debouncing - delay search by 500ms ✅ Highlighting - highlight found words ✅ Autocomplete - suggestions while typing

See you there! 🚀

Go to CodeWorlds