Understanding the SWR Return Object

And how SWR improves your data-fetching workflow

Read docs plz!
https://swr.vercel.app/docs/advanced/understanding

Code Example: SWR Return Object

const { data, error, isLoading, isValidating, mutate } = useSWR(
  "https://api.github.com/users/octocat",
  fetcher
);

SWR returns:

  • data – fetched or cached data
  • error – fetcher threw an error
  • isLoading – first load
  • isValidating – background revalidation
  • mutate – manual update or refetch

UI Logic Breakdown

if (error) return <p>Failed to load user</p>;
if (isLoading) return <p>Loading…</p>;

This gives:

  • Clean & predictable control flow
  • No empty/undefined rendering
  • Fast-to-read component structure

Showing Freshness with isValidating

{isValidating && <p>Updating…</p>}

Occurs during:

  • Background refetch
  • Browser refocus
  • Network reconnect
  • Manual mutate()

isLoading = first load
isValidating = any refresh after first load

Rendering User Data

<div>
  <h2>{data.name}</h2>
  <p>@{data.login}</p>
  <p>Followers: {data.followers}</p>
</div>

SWR ensures:

  • Instant rendering from cache
  • Smooth UI even while revalidating
  • No flicker or "blank" data

Manual Refresh with mutate()

<button onClick={() => mutate()}>Refresh</button>

This:

  1. Starts revalidation
  2. Fetches new data
  3. Updates UI automatically

mutate() = “refetch this key now”

Optimistic UI Example (Before Server Response)

mutate({ ...data, name: newName }, false);
  • UI updates immediately
  • No waiting for server
  • Great for responsive UX

Second argument = skip revalidation (for now)

Completing Optimistic Update

await fetch("/api/user", {
  method: "POST",
  body: JSON.stringify({ name: newName })
});

mutate(); // fetch real data

Ensures:

  • UI feels fast
  • Server gets real update
  • Data stays correct after refresh

BEFORE SWR: useEffect + fetch

function Profile() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch("/api/user")
      .then(r => r.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <p>Loading…</p>;
  if (error) return <p>Error!</p>;

  return <p>{data.name}</p>;
}

Problems:

  • Lots of boilerplate
  • Manual loading/error handling
  • No caching
  • No revalidation
  • Mutations are painful

AFTER SWR: Clean & Predictable

const { data, error, isLoading } = useSWR("/api/user", fetcher);

if (isLoading) return <p>Loading…</p>;
if (error) return <p>Error!</p>;

return <p>{data.name}</p>;

Benefits:

  • Caching built-in
  • Auto revalidation
  • Cleaner UI logic
  • Less code
  • Better user experience

BEFORE: Manual Refresh

function refresh() {
  setLoading(true);
  fetch("/api/user")
    .then(r => r.json())
    .then(setData)
    .finally(() => setLoading(false));
}

Lots of steps you must manage manually.

AFTER: SWR Refresh

<button onClick={() => mutate()}>Refresh</button>

SWR handles:

  • Setting loading state
  • Fetching new data
  • Updating UI
  • Caching

Summary

  • SWR simplifies the entire data lifecycle
  • data, error, isLoading, isValidating, mutate give full control
  • Revalidation keeps data fresh
  • Optimistic updates make apps feel fast
  • SWR eliminates boilerplate vs useEffect