nikostav96
nikostav96

Reputation: 211

How to implement pagination with filter in React

I have a React app where i use axios to get a list of 200 Todos from a free online REST API with fake data (https://jsonplaceholder.typicode.com/todos). My app has one input that you can search/filter for titles of todos and a select/filter that you can choose true or false for the completed todos. My problem is that i have implement a custom pagination that works well only with the default list of todos, so without filtering the list. For example, if you type in input search the word "vero" you must click in page "2" so you can see existing todo card. Same with select, if you select "true" you can see only 3 cards in the 1st page, but if you click in 2nd page you can see more etc. I have tried a lot but i can't make it work as i want. How can i implement pagination with filter?

Edit React list with filtering and pagination

App.js

import { useState, useEffect } from "react";
import axios from "axios";
import "./styles.css";

export default function App() {
  const [todos, setTodos] = useState([]);
  const [searchTerm, setSearchTerm] = useState("");
  const [filterCompleted, setFilterCompleted] = useState("");

  const [currentPage, setCurrentPage] = useState(1);
  const [todosPerPage, setTodosPerPage] = useState(10);

  // Get current todos
  const indexOfLastTodo = currentPage * todosPerPage;
  const indexOfFirstTodo = indexOfLastTodo - todosPerPage;
  const currentTodos = todos.slice(indexOfFirstTodo, indexOfLastTodo);

  const totalTodos = todos.length;
  const pageNumbers = [];

  for (let i = 1; i <= Math.ceil(totalTodos / todosPerPage); i++) {
    pageNumbers.push(i);
  }

  // Change page
  const paginate = (pageNumber) => setCurrentPage(pageNumber);

  useEffect(() => {
    axios
      .get(`https://jsonplaceholder.typicode.com/todos`)
      .then((response) => {
        setTodos(response.data);
      })
      .catch((error) => {
        console.log(error);
      });
  }, []);

  const resetFilter = () => {
    setSearchTerm("");
    setFilterCompleted("");
  };

  return (
    <div className="container">
      <h3>Filters</h3>
      <div className="mb-3">
        <label htmlFor="search" className="form-label">
          Search
        </label>
        <input
          type="text"
          className="form-control"
          id="search"
          placeholder="Search Title"
          value={searchTerm}
          onChange={(e) => {
            setSearchTerm(e.target.value);
          }}
        />
      </div>
      <div className="mb-3">
        <label htmlFor="search" className="form-label">
          Completed
        </label>
        <select
          className="form-select"
          value={filterCompleted}
          onChange={(e) => {
            setFilterCompleted(e.target.value);
          }}
        >
          <option defaultValue=""></option>
          <option value="true">true</option>
          <option value="false">false</option>
        </select>
      </div>
      <div className="mb-3">
        <button
          type="button"
          className="btn btn-danger btn-sm"
          onClick={resetFilter}
        >
          Reset Filters
        </button>
      </div>

      <nav>
        <ul className="pagination">
          {pageNumbers.map((number) => (
            <li key={number} className="page-item">
              <button onClick={() => paginate(number)} className="page-link">
                {number}
              </button>
            </li>
          ))}
        </ul>
      </nav>

      {currentTodos
        .filter((todo) => {
          if (searchTerm === "") {
            return todo;
          } else if (
            todo.title.toLowerCase().includes(searchTerm.toLowerCase())
          ) {
            return todo;
          }
        })
        .filter((todo) => {
          if (filterCompleted === "") {
            return todo;
          } else if (filterCompleted === "true" && todo.completed === true) {
            return todo;
          } else if (filterCompleted === "false" && todo.completed === false) {
            return todo;
          }
        })
        .map((todo) => {
          return (
            <div key={todo.id} className="card margin-bottom">
              <h5 className="card-header">
                <div className="card-header-flex">
                  <span className="id">{`#${todo.id}`}</span>
                </div>
              </h5>
              <div className="card-body">
                <div className="card-text">
                  <div className="card-body-flex">
                    <span>{`Title: ${todo.title}`}</span>
                    <span>{`Completed: ${todo.completed}`}</span>
                  </div>
                </div>
              </div>
            </div>
          );
        })}
    </div>
  );
}

Upvotes: 4

Views: 10723

Answers (1)

nikostav96
nikostav96

Reputation: 211

I fixed pagination with filter with the help of useMemo hook. The problem was that i was filtering the default array and i wasn't updating the totalTodos with the length of computed todos etc.. Now inside useMemo hook, i compute(filter) the todos first and then i update totalTodos with the length of computed todos.

import { useState, useEffect, useMemo } from "react";
import axios from "axios";
import "./styles.css";

export default function App() {
  const [todos, setTodos] = useState([]);
  const [searchTerm, setSearchTerm] = useState("");
  const [filterCompleted, setFilterCompleted] = useState("");

  const [currentPage, setCurrentPage] = useState(1);
  const [totalTodos, setTotalTodos] = useState(0);
  const todosPerPage = 10;

  useEffect(() => {
    axios
      .get(`https://jsonplaceholder.typicode.com/todos`)
      .then((response) => {
        setTodos(response.data);
      })
      .catch((error) => {
        console.log(error);
      });
  }, []);

  
  const pageNumbers = [];

  for (let i = 1; i <= Math.ceil(totalTodos / todosPerPage); i++) {
    pageNumbers.push(i);
  }


  const todosData = useMemo(() => {
    let computedTodos = todos;

    if (searchTerm) {
        computedTodos = computedTodos.filter(
            todo =>
            todo.title.toLowerCase().includes(searchTerm.toLowerCase())
        );
    }

    if (filterCompleted === "true") {
      computedTodos = computedTodos.filter(
          todo =>
          filterCompleted === "true" && todo.completed === true
      )
  }

  if (filterCompleted === "false") {
    computedTodos = computedTodos.filter(
        todo =>
        filterCompleted === "false" && todo.completed === false
    )
  }

    setTotalTodos(computedTodos.length);

    //Current Page slice
    return computedTodos.slice(
        (currentPage - 1) * todosPerPage,
        (currentPage - 1) * todosPerPage + todosPerPage
    );
}, [todos, currentPage, searchTerm, filterCompleted]);
  // Change page
  const paginate = (pageNumber) => setCurrentPage(pageNumber);

  const resetFilter = () => {
    setSearchTerm("");
    setFilterCompleted("");
    setCurrentPage(1);
  };

  return (
    <div className="container">
      <h3>Filters</h3>
      <div className="mb-3">
        <label htmlFor="search" className="form-label">
          Search
        </label>
        <input
          type="text"
          className="form-control"
          id="search"
          placeholder="Search Title"
          value={searchTerm}
          onChange={(e) => {
            setSearchTerm(e.target.value);
            setCurrentPage(1);
          }}
        />
      </div>
      <div className="mb-3">
        <label htmlFor="search" className="form-label">
          Completed
        </label>
        <select
          className="form-select"
          value={filterCompleted}
          onChange={(e) => {
            setFilterCompleted(e.target.value);
            setCurrentPage(1);
          }}
        >
          <option defaultValue=""></option>
          <option value="true">true</option>
          <option value="false">false</option>
        </select>
      </div>
      <div className="mb-3">
        <button
          type="button"
          className="btn btn-danger btn-sm"
          onClick={resetFilter}
        >
          Reset Filters
        </button>
      </div>

      <nav>
        <ul className="pagination">
          {pageNumbers.map((number) => (
            <li key={number} className="page-item">
              <button onClick={() => paginate(number)} className="page-link">
                {number}
              </button>
            </li>
          ))}
        </ul>
      </nav>

      {todosData
        .map((todo) => {
          return (
            <div key={todo.id} className="card margin-bottom">
              <h5 className="card-header">
                <div className="card-header-flex">
                  <span className="id">{`#${todo.id}`}</span>
                </div>
              </h5>
              <div className="card-body">
                <div className="card-text">
                  <div className="card-body-flex">
                    <span>{`Title: ${todo.title}`}</span>
                    <span>{`Completed: ${todo.completed}`}</span>
                  </div>
                </div>
              </div>
            </div>
          );
        })}
    </div>
  );
}

Upvotes: 5

Related Questions