Reputation: 211
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?
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
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