Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Async/Await and the Magento API in React

Async/Await and Connecting to Magento

~13 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

Now let's finally connect ProductListPage and ProductDetailPage to a real API, instead of the hard-coded SAMPLE_PRODUCTS array. For that, you'll learn async/await – the more readable syntax for .then() chains.

async/await: the same as .then(), written differently

// With .then() chains:
fetch(url)
  .then((response) => response.json())
  .then((data) => console.log(data));

// With async/await – same logic, linearly readable:
async function loadData() {
  const response = await fetch(url);
  const data = await response.json();
  console.log(data);
}

await "pauses" the function at that point until the promise resolves – without blocking the rest of the app. await is only allowed inside a function marked async.

Achtung: Important before you continue: the code below needs a REAL, reachable API behind it – https://mironsoft.test is this project's local Docker address and won't work for you. Do you have your own Magento 2 shop with the REST API enabled? Then enter its address below. Don't have one (yet)? Then skip the Magento block and use the free alternative further down instead – the rest of this chapter stays completely identical either way, since only this one file knows where the data comes from.

Option A: your own Magento shop

Create a new folder src/api/ with the file magentoApi.js inside it:

src/api/magentoApi.js
const BASE_URL = 'https://mironsoft.test/rest/en/V1'; // ← enter your own shop URL here

function mapMagentoProduct(item) {
  const imageAttribute = item.custom_attributes?.find(
    (attribute) => attribute.attribute_code === 'image'
  );

  return {
    sku: item.sku,
    name: item.name,
    price: item.price,
    imageUrl: imageAttribute
      ? `https://mironsoft.test/media/catalog/product${imageAttribute.value}`
      : 'https://picsum.photos/300',
  };
}

export async function fetchProducts() {
  const response = await fetch(`${BASE_URL}/products?searchCriteria[pageSize]=20`);

  if (!response.ok) {
    throw new Error(`Magento API error: ${response.status}`);
  }

  const data = await response.json();
  return data.items.map(mapMagentoProduct);
}

export async function fetchProductBySku(sku) {
  const response = await fetch(`${BASE_URL}/products/${encodeURIComponent(sku)}`);

  if (!response.ok) {
    throw new Error(`Magento API error: ${response.status}`);
  }

  const item = await response.json();
  return mapMagentoProduct(item);
}

Option B: no shop of your own? A free test API, no signup

dummyjson.com is a free, public test API – no account, no API key needed. Create the same file like this instead (the exported function names are deliberately identical to option A):

src/api/magentoApi.js
const BASE_URL = 'https://dummyjson.com';

function mapMagentoProduct(item) {
  return {
    sku: String(item.id),
    name: item.title,
    price: item.price,
    imageUrl: item.thumbnail,
  };
}

export async function fetchProducts() {
  const response = await fetch(`${BASE_URL}/products?limit=20`);

  if (!response.ok) {
    throw new Error(`API error: ${response.status}`);
  }

  const data = await response.json();
  return data.products.map(mapMagentoProduct);
}

export async function fetchProductBySku(sku) {
  const response = await fetch(`${BASE_URL}/products/${encodeURIComponent(sku)}`);

  if (!response.ok) {
    throw new Error(`API error: ${response.status}`);
  }

  const item = await response.json();
  return mapMagentoProduct(item);
}

Both options export exactly the same two functions with the same return shape – the rest of the app only calls fetchProducts()/fetchProductBySku() and has no idea where the data actually comes from.

ProductListPage.jsx: real data instead of SAMPLE_PRODUCTS

src/pages/ProductListPage.jsx
import { useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import ProductCard from '../components/ProductCard';
import { fetchProducts } from '../api/magentoApi';

function ProductListPage() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [query, setQuery] = useState('');
  const searchInputRef = useRef(null);
  const navigate = useNavigate();

  useEffect(() => {
    async function load() {
      try {
        const items = await fetchProducts();
        setProducts(items);
      } catch (error) {
        console.error(error);
      } finally {
        setLoading(false);
      }
    }
    load();
  }, []);

  const filtered = products.filter((product) =>
    product.name.toLowerCase().includes(query.toLowerCase())
  );

  const mostExpensiveProduct = useMemo(() => {
    return filtered.reduce(
      (max, p) => (p.price > (max?.price ?? 0) ? p : max),
      null
    );
  }, [filtered]);

  if (loading) {
    return <p>Loading products...</p>;
  }

  return (
    <div>
      <input
        ref={searchInputRef}
        type="text"
        placeholder="Search products..."
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      {mostExpensiveProduct && (
        <p>Most expensive product: {mostExpensiveProduct.name} (${mostExpensiveProduct.price.toFixed(2)})</p>
      )}
      <div className="product-grid">
        {filtered.map((product) => (
          <ProductCard
            key={product.sku}
            name={product.name}
            price={product.price}
            imageUrl={product.imageUrl}
            onSelect={() => navigate(`/products/${product.sku}`)}
          />
        ))}
      </div>
    </div>
  );
}

export default ProductListPage;

Notice: searchInputRef.current.focus() from chapter 9 is deliberately missing here – as long as loading is true, the input field doesn't exist in the DOM yet at all. We'll take a closer look at cleanly combining loading states with other effects in the next chapter (error handling and loading states).

ProductDetailPage.jsx: loading a single product

src/pages/ProductDetailPage.jsx
import { useEffect, useState } from 'react';
import { Link, Outlet, useParams } from 'react-router-dom';
import { fetchProductBySku } from '../api/magentoApi';

function ProductDetailPage() {
  const { sku } = useParams();
  const [product, setProduct] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function load() {
      try {
        const data = await fetchProductBySku(sku);
        setProduct(data);
      } catch (error) {
        console.error(error);
      } finally {
        setLoading(false);
      }
    }
    load();
  }, [sku]);

  if (loading) {
    return <p>Loading product...</p>;
  }

  if (!product) {
    return <p>Product not found.</p>;
  }

  return (
    <div>
      <img src={product.imageUrl} alt={product.name} />
      <h2>{product.name}</h2>
      <p>${product.price.toFixed(2)}</p>
      <Link to={`/products/${sku}/reviews`}>See reviews</Link>

      <Outlet />
    </div>
  );
}

export default ProductDetailPage;

[sku] as a dependency makes it reload when navigating to a DIFFERENT product – just like in the React Native tutorial.

Project structure after this chapter (abbreviated)

produktkatalog-web/
└── src/
    ├── api/
    │   └── magentoApi.js            ← NEW
    └── pages/
        ├── ProductListPage.jsx       (updated: loads real data)
        └── ProductDetailPage.jsx     (updated: loads real data)