Displaying Tasks List

Displaying a Tasks List: From Data to UI

Welcome back! In the last lesson, you learned how to show important statistics about your tasks on a dashboard. That gave users a “big picture” view of their progress.

Now, let’s take the next step: displaying a detailed list of tasks. This list will show every task’s title, status, and due date, and provide a button to view each task’s details. For now, this will be read-only — users can look but not edit or delete yet.

By the end of this lesson, you will know how to:

  • Fetch a list of tasks from an API using SWR.
  • Create a reusable Button component for consistent UI.
  • Build a TaskRow component to display individual tasks.
  • Put everything together in a Tasks page, handling loading, errors, and empty states.

Recap: From Stats to Lists

In the previous unit, you fetched all tasks from the backend (/api/tasks) and calculated summary numbers (total, completed, incomplete). That was a high-level overview.

In this unit, we’re using the same backend endpoint, but instead of summarizing tasks, we’ll display each one in detail. Think of it as zooming in:

  • Dashboard → summary counts.
  • Tasks page → detailed list of tasks.

The Reusable Button Component

Buttons are everywhere in web apps. To keep your UI consistent and avoid repeating code, we’ll use a reusable Button component.

Here’s the code:

import { ButtonHTMLAttributes, forwardRef } from 'react';
import clsx from 'clsx';

type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
  variant?: 'primary' | 'secondary' | 'danger';
};

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
  { className, variant = 'primary', ...props },
  ref
) {
  const base =
    'inline-flex items-center justify-center rounded-md text-sm font-medium px-3 py-2 transition-colors ' +
    'focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';

  const styles = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-600',
    secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-400',
    danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-600',
  } as const;

  return <button ref={ref} className={clsx(base, styles[variant], className)} {...props} />;
});

Let’s break it down carefully:

ButtonProps

  • We start with ButtonHTMLAttributes<HTMLButtonElement>. This means our component accepts all the normal button props (onClick, disabled, etc.).
  • Then we add our own prop: variant. This lets us choose styles ('primary' | 'secondary' | 'danger').

forwardRef

  • forwardRef is a React helper that lets us pass a ref down to the underlying <button> element.
  • A ref is like a direct reference to a DOM element. Other components can use it to focus the button, measure it, or integrate with accessibility tools.

In this course, we do not need advanced ref behavior yet, but using forwardRef makes the Button ready for common accessibility and focus-management integrations you may add later.

base

  • A string of Tailwind CSS classes that apply to every button, no matter the variant.
  • It sets things like inline-flex (so text and icons align), padding (px-3 py-2), focus styles (focus:ring), and disabled styles.

styles

  • An object that maps each variant to its specific colors.
  • Example: "primary" = blue background, white text. "danger" = red background.

clsx

  • A small utility library that combines multiple class strings into one.
  • Here, it merges the base classes with the chosen styles[variant] and any extra className passed in.

In this CodeSignal environment, clsx may be backed by a minimal local shim that supports the simple string-joining pattern used here. The full clsx package supports richer patterns such as arrays and objects.

This design makes the button:

  • Reusable (same code, different contexts).
  • Consistent (all buttons share the same base look).
  • Customizable (pick variant="secondary" when needed).

Example usage:

<Button variant="primary">Save</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="danger">Delete</Button>

The TaskRow Component: Displaying One Task

Now let’s build a component that shows a single task row in the list:

import Link from 'next/link';

export function TaskRow({ task }: { task: any }) {
  return (
    <div className="flex items-center justify-between p-4">
      <div>
        <div className="flex items-center gap-2">
          <span className="font-medium">{task.title}</span>
          <span
            className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
              task.completed ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'
            }`}
          >
            {task.completed ? 'Completed' : 'Pending'}
          </span>
        </div>
        <div className="text-sm text-gray-500">
          {task.dueDate ? `Due: ${task.dueDate}` : 'No due date'}
        </div>
      </div>
      <div className="flex items-center gap-2">
        <Link href={`/tasks/${task.id}`}>
          <Button variant="secondary">View</Button>
        </Link>
      </div>
    </div>
  );
}

Breaking it down:

  • Layout
    The outer <div> uses flex with justify-between so the task info is on the left and the button is on the right.

  • Title and Status
    The task’s title appears in bold.
    Next to it, a status badge shows whether it’s completed or pending.
    Conditional rendering is used: if task.completed is true → green badge, else → yellow badge.

  • Due Date
    Another line shows the due date.
    If task.dueDate exists, we display it. Otherwise, we show “No due date.”

  • View Button
    On the right side, we use our reusable Button component with variant="secondary".
    It’s wrapped in a Next.js <Link>. Clicking it navigates to a details page for that task (/tasks/[id]).

This demonstrates how we can compose components: Link handles navigation, and Button handles styling and behavior.

The Tasks Page: Bringing It All Together

Finally, we put everything into a full page that fetches tasks and renders them in a list:

'use client';
import Link from 'next/link';
import useSWR from 'swr';
import { api } from '@/lib/apiClient';
import { TaskRow } from '@/components/tasks/TaskRow';

export default function TasksPage() {
  const { data, error, isLoading } = useSWR('/api/tasks', api.get<any[]>);
  const tasks = Array.isArray(data) ? data : [];

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <h1 className="text-2xl font-semibold">Tasks</h1>
        <Link href="/tasks/new">
          <Button>New Task</Button>
        </Link>
      </div>

      {isLoading && <div className="text-gray-500">Loading tasks…</div>}
      {error && <div className="text-red-600">Failed to load tasks.</div>}

      <div className="divide-y rounded-lg border bg-white">
        {(Array.isArray(data) ? data : []).map((t) => (
          <TaskRow key={t.id} task={t} />
        ))}
        {(!data || data.length === 0) && (
          <div className="p-4 text-gray-500">No tasks yet.</div>
        )}
      </div>
    </div>
  );
}

Detailed explanation:

  • Data fetching
    useSWR('/api/tasks', api.get) fetches the tasks array from the backend.
    While waiting, isLoading is true.
    If something goes wrong, error is set.

  • Header bar
    Shows the page title (“Tasks”).
    A New Task button is included as a placeholder navigation affordance for a later course; creating tasks is not implemented in this read-only course.

  • Loading & error states
    If isLoading → show a gray “Loading tasks…” message.
    If error → show a red error message.

  • Task list
    If we have data, map through it and render a TaskRow for each task.
    key={t.id} ensures React can track each row.
    If no tasks exist, show a “No tasks yet” message.

This combination ensures that no matter the state (loading, error, empty, success), the page always gives users clear feedback.

Summary and Practice Preview

In this lesson, you:

  • Reviewed how to fetch a list of tasks from the backend using SWR.
  • Built a reusable Button component, learning about props, variants, forwardRef, clsx, and refs.
  • Created a TaskRow component that conditionally renders status, due dates, and action buttons.
  • Put everything together in a Tasks page that handles all states: loading, error, empty, and success.

Next, you’ll practice fetching and presenting data yourself — reinforcing how to design reusable UI and how to connect data from the backend to the frontend.

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