Initialize / Eager Load React Component in Background on App Start for later usage

Viewed 1912

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>
  );
}
4 Answers

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 = image
});

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.

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

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;

Related