Today, you’ll implement progressive enhancement to make your Progressive Web App (PWA) usable in low-data or limited connectivity scenarios. This technique improves the app’s usability by providing basic functionality even with low or unreliable data connections.
What You Will Do Today:
- Use lazy loading for large assets and images.
- Implement placeholder content for offline scenarios.
- Optimize API calls by caching API responses in IndexedDB.
Step 1: Using Lazy Loading for Large Assets and Images
Lazy loading delays loading of images or components until they are in view, which saves data and improves performance.
- In
Items.js
, update the component to load images only when they are visible.
import React, { useState, useEffect, Suspense, lazy } from 'react';
import { addItem, getAllItems, deleteItem } from './db';
// Lazy load Image component
const LazyImage = lazy(() => import('./LazyImage'));
function Items() {
const [items, setItems] = useState([]);
const [inputValue, setInputValue] = useState('');
useEffect(() => {
fetchItems();
}, []);
const fetchItems = async () => {
const storedItems = await getAllItems();
setItems(storedItems);
};
const handleAddItem = async () => {
if (inputValue.trim()) {
await addItem({ name: inputValue });
setInputValue('');
fetchItems();
}
};
const handleDeleteItem = async (id) => {
await deleteItem(id);
fetchItems();
};
return (
<div>
<h2>Items</h2>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add new item"
/>
<button onClick={handleAddItem}>Add Item</button>
<div className="items-grid">
{items.map((item) => (
<div key={item.id} className="item-card">
<Suspense fallback={<div>Loading...</div>}>
<LazyImage src={item.image} alt={item.name} />
</Suspense>
<p>{item.name}</p>
<button onClick={() => handleDeleteItem(item.id)}>Delete</button>
</div>
))}
</div>
</div>
);
}
export default Items;
- Create the
LazyImage.js
component for lazy loading images:
import React, { useState, useEffect } from 'react';
function LazyImage({ src, alt }) {
const [isLoaded, setIsLoaded] = useState(false);
const [hasError, setHasError] = useState(false);
useEffect(() => {
const img = new Image();
img.src = src;
img.onload = () => setIsLoaded(true);
img.onerror = () => setHasError(true);
}, [src]);
if (hasError) return <p>Image failed to load</p>;
return (
<img
src={isLoaded ? src : ''}
alt={alt}
style={{ visibility: isLoaded ? 'visible' : 'hidden' }}
/>
);
}
export default LazyImage;
Explanation of Code:
- Suspense: Wraps the
LazyImage
component, showing a fallback (Loading...
) until the image loads. - LazyImage: Preloads images and only shows them once fully loaded, with error handling for failed loads.
Step 2: Adding Placeholder Content for Offline Scenarios
Adding placeholders enhances the user experience when data is loading or unavailable due to low connectivity.
- Update
Items.js
to show placeholder items when loading:
function Items() {
const [items, setItems] = useState([]);
const [inputValue, setInputValue] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchItems();
}, []);
const fetchItems = async () => {
const storedItems = await getAllItems();
setItems(storedItems);
setLoading(false);
};
const placeholderItems = Array.from({ length: 5 }, (_, index) => (
<div key={index} className="item-card placeholder">
<div className="placeholder-content">Loading...</div>
</div>
));
return (
<div>
<h2>Items</h2>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add new item"
/>
<button onClick={() => handleAddItem()}>Add Item</button>
<div className="items-grid">
{loading ? placeholderItems : items.map((item) => (
<div key={item.id} className="item-card">
<p>{item.name}</p>
<button onClick={() => handleDeleteItem(item.id)}>Delete</button>
</div>
))}
</div>
</div>
);
}
- Add placeholder styles in
styles.css
:
.placeholder {
background-color: #e0e0e0;
}
.placeholder-content {
color: #888;
text-align: center;
}
Step 3: Caching API Responses in IndexedDB
Store API responses in IndexedDB so the app can retrieve data even with no network.
- In
db.js
, add a function to store and retrieve API data:
export async function cacheApiData(key, data) {
const db = await initDB();
const tx = db.transaction('api_cache', 'readwrite');
tx.store.put({ key, data });
await tx.done;
}
export async function getCachedApiData(key) {
const db = await initDB();
return db.get('api_cache', key);
}
- In
Items.js
, updatefetchItems
to use cached data if the API fails:
import { cacheApiData, getCachedApiData } from './db';
const fetchItems = async () => {
try {
const response = await fetch('/api/items');
const data = await response.json();
setItems(data);
await cacheApiData('items', data);
} catch (error) {
console.log('Failed to fetch from API, using cached data');
const cachedData = await getCachedApiData('items');
setItems(cachedData || []);
}
setLoading(false);
};
Summary
Today, you implemented progressive enhancement by:
- Lazy loading images to improve performance in low-data scenarios.
- Adding placeholders for offline or slow connections.
- Caching API responses in IndexedDB to retrieve data when the network is unavailable.
Tomorrow, you’ll learn how to convert an existing React app into a PWA.