---
title: "Simplifying code with standardized pagination, sorting, and search"
description:
  "Explore the power of standardized naming practices in creating a unified and
  scalable codebase."
canonical_url: "https://www.bigbinary.com/blog/standardize-pagination-keywords"
markdown_url: "https://www.bigbinary.com/blog/standardize-pagination-keywords.md"
---

# Simplifying code with standardized pagination, sorting, and search

Explore the power of standardized naming practices in creating a unified and
scalable codebase.

- Author: Abilash Sajeev
- Published: August 27, 2024
- Categories: ReactJS, Rails

[Neeto](https://www.neeto.com) is a collection of products. Here, we standardize
boilerplate and repetitive code into
[Nanos](https://blog.neeto.com/p/nanos-make-neeto-better). The search, sorting,
and pagination functionalities are essential for every listing page. We realized
that each product had its own custom implementations for these operations,
resulting in a lot of duplicated code. We wanted this logic to be abstracted and
handled uniformly.

## Sorting and pagination

If you have worked with tables, you may already be familiar with the following
code. In this example, we are using the
[Table](https://neeto-ui.neeto.com/?path=/docs/components-table--docs) component
from [NeetoUI](https://neeto-ui.neeto.com/). It is the developer's
responsibility to handle the sorting and pagination logic, make the API calls
with an updated payload, and ensure that the URL is updated accordingly.

```javascript
import React, { useState } from "react";
import { Table } from "@bigbinary/neetoui";

const Teams = () => {
  const [page, setPage] = useState(1);
  const [sortBy, setSortBy] = useState(null);
  const [orderBy, setOrderBy] = useState(null);

  const handleSort = ({ sortBy, orderBy }) => {
    setSortBy(sortBy);
    setOrderBy(orderBy);
    // Custom sort logic
  };

  const handlePageChange = page => {
    setPage(page);
    // Custom pagination logic.
  };

  const { data: teams, isLoading } = useFetchTeams({ page, sortBy, orderBy });

  return (
    <Table
      currentPageNumber={page}
      handlePageChange={handlePageChange}
      onChange={(_, __, sorter) => handleSort(sorter)}
      {...{ totalCount, rowData, columnData, ...otherProps }}
    />
  );
};

export default Teams;
```

To make both the frontend and backend more standardized and reusable, we
established some conventions. The NeetoUI Table will handle both sorting and
pagination internally. When the user navigates to a different page, NeetoUI will
update the `page` and `page_size` query parameters in the URL.

Similarly, when a column is sorted, the `sort_by` and `order_by` query
parameters are updated. We no longer need a dedicated state to store these
values. When the query parameter changes, the API will be called with the
modified payload.

Here is what the simplified code looks like:

```javascript
import React from "react";
import { Table } from "@bigbinary/neetoui";
import { useQueryParams } = "@bigbinary/neeto-commons-frontend/react-utils"

const Teams = () => {
  const { page, sortBy, orderBy } = useQueryParams();

  const { data: teams, isLoading } = useFetchTeams({ page,
                                                     sortBy,
                                                     orderBy });

  return (
    <Table {...{ totalCount, rowData, columnData, ...otherProps }} />
  );
};
export default Teams;
```

Here, we utilize a `useQueryParams` utility helper. It parses the URL and
returns the query parameters after converting them to camel case.

We noticed that a similar cleanup can be done on the backend side. After
establishing a consistent variable naming convention for `page`, `page_size`,
`order_by`, and `sort_by` in every listing page, it became easier to create
common utility functions for the backend. Here's what it looks like:

```ruby
module Filterable
  extend ActiveSupport::Concern
  include Pagy::Backend

  def sort_and_paginate(records)
    sorted_records = apply_sort(records)
    apply_pagination(sorted_records)
  end

  def apply_sort(records)
    records.order(sort_by => order_by)
  end

  def sort_by
    params[:sort_by].presence || "created_at"
  end

  def order_by
    case params[:order_by]&.downcase
    when "asc", "ascend"
      "ASC"
    when "desc", "descend"
      "DESC"
    else
      "ASC"
    end
  end

  def apply_pagination(records)
    pagination_method = records.is_a?(ActiveRecord::Relation) ? :pagy : :pagy_array
    pagy, paginated_records = send(pagination_method, records, page: params[:page], items: params[:page_size])

    [pagy, paginated_records]
  end
end
```

The `apply_pagination` helper method can be used to paginate the results. It
uses the `Pagy` gem internally to perform the pagination. It provides a generic
method `pagy` that works with `ActiveRecord` out of the box. It also provides a
`pagy_array` method which paginates an array of records. Both these methods
returns a `Pagy` instance along with the paginated records.

The `apply_sort` method sorts the given records based on a specified column name
and direction. By default, records are sorted in ascending order of `created_at`
timestamps, but this can be customized by specifying values for `sort_by` and
`order_by` params. As sorting and pagination are common requirements of the
listing pages, we also added a `sort_and_paginate` method which performs both
these operations on provided records.

## Searching

Searching is a common functionality in any web application. Developers need to
ensure that the results are refetched whenever the search term changes. The
debouncing logic is added to minimize API requests and improve user experience.
As you can see in the below snippet, we had to maintain a dedicated state for
search term as well.

```javascript
import React, { useState } from "react";
import { useDebounce } from "@bigbinary/neeto-commons-frontend/react-utils";
import { useFetchTeams } from "hooks/reactQuery/useFetchTeamsApi";

const Teams = () => {
  const [searchString, setSearchString] = useState("");
  const debouncedSearchString = useDebounce(searchString.trim());

  const { data: teams, isLoading } = useFetchTeams(debouncedSearchString);

  return (
    <Input
      type="search"
      value={searchString}
      onChange={({ target: { value } }) => setSearchString(value)}
    />
  );
};

export default Teams;
```

As we standardized the naming pattern for the search term, we were able to
directly incorporate the search term value into the URL query parameters. This
helped us to retrieve the search term, eliminating the need for a separate
state.

We also introduced a new component in
[NeetoMolecules](https://neeto-molecules.neeto.com), to handle the search
functionality in all products. This
[Search](https://neeto-molecules.neeto.com/?path=/docs/search--docs) component
will internally handle the debounced updates when the search term changes. It
will also update the `search_term` query param in URL. Here is what the
simplified code looks like:

```javascript
import React from "react";
import Search from "@bigbinary/neeto-molecules/Search";
import { useFetchTeams } from "hooks/reactQuery/useFetchTeamsApi";

const Teams = () => {
  const { searchTerm = "" } = useQueryParams();

  const { data: teams, isLoading } = useFetchTeams(searchTerm);

  return <Search />;
};

export default Teams;
```

We also updated the `Filterable` concern mentioned earlier to simplify the logic
in the backend. The `search_term` method retrieves the keyword specified in the
URL query parameters, while `search?` checks whether the results should be
filtered based on any keyword.

```ruby
module Filterable
  extend ActiveSupport::Concern

  # ... previous code

  def search_term
    filter_params[:search_term]
  end

  def search?
    search_term.present?
  end
end
```

Let's consolidate everything in the `TeamsController`. In the following code, we
need to fetch all the teams belonging to an organization, filter them based on
the search term and return the results after sorting and pagination.

```ruby
def index
  if params[:search_string].present?
    @teams = @organization.teams.filter_by_name(params[:search_string])
  end

  @teams = @teams
    .order(params[:column] => params[:direction])
    .page(params[:current_page])
    .per(params[:limit])
end
```

We were able to simplify this logic with the help of the methods provided by the
`Filterable` concern. As you can see below, this helped us in removing a lot of
boilerplate code and improving the readability.

```ruby
def index
  @teams = @organization.teams.filter_by_name(search_term) if search?
  @teams = sort_and_paginate(@teams)
end
```

This standardized approach helped us to extract and centralize the common logic.
It also accelerated the development and maintenance cycle as the code structure
is now clear and consistent across all the products.

## Links

- [Human page](https://www.bigbinary.com/blog/standardize-pagination-keywords)
