React Components: Building Search, Filter and Pagination Functionality

As a full-stack developer, I‘ve lost count of how many times I‘ve had to implement search, filtering, and pagination functionality in my apps. And for good reason – they are essential interface components for any app that manages a large collection of data.

Search allows users to quickly find specific items by keywords. Filtering enables users to narrow down the data by categories. And pagination improves performance and usability by splitting the data into discrete pages. Together, they provide an intuitive way for users to navigate and make sense of large datasets.

In this guide, we‘ll walk through how to build these interface components in a React app. I‘ll explain the concepts, provide working examples, and share some best practices I‘ve learned along the way. By the end, you‘ll have a solid blueprint for adding search, filtering, and pagination to your own React projects.

Setting Up the Sample Project

To demonstrate these concepts, we‘ll build a simple contact management app in React. Our app will fetch a dataset of people and allow the user to search, filter, and paginate the records.

Let‘s start by spinning up a new project using Create React App:

npx create-react-app contact-manager
cd contact-manager
npm start

Next, we‘ll define our sample dataset in a separate contacts.js file:

export const contacts = [
  {
    "id": 1,
    "name": "Leanne Graham",
    "email": "[email protected]",
    "phone": "1-770-736-8031 x56442",
    "company": "Romaguera-Crona"
  },
  {
    "id": 2,
    "name": "Ervin Howell",
    "email": "[email protected]",  
    "phone": "010-692-6593 x09125",
    "company": "Deckow-Crist"
  },
  // ... more contacts
];

Building the Search Component

Our search component will consist of a search input where the user can type a query. As they type, we‘ll filter the displayed contacts to only those that match the search string.

In our App.js file, let‘s import the contacts data and set up the basic structure:

import React, { useState } from ‘react‘; 
import ‘./App.css‘;
import { contacts } from ‘./contacts‘;

function App() {
  const [query, setQuery] = useState(‘‘);

  const search = (data) => {
    return data.filter(item => item.name.toLowerCase().includes(query) 
      || item.email.toLowerCase().includes(query)
      || item.phone.toLowerCase().includes(query) 
      || item.company.toLowerCase().includes(query)
    );
  };

  return (
    <div className="app">
      <input 
        className="search-input" 
        placeholder="Search..." 
        onChange={e => setQuery(e.target.value)}
      />
      <ul className="list">
        {search(contacts).map(item => (
          <li key={item.id} className="list-item">
            {item.name}
          </li>
        ))}  
      </ul>
    </div>
  );
}

export default App;

Here‘s how it works:

  1. We import the useState hook to manage the state of the search query
  2. In the search input, we update the query state variable whenever the input value changes
  3. We define a search function that filters the contacts data to only items where the name, email, phone or company includes the query string (case-insensitive)
  4. Finally, we map over the filtered data to render a list of matching contacts

With a bit of CSS styling, here‘s what our working search component looks like:

Search component

Adding the Filter Functionality

Next, let‘s add the ability to filter the contacts by company. We‘ll render a dropdown of all the unique company names, and filter the results when a company is selected.

function App() {
  const [query, setQuery] = useState(‘‘);
  const [company, setCompany] = useState(‘‘);

  const companies = [...new Set(contacts.map(item => item.company))];

  const search = (data) => {
    return data.filter(item => 
      (item.name.toLowerCase().includes(query) 
      || item.email.toLowerCase().includes(query)
      || item.phone.toLowerCase().includes(query) 
      || item.company.toLowerCase().includes(query))
      && (!company || item.company === company)
    );
  };

  return (
    <div className="app">
      <input 
        className="search-input" 
        placeholder="Search..." 
        onChange={e => setQuery(e.target.value)}
      />
      <select onChange={e => setCompany(e.target.value)}>
        <option value="">All Companies</option>
        {companies.map(company => (
          <option key={company} value={company}>
            {company}
          </option>
        ))}
      </select>

      <ul className="list">
        {search(contacts).map(item => (
          <li key={item.id} className="list-item">
            {item.name} - {item.company}  
          </li>
        ))}
      </ul>
    </div>
  );
}

The key changes are:

  1. We create a companies array of unique company names using Set
  2. We add a select dropdown with the company names, and an "All Companies" default option
  3. We update the search function to additionally filter by the selected company (or show all if no company selected)

With those changes, our app now supports both searching and filtering:

Search and filter

Implementing Pagination

For large datasets, it‘s often necessary to split the data into pages for better performance and usability. Let‘s add a simple pagination component to our app.


function App() {
  const [query, setQuery] = useState(‘‘);
  const [company, setCompany] = useState(‘‘);
  const [currentPage, setCurrentPage] = useState(1);
  const pageSize = 10;

  const companies = [...new Set(contacts.map(item => item.company))];

  const search = (data) => {
    return data.filter(item => 
      (item.name.toLowerCase().includes(query) 
      || item.email.toLowerCase().includes(query)
      || item.phone.toLowerCase().includes(query) 
      || item.company.toLowerCase().includes(query))
      && (!company || item.company === company)
    );
  };

  const data = search(contacts);
  const numberOfPages = Math.ceil(data.length / pageSize);

  return (
    <div className="app">
      <input 
        className="search-input" 
        placeholder="Search..." 
        onChange={e => setQuery(e.target.value)}
      />
      <select onChange={e => setCompany(e.target.value)}>
        <option value="">All Companies</option>
        {companies.map(company => (
          <option key={company} value={company}>
            {company}
          </option>
        ))}
      </select>

      <ul className="list">
        {data
          .slice((currentPage - 1) * pageSize, currentPage * pageSize)
          .map(item => (
            <li key={item.id} className="list-item">
              {item.name} - {item.company}
            </li>
        ))}
      </ul>

      <div className="pagination">
        {Array.from({ length: numberOfPages }, (_, i) => i + 1).map(page => (
          <button 
            key={page} 
            onClick={() => setCurrentPage(page)}
            className={page === currentPage ? ‘active‘ : ‘‘}
          >
            {page}
          </button>
        ))}
      </div>
    </div>
  );  
}

To implement pagination, we:

  1. Define a pageSize constant for the number of items per page
  2. Keep track of the currentPage in state
  3. Calculate the total numberOfPages based on the filtered data length and page size
  4. Slice the data array based on the current page
  5. Render a series of page buttons that update the currentPage state when clicked

Here‘s the result:

Pagination component

Performance Considerations

While our example app is relatively simple, there are a few key performance considerations to keep in mind when implementing search, filtering, and pagination in a React app.

First, make sure your search and filter functions are optimized and not causing unnecessary re-renders. In our example, we‘re filtering the full contacts array on every render. For larger datasets, it would be more efficient to memoize the filtered and paginated data using useMemo.

Second, consider debouncing the search input to avoid filtering the data on every keystroke, which can be expensive for large datasets. You can use a library like lodash or implement your own debounce function.

Finally, if you‘re working with very large datasets, it‘s often better to handle pagination and filtering on the server-side via API queries. This reduces the amount of data transferred to the client and takes advantage of backend optimizations like indexed lookups.

Conclusion

In this guide, we covered how to add search, filtering, and pagination functionality to a React app. These are essential interface components for any app dealing with collections of data.

We walked through a practical example of building these components in the context of a contact management app. You saw how to implement a search input, company filter dropdown, and page buttons.

We also discussed some performance considerations and best practices to keep in mind, such as memoizing computed values, debouncing user input, and leveraging server-side processing for large datasets.

Adding search, filtering, and pagination to your React apps doesn‘t have to be complicated. By following the patterns outlined in this guide, you‘ll be able to implement these features with confidence and provide your users with a better experience for navigating and understanding their data.

I hope you found this guide helpful! Feel free to reach out if you have any questions or suggestions. Happy coding!

Similar Posts