Implementing Pagination in Your To-Do Application

Introduction to Implementing Pagination

Welcome to the next step in enhancing your to-do application! In this lesson, we will focus on implementing pagination. As your list of to-do items grows, it becomes essential to manage how they are displayed to ensure a smooth user experience. Pagination helps in breaking down the list into manageable chunks, making it easier for users to navigate through their tasks. By the end of this lesson, you'll be able to implement pagination in your application, providing a more organized and user-friendly interface.

What You'll Learn

In this lesson, you will learn how to add pagination to your to-do application using the Material-UI (MUI) Pagination component. This involves dividing your list of to-do items into pages and allowing users to navigate between them. Here's a quick look at how this can be integrated into the Todos.js file:

const Todos = () => {
  const [todos, setTodos] = useState([]); // State to store the list of todos
  const [error, setError] = useState(null); // State to store any error messages
  const [loading, setLoading] = useState(true); // State to manage loading status
  const [filter, setFilter] = useState('all'); // State to manage the current filter
  const [searchTitle, setSearchTitle] = useState(''); // State to manage the search input
  const [currentPage, setCurrentPage] = useState(1); // State to track the current page
  const [todosPerPage, setTodosPerPage] = useState(5); // State to set the number of todos per page
  const { token } = useAuth(); // Retrieve authentication token
  const todoService = useTodoService(); // Service to interact with the todo API

  const fetchTodos = async () => {
    try {
      const data = await todoService.getTodos(filter, searchTitle);
      if (!data?.error) {
        setTodos(data); // Update todos state with fetched data
      } else {
        setError(data.error); // Set error state if there's an error in the response
      }
    } catch (error) {
      setError('An error occurred while fetching data'); // Handle fetch errors
    } finally {
      setLoading(false); // Set loading to false after fetch attempt
    }
  };

In this snippet, we initialize the state variables needed for managing todos, errors, loading status, filters, search titles, pagination, and authentication. The fetchTodos function is defined to retrieve the list of todos from the service, handling any errors that may occur.

  useEffect(() => {
    if (token) {
      fetchTodos(); // Fetch todos when the component mounts or when dependencies change
    }
  }, [token, filter, searchTitle]);

  const handleDelete = async (id) => {
    try {
      await todoService.deleteTodo(id); // Delete the todo with the given id
      fetchTodos(); // Refresh the list of todos after deletion
    } catch (error) {
      setError('An error occurred while deleting the todo'); // Handle deletion errors
    }
  };

  // Calculate pagination values
  const indexOfLastTodo = currentPage * todosPerPage; // Determine the index of the last todo on the current page
  const indexOfFirstTodo = indexOfLastTodo - todosPerPage; // Determine the index of the first todo on the current page
  const currentTodos = todos.slice(indexOfFirstTodo, indexOfLastTodo); // Slice the todos array to get the current page's todos
  const totalPages = Math.ceil(todos.length / todosPerPage); // Calculate the total number of pages

  const handlePageChange = (event, value) => {
    setCurrentPage(value); // Update the current page when the user navigates
  };

This snippet includes the useEffect hook to fetch todos whenever the token, filter, or search title changes. It also defines the handleDelete function to remove a todo and recalculate pagination values to determine which todos to display on the current page. The handlePageChange function updates the current page when the user navigates through pages.

  return (
      ...
      <FormControl style={{ marginBottom: '20px', marginTop: '10px', minWidth: '120px' }}>
        <InputLabel>Items per page</InputLabel>
        <Select
          value={todosPerPage}
          onChange={(e) => {
            setTodosPerPage(Number(e.target.value)); // Update the number of todos per page
            setCurrentPage(1); // Reset to first page when changing items per page
          }}
          label="Items per page"
        >
          <MenuItem value={5}>5</MenuItem>
          <MenuItem value={10}>10</MenuItem>
          <MenuItem value={20}>20</MenuItem>
        </Select>
      </FormControl>
    ...

Here, we render a dropdown menu to allow users to select the number of todos displayed per page. Changing this value resets the current page to the first page to ensure a smooth user experience.

      {todos.length ? (
        <>
          <List>
            {currentTodos.map(todo => (
              <ListItem key={todo.id} sx={{ width: '400px' }}>
                <TodoDetails todo={todo} onDelete={handleDelete} /> {/* Render each todo item */}
              </ListItem>
            ))}
          </List>
          <Box display="flex" justifyContent="center" mt={2}>
            <Pagination 
              count={totalPages} 
              page={currentPage} 
              onChange={handlePageChange} 
              color="primary" 
            /> {/* Render pagination controls */}
          </Box>
        </>
      ) : (
        <Typography variant="h6">No Todos Found</Typography> {/* Display message if no todos */}
      )}

This snippet renders the list of current todos and includes the MUI Pagination component to navigate between pages. If no todos are found, a message is displayed to inform the user.

Server-Side Pagination

While client-side pagination is useful for small datasets, it can become inefficient as the number of to-do items grows significantly. This is where server-side pagination comes into play. By handling pagination on the server, you can reduce the amount of data sent to the client, improving performance and reducing load times. Server-side pagination is especially beneficial when dealing with large datasets, as it allows the server to send only the necessary data for the current page, rather than the entire dataset.

Why Server-Side Pagination Matters

  1. Performance Optimization: By fetching only the required data for the current page, you minimize the amount of data transferred over the network, leading to faster load times and a more responsive application.
  2. Scalability: Server-side pagination allows your application to handle larger datasets efficiently, as the server manages the data slicing and only sends the relevant portion to the client.
  3. Reduced Client Load: With less data to process and render, the client-side application can perform better, especially on devices with limited resources.

Example: Fetch Request for Server-Side Pagination

To implement server-side pagination, you need to modify your fetch request to include pagination parameters such as page and limit. Here's an example of how the fetch request might look:

const getTodos = async (filter = 'all', searchTitle = '', page = 1, limit = 5) => {
  let url = '/api/todos';
  let params = [];
  
  if (filter === 'completed') {
    params.push('done=true'); // Add filter for completed todos
  } else if (filter === 'pending') {
    params.push('done=false'); // Add filter for pending todos
  }
  
  if (searchTitle) {
    params.push(`title=${encodeURIComponent(searchTitle)}`); // Add search title parameter
  }
  
  // Add pagination parameters
  params.push(`page=${page}`);
  params.push(`limit=${limit}`);
  
  if (params.length > 0) {
    url += '?' + params.join('&'); // Construct the query string
  }

  const response = await fetch(url, {
    headers: {
      'Authorization': `Bearer ${token}`, // Include authorization header
    },
  });
  if (!response.ok) {
    throw new Error('Failed to fetch todos'); // Handle fetch errors
  }
  return response.json(); // Return the JSON response
};

In this example, the server is expected to handle the pagination logic and return only the relevant to-do items for the specified page and limit, along with the total number of pages. The backend API should support page and limit query parameters and return paginated results in a structured response format. This approach ensures that your application remains efficient and scalable as the dataset grows.

Why It Matters

Implementing pagination is crucial for maintaining a clean and efficient user interface, especially as the number of tasks grows. Without pagination, users might find it overwhelming to scroll through a long list of items. By breaking the list into pages, you enhance the user experience, making it easier for users to find and manage their tasks. This feature not only improves usability but also optimizes the performance of your application by reducing the amount of data rendered at once.

Ready to make your to-do application more dynamic and user-friendly? Let's dive into the practice section and start implementing pagination together!

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal