0

I am working on a react file-upload component. I got stuck with a rather trivial issue – I want for each file to show icon corresponding to a file extension. Icons are loaded via css as background images (using inline styles). The problem arises when I don't have an icon for given extension and thus want to show a fallback icon.

– I tried to use multiple css background-image declarations like this:

style={{
  backgroundImage: `url(./icons/fallback.svg), url(./icons/${item.extension}.svg)`,
}}

or like this:

style={{
  backgroundImage: `url(./icons/fallback.svg)`,
  backgroundImage: `url(./icons/${item.extension}.svg)`,
}}

But it doesn't work, the fallback icon is not being used (or in one case I am not able to reproduce, both icon are shown, which is also undesired).

I tried to fetch the file to determine if it does exist, but the node server (i use create-react-app) is configured in a way that returns 200 or 304 even if the file isn't actually present.

I tried to use a solution which creates an image and uses onload and onerror events as beeng suggested in this question, which actually works fine – i am currently using slightly modified implementation of image-exists npm module – but I wasn't able to figure out how to refactor this function to simply return a boolean. Using console.log() and callbacks works fine; returning a boolean results in undefined. I suppose it is due to an asynchronous behaviour od Image methods, but I wasn't able to create a workaround – maybe using a Promise API?

My code:

exists = src => {
  const checks = {};
  return callback => {
    if (src in checks) {
      return callback(checks[src]);
    }
    let img = new Image();

    img.onload = function() {
      checks[src] = true;
      callback(true);
    };

    img.onerror = function() {
      checks[src] = false;
      callback(false);
    };

    img.src = src;
  };
};

Render method:

render() {
  // So far so good, logs as expected, but not such useful      
  console.log(this.exists('./icons/jpg.svg')(bool => {
    if(bool) {
      console.log('yes') 
    } else {
      console.log('no');
    }
  }));
// ...
}

If I try to return a boolean directly, it results in undefined:

render() {     
  console.log(this.exists('./icons/jpg.svg')(bool => bool));
  // ...
}
HynekS
  • 2,738
  • 1
  • 19
  • 34

2 Answers2

1

You are right, the function does not return a boolean because this is the parameter of the callback of your exists function, which is called asynchronously. The solution is to render your icon asynchronously too, something like...

this.exists(img)(bool => {
  if (bool) {
    render(img) 
  } else {
    render('fallback.svg');
  }
}
PA.
  • 28,486
  • 9
  • 71
  • 95
0

O.K. I finally promisify the whole thing. I hooked the former exists function (now checkImage) to a promise chain(saw… massacre…) which is triggered by reading files to upload and results in setState and rerender:

The url checking function:

checkImage = (path, fallback) => {
  return new Promise(resolve => {
    const img = new Image();
    img.src = path;
    img.onload = () => resolve(path);
    img.onerror = () => resolve(fallback);
  });
};

Calling with Promise.all():

// items are array of objects which contains file contents, name, extension etc...
checkIcons = items =>
Promise.all(
  items.map(item => {
    const url = `./icons/${item.extension}.svg`;
    return this.checkImage(url, this.state.fallbackIconUrl).then(result => {
      return { ...item, icon: result };
    });
  })
);

Definitely not the slickiest one in town and it would possibly need some caching (or may not – it does seem the browser can handle this by itself), but works fine.

HynekS
  • 2,738
  • 1
  • 19
  • 34