Guides / Tutorials

Build a dog breed picker in React

By the Dog Image API team. Updated .

A breed picker needs two things: the list of breeds and one image per breed. Dog Image API gives you both without a key. Fetch the catalog once with fetch, filter it as the user types, and point an <img> at the preview URL for the selected breed.

What you will build

A text box that filters the catalog, a list of matching breeds, and a 512 pixel preview of the selected breed seen from the front three-quarter left. The preview is a transparent WebP, so it sits on any background color. The code uses Vite with React 18 or 19 and plain fetch. No other package is needed.

Create the project if you do not have one:

npm create vite@latest breed-picker -- --template react
cd breed-picker
npm install

How the data is shaped

GET /api/v1/breeds returns views and breeds. Each breed has a slug, a name, a default_variant and a list of variants (colors, patterns or coats). You only need slug and name for a picker.

For the image, build the URL from the slug. The word default stands for the breed's main set, and front-left is the view id:

https://dogimage.dev/api/v1/images/{slug}/default/front-left?size=512

That URL answers with a redirect to the WebP file, which an <img> follows without extra code. Pass size=1024 if you want the larger preview.

The component

Replace src/App.jsx with this file:

import { useEffect, useMemo, useState } from "react";

const API = "https://dogimage.dev";

function previewUrl(slug, view = "front-left", size = 512) {
  return `${API}/api/v1/images/${slug}/default/${view}?size=${size}`;
}

export default function App() {
  const [breeds, setBreeds] = useState([]);
  const [query, setQuery] = useState("");
  const [selected, setSelected] = useState(null);
  const [error, setError] = useState("");

  useEffect(() => {
    const controller = new AbortController();
    fetch(`${API}/api/v1/breeds`, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`Breed list failed with status ${res.status}`);
        return res.json();
      })
      .then((data) => setBreeds(data.breeds))
      .catch((err) => {
        if (err.name !== "AbortError") setError(err.message);
      });
    return () => controller.abort();
  }, []);

  const matches = useMemo(() => {
    const q = query.trim().toLowerCase();
    return q ? breeds.filter((b) => b.name.toLowerCase().includes(q)) : breeds;
  }, [breeds, query]);

  return (
    <main style={{ maxWidth: 720, margin: "2rem auto", padding: "0 1rem", fontFamily: "system-ui" }}>
      <h1>Pick a breed</h1>
      {error && <p role="alert">{error}</p>}

      <label htmlFor="breed-search">Search breeds</label>
      <input
        id="breed-search"
        type="search"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Try pug or retriever"
        style={{ display: "block", width: "100%", padding: "0.5rem", margin: "0.25rem 0 1rem" }}
      />

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1.5rem" }}>
        <ul style={{ listStyle: "none", margin: 0, padding: 0, maxHeight: 360, overflowY: "auto" }}>
          {matches.map((b) => (
            <li key={b.slug}>
              <button
                type="button"
                aria-pressed={selected?.slug === b.slug}
                onClick={() => setSelected(b)}
                style={{
                  width: "100%",
                  textAlign: "left",
                  padding: "0.4rem 0.6rem",
                  background: selected?.slug === b.slug ? "#e8eefc" : "transparent",
                  border: 0,
                  cursor: "pointer",
                }}
              >
                {b.name}
              </button>
            </li>
          ))}
          {breeds.length > 0 && matches.length === 0 && <li>No breed matches.</li>}
        </ul>

        <section aria-live="polite">
          {selected ? (
            <figure style={{ margin: 0 }}>
              <img
                src={previewUrl(selected.slug)}
                alt={`${selected.name}, AI-generated illustration, front three-quarter view`}
                width={256}
                height={256}
                style={{ background: "#f4f1ea", borderRadius: 8 }}
              />
              <figcaption>
                <a href={`${API}/dogs/${selected.slug}`}>
                  {selected.name} image: Dog Image API
                </a>
              </figcaption>
            </figure>
          ) : (
            <p>Select a breed to see it.</p>
          )}
        </section>
      </div>
    </main>
  );
}

Run npm run dev and open the local address. Type "pug" and the list narrows to one entry. Click it and the preview loads.

Details worth knowing

The credit link is part of the free tier. Previews are free for any use, commercial included, as long as a visible link credits Dog Image API next to the image or on the same page. The caption link above uses the form each breed page gives you. See the license for the exact terms.

The catalog is fetched once. The list is the same for every visitor, so one request on mount is enough. Do not hardcode the number of breeds anywhere in your code, because the live count changes as sets are added.

Cleanup matters. The AbortController stops the request if the component unmounts, which avoids a state update on a component that is gone. React 18 and later run effects twice in development strict mode, so this also keeps the dev console quiet.

The background is transparent. The preview has no backdrop, so the #f4f1ea fill in the sample is yours. Change it or remove it.

Try another view

The nine view ids are front, front-left, left, rear-left, rear, rear-right, right, front-right and head. Swap the second argument of previewUrl to change the angle. A head portrait works well in a small avatar slot, while left is the usual choice for a side profile card. The Left side gallery shows every breed from that angle.

Before you ship

Keep the key out of this app. The full-size PNG endpoint needs Authorization: Bearer <key> and is meant for a server or a build step, so a browser picker should stay on previews. If you also want print-quality files, the Next.js guide shows how to download them on the server into your own storage. Pricing for full-size plans is on the pricing page, and the endpoints are listed in the docs.

Say in your own interface that the dogs are AI-generated illustrations if your users might assume they are photos. The alt text in the sample does that for screen readers.

Questions

Do I need an API key for a React breed picker?

No. The breed list and the 512 pixel previews need no key, and the API allows cross-origin reads from browser apps. Keys are only for full-size PNG downloads, which belong on a server.

Can I hot-link the preview images in my React app?

Yes. Previews may be hot-linked, and they are served with a one-year immutable cache header. Show the credit link to Dog Image API next to the image or on the same page.

Are the dog images photos?

No. They are AI-generated illustrations of a dog on a transparent background. They show no real dog and are not a breed standard.

Next steps