Open In App

Implement Search Functionality for Blogs And News Website

Last Updated : 28 Aug, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

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:

JavaScript
// 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;
JavaScript
// 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.debounce

Example:

JavaScript
// 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;
JavaScript
// 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;

Output


Next Article

Similar Reads