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:
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):
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
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
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)