Cross View Cache Sync

Introduction: Why Cross-View Sync Matters

In the previous lesson, you learned how to filter tasks based on completion status.
Now imagine this scenario:

You open the Incomplete filter view and mark a task as Complete.

Then you switch to the Completed view — but the newly completed task doesn’t appear right away.

Or you go back to All Tasks, and the status hasn’t updated there either.

That inconsistency makes your app feel unreliable.

In this lesson, you’ll fix that by implementing Cross-View Cache Synchronization — ensuring that task updates propagate instantly across all views:

  • /tasks → All Tasks
  • /tasks/filter?completed=true → Completed Tasks
  • /tasks/filter?completed=false → Incomplete Tasks
  • / → Dashboard summary counts

No page refreshes, no waiting for a full reload — everything stays perfectly in sync using SWR’s caching system.

The Goal: One Change, Everywhere

After completing this unit, your app will:

  • Reflect task status changes instantly across All, Filtered, and Dashboard views.
  • Automatically update counts (Total, Completed, Incomplete) on the Dashboard.
  • Remove or add tasks in the appropriate filter views (e.g., completed tasks disappear from the “Incomplete” view immediately).
  • Require no manual refresh — everything is handled through SWR’s mutate() function.

Understanding the Problem

Each page in your app uses SWR to fetch and cache data independently:

  • /api/tasks → used by /tasks (All Tasks)
  • /api/tasks/filter?completed=true → used by Completed filter view
  • /api/tasks/filter?completed=false → used by Incomplete filter view
  • /api/tasks/:id → used by the Task Detail Page

Each of these URLs is a unique cache key in SWR’s global store.
If you only update one cache key after toggling a task, the other caches remain stale — causing one view to show outdated data.

That’s where cross-view cache sync comes in.
You’ll explicitly tell SWR which caches to update using the mutate() function and a new prop: revalidateKeys.

How revalidateKeys Enables Cross-View Sync

Each TaskRow now accepts an optional prop named revalidateKeys.
It’s an array of cache keys that the component should refresh when a task’s status changes.

Example from the updated Tasks Page (src/app/(dashboard)/tasks/page.tsx):

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

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

  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} revalidateKeys={["/api/tasks"]} />
        ))}
        {(!data || data.length === 0) && (
          <div className="p-4 text-gray-500">No tasks yet.</div>
        )}
      </div>
    </div>
  );
}

What’s new here:

  • The main Tasks Page does not need to pass /api/tasks as a revalidateKeys value because TaskRow already updates and revalidates that cache key directly.
  • Filtered views can still pass their filtered cache keys so TaskRow knows which additional lists should update.
  • This same pattern will later extend to the filtered views, where you’ll pass multiple revalidate keys (e.g., both /api/tasks and /api/tasks/filter?...).

Inside the TaskRow Component: Four Smaller Pieces

The full component is easiest to understand as four separate responsibilities:

  1. Main-list optimistic update — immediately update /api/tasks.
  2. Filtered cache update — move the task into or out of filtered lists.
  3. Server confirmation — send the PATCH request.
  4. Rollback and revalidation — recover if the server rejects the update.

Here is the complete version, followed by a breakdown of each piece:

"use client";
import Link from 'next/link';
import { Button } from '@/components/ui/Button';
import { api } from '@/lib/apiClient';
import { useToast } from '@/components/ui/Toast';
import { mutate } from 'swr';
import { useState } from 'react';

export function TaskRow({ task, revalidateKeys = [] }: { task: any; revalidateKeys?: string[] }) {
  const toast = useToast();
  const [isUpdating, setIsUpdating] = useState(false);

  const toggle = async () => {
    if (isUpdating) return;
    setIsUpdating(true);

    const key = `/api/tasks/${task.id}`;
    const optimistic = { ...task, completed: !task.completed };

    // Optimistically update the main list
    mutate('/api/tasks', (current: any[] = []) => current.map((t) => (t.id === task.id ? optimistic : t)), false);

    // Update all relevant filtered lists
    for (const listKey of revalidateKeys) {
      if (!listKey.startsWith('/api/tasks/filter')) continue;
      try {
        const query = listKey.split('?')[1] || '';
        const params = new URLSearchParams(query);
        const p = params.get('completed');
        if (p === 'true' || p === 'false') {
          const filterCompleted = p === 'true';
          const shouldInclude = optimistic.completed === filterCompleted;
          mutate(
            listKey,
            (current: any[] = []) => {
              const updated = current.map((t) => (t.id === task.id ? optimistic : t));
              // Remove task if it no longer matches the filter
              return shouldInclude ? updated : updated.filter((t) => t.id !== task.id);
            },
            false
          );
        }
      } catch {
        // skip invalid filter keys
      }
    }

    // API request to update completion status
    const res = await api.patch(key, { completed: optimistic.completed });

    if ('error' in res) {
      toast.error('Failed to update status');
      // Roll back and revalidate to correct data
      mutate(key);
      mutate('/api/tasks');
      for (const k of revalidateKeys) mutate(k);
      setIsUpdating(false);
      return;
    }

    // Revalidate all keys to confirm correct server state
    mutate(key);
    mutate('/api/tasks');
    for (const k of revalidateKeys) mutate(k);
    toast.success('Updated');
    setIsUpdating(false);
  };

  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}`} className="inline-flex items-center rounded-md border px-3 py-2 text-sm font-medium hover:bg-gray-100">
          View
        </Link>
        <Button onClick={toggle} disabled={isUpdating}>
          {task.completed ? 'Mark Incomplete' : 'Mark Complete'}
        </Button>
      </div>
    </div>
  );
}

Let’s break this down step by step:

1. Optimistic Updates This is the familiar optimistic update pattern: update the visible /api/tasks cache first, then confirm with the server.

const optimistic = { ...task, completed: !task.completed };
mutate('/api/tasks', (current = []) => current.map((t) => (t.id === task.id ? optimistic : t)), false);

SWR updates the /api/tasks cache immediately.

The third parameter (false) means “don’t refetch yet” — we’re doing an optimistic update.
The result: the task’s completion status updates visually right away.

2. Updating All Filtered Lists

for (const listKey of revalidateKeys) {
  if (!listKey.startsWith('/api/tasks/filter')) continue;
  ...
  mutate(listKey, updatedList, false);
}

This ensures:

  • Completed tasks disappear from /tasks/filter?completed=false.
  • Incomplete tasks disappear from /tasks/filter?completed=true.

Each affected cache is updated instantly.

3. Server Confirmation and Revalidation

const res = await api.patch(key, { completed: optimistic.completed });
...
mutate(key);
mutate('/api/tasks');
for (const k of revalidateKeys) mutate(k);

Once the server responds:

  • All caches are revalidated to ensure they match the server truth.
  • If an error occurs, the caches are rolled back to the correct state.
  • Toasts give clear success/error feedback.

Why This Works So Well

This approach leverages SWR’s global cache to maintain data consistency between multiple views that fetch the same resources differently.

Here’s what happens under the hood:

  1. You toggle a task’s status → SWR updates /api/tasks instantly.
  2. Filtered lists (like /api/tasks/filter?completed=false) update too.
  3. Dashboard summary (which depends on /api/tasks) automatically recalculates counts because it’s subscribed to the same cache.
  4. When SWR revalidates, all caches confirm correctness with the server.

No reloading. No refetching entire pages. Just smooth, synchronized updates across every view.

Example in Action

  • You’re on /tasks/filter?completed=false.
  • You click “Mark Complete” on a task.
  • The task instantly disappears from this list (it no longer matches the filter).
  • If you switch to /tasks/filter?completed=true, it’s already there.
  • On /, the Dashboard’s “Completed” count increases immediately.

All of this happens without a single page reload.

Summary

In this lesson, you learned how to:

  • Keep every task view in your app perfectly synchronized using SWR’s mutate().
  • Use the revalidateKeys prop to tell components which caches to refresh.
  • Ensure instant, cross-view updates between /tasks, /tasks/filter, and /.
  • Automatically update dashboard summaries and filtered lists without reloading.
  • Combine optimistic updates, error handling, and revalidation for production-level consistency.

Your task manager is now real-time at the UI level — every page stays accurate and responsive, no matter where changes happen.

In the next and final unit, you’ll wrap up by polishing your app’s mobile layout and production UI, giving it the look and feel of a professional web app.

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