In blogs and news website, having an efficient search functionality is important for providing a good user experience. Two common approaches to improve search functionality in React applications are filtering and debouncing.
Table of Content
In this article, you will learn about these approaches and how to integrate them into the existing ArticleDetail component.
Approach 1: Filtering
Filtering is a common method to implement search functionality on the client side. It involves searching through the list of articles (or comments) by matching the search query against specific fields (like title, content, or tags). This approach works well for smaller datasets.
Implementation Steps:
- State Management: Create a state variable to store the search query.
- Filtering Logic: Implement a function to filter the articles or comments based on the search query.
- Rendering Filtered Results: Update the UI to display only the filtered articles or comments.
Example:
// App.jsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import AuthorList from "./pages/AuthorList";
import CreateAuthor from "./components/CreateAuthor";
import UpdateAuthor from "./components/UpdateAuthor";
import AuthorBio from "./pages/AuthorBio";
import CreateArticle from "./pages/CreateArticle";
import ArticleList from "./pages/ArticleList";
import ArticleDetail from "./pages/ArticleDetail";
const App = () => {
return (
<>
<Router>
<Routes>
<Route path="/" element={<AuthorList />} />
<Route path="/create-author" element={<CreateAuthor />} />
<Route path="/update-author/:id" element={<UpdateAuthor />} />
<Route path="/bio/:id" element={<AuthorBio />} />
<Route path="/create-article" element={<CreateArticle />} />
<Route path="/article" element={<ArticleList />} />
<Route path="/articles/:id" element={<ArticleDetail />} />
</Routes>
</Router>
</>
);
};
export default App;
// src/pages/ArticleList.jsx
import React, { useState, useEffect } from "react";
import axios from "axios";
import { Link } from "react-router-dom";
import {
Container,
Typography,
Button,
List,
ListItem,
ListItemText,
IconButton,
Tooltip,
Box,
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
Chip,
} from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
const ArticleList = () => {
const [articles, setArticles] = useState([]);
const [categories, setCategories] = useState([]);
const [tags, setTags] = useState([]);
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState("");
const [selectedTags, setSelectedTags] = useState([]);
useEffect(() => {
const fetchArticles = async () => {
try {
const response = await axios.get("http://localhost:5000/api/articles");
setArticles(response.data);
// Extract unique categories and tags
const allCategories = [
...new Set(response.data.flatMap((article) => article.categories)),
];
const allTags = [
...new Set(response.data.flatMap((article) => article.tags)),
];
setCategories(allCategories);
setTags(allTags);
} catch (error) {
console.error("Error fetching articles:", error);
}
};
fetchArticles();
}, []);
const deleteArticle = async (id) => {
try {
await axios.delete(`http://localhost:5000/api/articles/${id}`);
setArticles(articles.filter((article) => article._id !== id));
} catch (error) {
console.error("Error deleting article:", error);
}
};
const filteredArticles = articles.filter((article) => {
const matchesSearch = article.title
.toLowerCase()
.includes(searchQuery.toLowerCase());
const matchesCategory =
!selectedCategory || article.categories.includes(selectedCategory);
const matchesTags = selectedTags.every((tag) => article.tags.includes(tag));
return matchesSearch && matchesCategory && matchesTags;
});
const handleTagChange = (event) => {
const { value } = event.target;
setSelectedTags(typeof value === "string" ? value.split(",") : value);
};
return (
<Container maxWidth="md" sx={{ mt: 4 }}>
<Typography variant="h4" gutterBottom>
Articles
</Typography>
<Button
variant="contained"
color="primary"
component={Link}
to="/create-article"
sx={{ mb: 2 }}
>
Write New Article
</Button>
<Box sx={{ mb: 2 }}>
<TextField
fullWidth
label="Search by title"
variant="outlined"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</Box>
<Box sx={{ mb: 2 }}>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>Category</InputLabel>
<Select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
label="Category"
>
<MenuItem value="">All Categories</MenuItem>
{categories.map((category) => (
<MenuItem key={category} value={category}>
{category}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Tags</InputLabel>
<Select
multiple
value={selectedTags}
onChange={handleTagChange}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} />
))}
</Box>
)}
>
{tags.map((tag) => (
<MenuItem key={tag} value={tag}>
{tag}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
<List>
{filteredArticles.map((article) => (
<ListItem
key={article._id}
secondaryAction={
<>
<Tooltip title="Edit">
<IconButton
edge="end"
component={Link}
to={`/update-article/${article._id}`}
color="primary"
>
<EditIcon />
</IconButton>
</Tooltip>
<Tooltip title="Delete">
<IconButton
edge="end"
onClick={() => deleteArticle(article._id)}
color="error"
>
<DeleteIcon />
</IconButton>
</Tooltip>
</>
}
>
<ListItemText
primary={
<Link to={`/articles/${article._id}?authorId=${article.author._id}`}>
{article.title}
</Link>
}
secondary={`By ${article.author.name} on ${new Date(
article.createdAt
).toLocaleDateString()} | Categories: ${article.categories.join(
", "
)} | Tags: ${article.tags.join(", ")}`}
/>
</ListItem>
))}
</List>
</Container>
);
};
export default ArticleList;
Output
Approach 2: Debouncing
Debouncing is an effective technique to limit the number of API requests made during user input. Instead of firing an API request on every keystroke, debouncing waits until the user stops typing for a certain period before sending the request. This approach is more suitable for larger datasets where filtering on the client side is not feasible.
Implementation Steps:
- Debounce Function: Implement a debounce function that delays the execution of the search request.
- State Management: Store the search query in a state variable and trigger the search logic only after the debounce delay.
- API Request: Use the search query to fetch the relevant articles or comments from the backend.
Note: Install the below dependency in your package.json file:
npm install lodash.debounceExample:
// App.jsx
import React from "react";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import AuthorList from "./pages/AuthorList";
import CreateAuthor from "./components/CreateAuthor";
import UpdateAuthor from "./components/UpdateAuthor";
import AuthorBio from "./pages/AuthorBio";
import CreateArticle from "./pages/CreateArticle";
import ArticleList from "./pages/ArticleList";
import ArticleDetail from "./pages/ArticleDetail";
const App = () => {
return (
<>
<Router>
<Routes>
<Route path="/" element={<AuthorList />} />
<Route path="/create-author" element={<CreateAuthor />} />
<Route path="/update-author/:id" element={<UpdateAuthor />} />
<Route path="/bio/:id" element={<AuthorBio />} />
<Route path="/create-article" element={<CreateArticle />} />
<Route path="/article" element={<ArticleList />} />
<Route path="/articles/:id" element={<ArticleDetail />} />
</Routes>
</Router>
</>
);
};
export default App;
// src/pages/ArticlList.jsx
import React, { useState, useEffect, useCallback } from "react";
import axios from "axios";
import { Link } from "react-router-dom";
import {
Container,
Typography,
Button,
List,
ListItem,
ListItemText,
IconButton,
Tooltip,
Box,
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
Chip,
} from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit";
import debounce from "lodash.debounce";
const ArticleList = () => {
const [articles, setArticles] = useState([]);
const [categories, setCategories] = useState([]);
const [tags, setTags] = useState([]);
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState("");
const [selectedTags, setSelectedTags] = useState([]);
const [filteredArticles, setFilteredArticles] = useState([]);
useEffect(() => {
const fetchArticles = async () => {
try {
const response = await axios
.get("http://localhost:5000/api/articles");
setArticles(response.data);'
// Extract unique categories and tags
const allCategories = [
...new Set(response.data.flatMap((article) => article.categories)),
];
const allTags = [
...new Set(response.data.flatMap((article) => article.tags)),
];
setCategories(allCategories);
setTags(allTags);
} catch (error) {
console.error("Error fetching articles:", error);
}
};
fetchArticles();
}, []);
const debounceSearch = useCallback(
debounce((query) => {
const filtered = articles.filter((article) => {
const matchesSearch = article.title
.toLowerCase()
.includes(query.toLowerCase());
const matchesCategory =
!selectedCategory || article
.categories.includes(selectedCategory);
const matchesTags = selectedTags
.every((tag) => article.tags.includes(tag));
return matchesSearch && matchesCategory && matchesTags;
});
setFilteredArticles(filtered);
}, 500), // Debounce time in milliseconds
[articles, selectedCategory, selectedTags]
);
useEffect(() => {
debounceSearch(searchQuery);
}, [searchQuery, debounceSearch]);
const deleteArticle = async (id) => {
try {
await axios.delete(`http://localhost:5000/api/articles/${id}`);
setArticles(articles.filter((article) => article._id !== id));
} catch (error) {
console.error("Error deleting article:", error);
}
};
const handleTagChange = (event) => {
const { value } = event.target;
setSelectedTags(typeof value === "string" ? value.split(",") : value);
};
return (
<Container maxWidth="md" sx={{ mt: 4 }}>
<Typography variant="h4" gutterBottom>
Articles
</Typography>
<Button
variant="contained"
color="primary"
component={Link}
to="/create-article"
sx={{ mb: 2 }}
>
Write New Article
</Button>
<Box sx={{ mb: 2 }}>
<TextField
fullWidth
label="Search by title"
variant="outlined"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</Box>
<Box sx={{ mb: 2 }}>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>Category</InputLabel>
<Select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
label="Category"
>
<MenuItem value="">All Categories</MenuItem>
{categories.map((category) => (
<MenuItem key={category} value={category}>
{category}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Tags</InputLabel>
<Select
multiple
value={selectedTags}
onChange={handleTagChange}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} />
))}
</Box>
)}
>
{tags.map((tag) => (
<MenuItem key={tag} value={tag}>
{tag}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
<List>
{filteredArticles.map((article) => (
<ListItem
key={article._id}
secondaryAction={
<>
<Tooltip title="Edit">
<IconButton
edge="end"
component={Link}
to={`/update-article/${article._id}`}
color="primary"
>
<EditIcon />
</IconButton>
</Tooltip>
<Tooltip title="Delete">
<IconButton
edge="end"
onClick={() => deleteArticle(article._id)}
color="error"
>
<DeleteIcon />
</IconButton>
</Tooltip>
</>
}
>
<ListItemText
primary={
<Link
to={`/articles/${article._id}?authorId=${article.author._id}`}>
{article.title}
</Link>
}
secondary={`By ${article.author.name} on ${new Date(
article.createdAt
).toLocaleDateString()} | Categories: ${
article.categories
.join(", ")} | Tags: ${article.tags.join(", ")}`}
/>
</ListItem>
))}
</List>
</Container>
);
};
export default ArticleList;