3

I'm writing a React App, where I have a Fallback component, which gets displayed when something goes wrong, for example: network is down, API isn't reachable, unknown route, etc.

This component will fetch some URLs of cat pictures and displays a slide show.

But of course this isn't possible when the network is down.

Though I'd like to somehow create and initialize this component in the background when the App starts, so everything is ready in the case of emergency.

Additional info: The Fallback component will be used as child component of different views. So it's not possible to simply mount it in App.jsx and use CSS visibility: hidden / visible to hide and display it.

Is this possible and does someone know how to do it?

EDIT: Example code

const Fallback = () => {
  // will contain urls like:
  //   - https://cats.example.org/images/foo.jpg
  //   - https://cats.example.org/images/bar.png
  //   - https://cats.example.org/images/42.gif
  const [urls, setUrls] = useState([]);

  useEffect(() => {
    fetch('https://catpictures.example.org')
      .then(response => response.json())
      .then(data => setUrls(data));
  }, []);

  // this should be cached somehow:
  return (
    <div>
      {urls.map(url =>
        <img src={url} />
      }
    </div>
  );
}
Benjamin M
  • 23,599
  • 32
  • 121
  • 201
  • My first guess: Create a [redux store](https://redux.js.org/api/store), after the App has loaded, start receiving those images. Save the image as raw **base64** in the store. Then, when internet fails, and the `` is rendered, it can load the images from the store. – 0stone0 Mar 23 '21 at 22:50
  • Does `Fallback` component do an API request to get the image srcs? Are you trying to preload the images or the component code? If you had a small example of the `Fallback` that would be great – Cameron Downer Mar 23 '21 at 22:51
  • @CameronDowner There's no example code, because I don't have a clue how to do this at all. I can give you a trivial piece of JSX like this: `urls.map(url => )`. That's the problematic part. Retrieving the `urls` at application start and storing them in react `Context` is simple. I'll add some code to the question. – Benjamin M Mar 24 '21 at 05:01

4 Answers4

1

You can do this and I've done it in big production apps by simply creating a new Image() and setting the src. The image will be preloaded when the component is first rendered.

const LoadingComponent() {
  useEffect(() => {
    const img = new Image();
    img.src = imgUrl;
  }, []);

  return null; // doesn't matter if you display the image or not, the image has been preloaded
}

It could even become a hook such as useImagePreloader(src) but that's up to you.

Here is a Sandbox with a working version.

Steps to try it:

  1. Create an incognito window, open devtools and check the network tab, search for "imgur". The image is loaded.
  2. Set the network offline or disconnect from your WIFI.
  3. Click on the Show Image button. The image will display correctly.

This solution will always work provided your cache settings for images are set correctly (usually they are). If not, you can save the images to blobs and get a URL to that blob, that will work 100% of times.

As you noted that you need an array of images, you can do that same code inside a loop and it will work just fine, images will still be cached:

const images = ['first.jpg', 'second.png', 'etc.gif'];

images.forEach(imageUrl => {
  const img = new Image();
  img.src = imageUrl
});
Pablo Recalde
  • 3,334
  • 1
  • 22
  • 47
Yuan-Hao Chiang
  • 2,484
  • 9
  • 20
  • That looks promising! Could you provide an example using `blob`? Then I don't have to care about the server cache settings. And then I (hopefully) can know for sure what images haves been preloaded successfully – Benjamin M Mar 24 '21 at 06:54
0

You can manually add a resource to the cache by using a preloaded <link>:

<link rel="preload" as="image" href="https://cats.example.org/images/foo.jpg">

Put this inside your index.html and use it from cache when needed by using the same href.

Mordechai
  • 15,437
  • 2
  • 41
  • 82
  • I can't put it inside `index.html` because the image urls aren't known at build time. Maybe it's possible to inject the `` tags with JavaScript. But looks quite hacky to me – Benjamin M Mar 24 '21 at 05:09
  • `react-helmet` should probably help here – Mordechai Mar 24 '21 at 19:16
0

How about a service worker that would cache your assets and then serve them when offline? Then you could send a message with the URL to change to notify your "app" it is back online with some new content.

Thre is a working example here: https://serviceworke.rs/strategy-cache-update-and-refresh_demo.html

var CACHE = 'cache-update-and-refresh';
 
self.addEventListener('install', function(evt) {
  console.log('The service worker is being installed.');
 
  evt.waitUntil(caches.open(CACHE).then(function (cache) {
    cache.addAll([
      './controlled.html',
      './asset'
    ]);
  }));
});

function fromCache(request) {
  return caches.open(CACHE).then(function (cache) {
    return cache.match(request);
  });
}

function update(request) {
  return caches.open(CACHE).then(function (cache) {
    return fetch(request).then(function (response) {
      return cache.put(request, response.clone()).then(function () {
        return response;
      });
    });
  });
}

function refresh(response) {
  return self.clients.matchAll().then(function (clients) {
    clients.forEach(function (client) {

 
      var message = {
        type: 'refresh',
        url: response.url,

        eTag: response.headers.get('ETag')
      };
 
      client.postMessage(JSON.stringify(message));
    });
  });
}

self.addEventListener('fetch', function(evt) {
  console.log('The service worker is serving the asset.');

  evt.respondWith(fromCache(evt.request));

  evt.waitUntil(
    update(evt.request)
 
    .then(refresh)
  );
});
Fabien Greard
  • 1,854
  • 1
  • 16
  • 26
0

Local Storage

  1. On page/App load;

  2. On network fail

    • Render <FallBack />
  3. <FallBack />

    • Read localStorage
    • Render base64 images

Small example

  • We use fetch to get the cat images in the <App/> component

  • Save those to the localStorage

    (NOTE: StackSnippet doesn't allow localStorage, so please test it on JSFiddle)

  • We use a useState to 'fake' the network status

// Init
const { useState } = React;

// <Fallback />
const Fallback = () => {

    // Get all localstorage items
    let ls = { ...localStorage };

    // Get all starting with 'image_'
    ls = Object.keys(ls).filter(key => key.startsWith('image_'));
 
    // Render images
    return (
        <div>
            <p>{'FallBack'}</p>
            {
                (ls.length < 1)
                    ? 'Unable to find cached images!'
                    : (
                        ls.map((key) => {
                        
                            // Get current key from localstorage
                            const base64 = localStorage.getItem(key);
                            
                            
                            
                            // Render image    
                            return <img src={base64} />;
                        })
                    )
            }
        </div>
    );
}

// <App />
const App = ({title}) => {
    const [network, setNetwork] = useState(true);
    const [urls, setUrls] = useState([ 'https://placekitten.com/200/300', 'https://placekitten.com/200/300']);

    // Render Fallback on lost network
    if (!network) {
        return <Fallback />;
    }

    // While network is active, get the images
    urls.forEach((url, index) => {
        
        fetch(url)
            .then(response => response.blob())
            .then(blob => {

                // Convert BLOB to base64
                var reader = new FileReader();
                reader.readAsDataURL(blob); 
                reader.onloadend = function() {

                    // Write base64 to localstorage   
                    var base64data = reader.result;     
                    localStorage.setItem('image_' + index, base64data);
                    console.log('Saving image ' + index + ' to localstorage');
                };
            });
    })

    return (
        <div>
            <p>{'App'}</p>
            <p>Press me to turn of the internet</p>
            <button onClick={() => setNetwork(false)}>{'Click me'}</button>
        </div>
    );
};

// Render <App />
ReactDOM.render(<App />, document.getElementById("root"));

JSFiddle Demo


Pros;

  • LocalStorage will not be cleared, if the same app is loaded the next day, we don't need to get those images again

Cons;

0stone0
  • 34,288
  • 4
  • 39
  • 64