Day 8: Progressive Enhancement – Making the App Usable in Low-Data Scenarios


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:

  1. Use lazy loading for large assets and images.
  2. Implement placeholder content for offline scenarios.
  3. 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.

  1. 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;
  1. 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.
See also  Day 10: Deploying the PWA to Netlify, Heroku, or AWS and Making It Installable

Step 2: Adding Placeholder Content for Offline Scenarios

Adding placeholders enhances the user experience when data is loading or unavailable due to low connectivity.

  1. 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>
     );
   }
  1. 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.

  1. 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);
   }
  1. In Items.js, update fetchItems 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.
See also  Day 7: Testing and Optimizing for Different Screen Sizes (Responsive Design)

Tomorrow, you’ll learn how to convert an existing React app into a PWA.


Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.