Skip to the solution
EasyMachine Coding10 min read

Build a Skeleton Loader

477 views

The problem

Question presented to candidate: "Our team page loads a list of profile cards from the server, and right now it shows a spinner and then everything jumps into place. Build a skeleton loader instead: grey placeholder shapes in the same layout as the real cards, with a shimmer, that are swapped for the real content when it arrives. Make the skeleton pieces reusable, and handle the request failing."

Requirements:

  • Reusable primitive Skeleton with variant (text, circle, rect), width and height; numbers mean pixels.
  • A ProfileCardSkeleton built from the primitives with exactly the same box sizes as the real ProfileCard, so nothing shifts when content arrives.
  • Loading state modelled explicitly: loading, success (with data) or error (with a message). Three skeleton cards while loading, real cards on success, an error with a Retry button on failure.
  • A Reload button (disabled while loading) shows the skeleton again; a "make the next request fail" checkbox demonstrates the error path. The data comes from a simulated API (a setTimeout promise), and only the latest request may update the screen.
  • Accessibility: the skeleton shapes are hidden from assistive technology; the list region has aria-busy while loading and a status message ("Loading team members"); errors use role="alert".
  • The shimmer is a CSS animation that is switched off for prefers-reduced-motion.

What a strong answer should cover:

  • Modelling loading as one state with explicit variants instead of several booleans.
  • Matching placeholder geometry to the real layout to avoid layout shift, and how to check it.
  • Composition: small primitives combined into content-specific skeletons.
  • What screen readers should hear instead of empty boxes (aria-hidden, aria-busy, a live status).
  • Ignoring stale responses (race conditions, React StrictMode double effects), and when to delay the skeleton to avoid a flash on fast responses.

Clarifying questions expected:

  • "How long do requests usually take? Should we avoid flashing a skeleton for very fast responses?"
  • "On reload, should old content stay visible while new data loads, or go back to the skeleton?"
  • "Do we know how many items to expect, so the skeleton count matches?"

Code / implementation expected: Yes. Primitives, a card skeleton, a page that fetches from a fake API with loading, success and error states.

  • React
  • Loading
  • UI

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

10 min read ยท 9 sections

React solution

Every file of the working solution. Open it in the playground to run and change it.

ReactComplete solution
import { useEffect, useState } from "react";
import { fetchProfiles } from "./src/api/api";
import { ProfileCard, ProfileCardSkeleton } from "./src/components/ProfileCard";

export default function App() {
  // One state object, three shapes: loading,
  // success (with data) or error (with a message).
  const [state, setState] = useState({ status: "loading" });
  const [failNext, setFailNext] = useState(false);
  // Each request carries its own options; a new object (re)runs the effect.
  const [request, setRequest] = useState({ id: 0, fail: false });

  useEffect(() => {
    // only the latest request may write state
    // (also absorbs the StrictMode re-run)
    let current = true;
    setState({ status: "loading" });
    fetchProfiles({ fail: request.fail })
      .then((profiles) => current && setState({ status: "success", profiles }))
      .catch(
        (err) => current && setState({ status: "error", message: err.message }),
      );
    return () => {
      current = false;
    };
  }, [request]);

  function reload() {
    setRequest((r) => ({ id: r.id + 1, fail: failNext }));
  }

  function retry() {
    setFailNext(false);
    setRequest((r) => ({ id: r.id + 1, fail: false }));
  }

  const loading = state.status === "loading";
  return (
    <main className="page">
      <h1>Team</h1>
      <div className="toolbar">
        <button type="button" onClick={reload} disabled={loading}>
          Reload
        </button>
        <label>
          <input
            type="checkbox"
            checked={failNext}
            onChange={(e) => setFailNext(e.target.checked)}
          />
          Make the next request fail
        </label>
      </div>
      <section aria-busy={loading} aria-label="Team members" className="list">
        {/* Screen readers hear this instead of a pile of empty grey boxes */}
        <p role="status" className="sr-only">
          {loading ? "Loading team members" : ""}
        </p>
        {loading && [0, 1, 2].map((i) => <ProfileCardSkeleton key={i} />)}
        {state.status === "success" &&
          state.profiles.map((p) => <ProfileCard key={p.id} profile={p} />)}
        {state.status === "error" && (
          <div className="error" role="alert">
            Could not load the team ({state.message}).{" "}
            <button type="button" onClick={retry}>
              Retry
            </button>
          </div>
        )}
      </section>
    </main>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 18 of 107 in Machine coding

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextEasy

Build a Stopwatch

Continue