In my recent project portfolio, I focused on improving the user experience by implementing a real-time search filter for blog content. This feature allows users to quickly find relevant posts by filtering through blog titles and descriptions as they type, resulting in a more efficient and user-friendly interface.
State Management with useState
The first step in building the search functionality was to manage the user’s input dynamically. For this, I used React’s useState
hook to store the search term:
const [searchTerm, setSearchTerm] = useState<string>('');
This state variable, searchTerm
, holds the current value of the text entered by the user in the search input field. As the user types, this state is updated in real-time, providing the foundation for the filtering logic.
Filtering Logic with useEffect
To filter the list of blogs based on the search term, I used the useEffect
hook. This ensures that every time the searchTerm
or the list of blogs (blogs
) changes, the filtering logic is executed:
useEffect(() => {
const searchTermLower = searchTerm.toLowerCase();
const filtered = blogs.filter((blog: BlogItem) =>
blog.title.toLowerCase().includes(searchTermLower) ||
blog.description.toLowerCase().includes(searchTermLower)
);
setFilteredBlogs(filtered);
}, [searchTerm, blogs]);
Here's how it works:
- Normalization: The search term and the blog data are converted to lowercase to make the search case-insensitive.
-
Filtering: The
filter
method is applied to theblogs
array, returning only those blog items where the title or description contains the search term. -
Updating State: The filtered list is stored in a new state variable,
filteredBlogs
, ensuring the UI only displays relevant results.
Designing the Input Component with Tailwind CSS
The next step was to design an intuitive and responsive input field using Tailwind CSS. One challenge was ensuring the input field had a distinct border color that changes when focused, especially in different themes like dark mode.
Here’s how I implemented it:
<div className="relative w-40">
<div className="absolute inset-0 border-4 border-blue-500 rounded-lg pointer-events-none transition-all duration-200 focus-within:border-blue-700"></div>
<input
type="text"
placeholder="Search blogs..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full h-full p-2 rounded-lg bg-black/[.05] dark:bg-white/[.05] text-black/80 dark:text-white/80 focus:outline-none"
/>
</div>
-
Wrapper
div
: Thisdiv
wraps the input field and contains the customizable border.-
border-4 border-blue-500
: Adds a thick blue border by default. -
focus-within:border-blue-700
: Changes the border color to a darker blue when the input field is focused, providing a clear visual indication.
-
-
Input Field:
-
w-full h-full
: Ensures the input field occupies the full width and height of its container, creating a seamless design. -
focus:outline-none
: Removes the default outline to rely solely on the custom border effect.
-
Bringing It All Together
The combination of useState
for managing input, useEffect
for filtering logic, and Tailwind CSS for styling, results in a robust and visually appealing search functionality. Here’s a brief walkthrough of how it all connects:
-
User Input: As the user types in the search box,
searchTerm
is updated in real-time. -
Filtering: The
useEffect
hook triggers, filtering the blog list based on the updatedsearchTerm
. -
Rendering: The filtered list (
filteredBlogs
) is rendered, showing only the relevant blog posts. - Responsive Design: The input field and search results are styled using Tailwind CSS, ensuring the interface remains clean and user-friendly across all devices.
Conclusion
Full of my code in Blog.tsx
'use client'
import { fetcher } from '../../../services/fetcher';
import clsx from 'clsx';
import { motion } from 'framer-motion';
import useSWR from 'swr';
import { useEffect, useState } from 'react';
import EmptyState from '@/common/components/elements/EmptyState';
import LoadingCard from '@/common/components/elements/LoadingCard';
import { DEVTO_BLOG_API } from '@/common/constants';
import { BlogItem } from '@/common/types/blog';
import { useBlogView } from '../../../stores/blog-view';
import useIsMobile from '../../../hooks/useIsMobile';
import BlogCard from './BlogCard';
import BlogListHeader from './BlogListHeader';
export default function Blog() {
const isMobile = useIsMobile();
const [searchTerm, setSearchTerm] = useState<string>('');
const [filteredBlogs, setFilteredBlogs] = useState<BlogItem[]>([]);
const { viewOption, setViewOption } = useBlogView();
const { data, isLoading } = useSWR(DEVTO_BLOG_API, fetcher, {
revalidateOnMount: true,
});
const blogs: BlogItem[] = data?.filter((blog: BlogItem) => blog.collection_id === null) || [];
// UseEffect untuk memperbarui filteredBlogs setiap kali searchTerm atau blogs berubah
useEffect(() => {
const searchTermLower = searchTerm.toLowerCase();
const filtered = blogs.filter((blog: BlogItem) =>
blog.title.toLowerCase().includes(searchTermLower) ||
blog.description.toLowerCase().includes(searchTermLower)
);
setFilteredBlogs(filtered);
}, [searchTerm, blogs]);
if (isLoading) {
return (
<div
className={clsx(
'gap-5 sm:gap-4',
viewOption === 'list' || isMobile ? 'flex flex-col' : 'grid grid-cols-2 sm:!gap-5'
)}
>
{[1, 2].map((item) => (
<LoadingCard key={item} view={viewOption} />
))}
</div>
);
}
if (blogs.length === 0 && !isLoading) {
return <EmptyState message="No Data" />;
}
return (
<>
{!isMobile && <BlogListHeader viewOption={viewOption} setViewOption={setViewOption} />}
<div
className={clsx(
'mt-6 gap-5 sm:gap-4',
viewOption === 'list' || isMobile ? 'flex flex-col' : 'grid grid-cols-2'
)}
>
<input
type="text"
placeholder="Search blogs..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-40 rounded-lg border border-neutral-300 dark:border-neutral-600 text-black/80 dark:text-white/80 bg-black/[.05] dark:bg-white/[.05] col-span-full hover:bg-black/10 dark:hover:bg-white/10 focus:border-blue-500 dark:focus:border-blue-500 focus:outline-none p-2"
/>
{filteredBlogs.map((item: BlogItem, index: number) => (
<motion.div
key={item.id}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
>
<BlogCard view={viewOption} {...item} />
</motion.div>
))}
</div>
</>
);
}
This enhanced search functionality not only improves the user experience by making content more accessible but also showcases the flexibility of combining React and Tailwind CSS. Whether dealing with large datasets or simply aiming to provide a more intuitive user interface, this approach ensures high performance and a seamless user experience.
Top comments (0)