Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Avatar] Fallback images when fails to load #18711

Merged
merged 3 commits into from
Dec 7, 2019

Conversation

netochaves
Copy link
Contributor

@netochaves netochaves commented Dec 7, 2019

Closes #16161

Before
before

After
after

@mui-pr-bot
Copy link

mui-pr-bot commented Dec 7, 2019

@material-ui/core: parsed: +0.18% , gzip: +0.32%

Details of bundle changes.

Comparing: e8703bf...fa995f0

bundle Size Change Size Gzip Change Gzip
Avatar ▲ +2.48 kB (+4.02% ) 64.1 kB ▲ +817 B (+4.23% ) 20.1 kB
@material-ui/core ▲ +650 B (+0.18% ) 355 kB ▲ +310 B (+0.32% ) 97.1 kB
@material-ui/core[umd] ▲ +618 B (+0.20% ) 312 kB ▲ +362 B (+0.40% ) 90 kB
ButtonBase ▲ +1 B (0.00% ) 72.8 kB ▲ +1 B (0.00% ) 22.8 kB
SvgIcon ▼ -1 B (-0.00% ) 61.9 kB -- 19.3 kB
TextField ▼ -1 B (-0.00% ) 122 kB -- 35.5 kB
Snackbar -- 76 kB ▲ +13 B (+0.05% ) 23.7 kB
@material-ui/lab -- 174 kB ▲ +8 B (+0.02% ) 52.3 kB
CardActions -- 60.9 kB ▼ -8 B (-0.04% ) 19 kB
GridListTileBar -- 62.1 kB ▲ +6 B (+0.03% ) 19.4 kB
Autocomplete -- 128 kB ▲ +4 B (+0.01% ) 39.8 kB
Checkbox -- 80.6 kB ▼ -3 B (-0.01% ) 25.3 kB
ExpansionPanelSummary -- 76.9 kB ▼ -3 B (-0.01% ) 24.2 kB
SwipeableDrawer -- 90.6 kB ▼ -3 B (-0.01% ) 28 kB
Switch -- 80 kB ▼ -3 B (-0.01% ) 25 kB
TablePagination -- 140 kB ▲ +3 B (+0.01% ) 40.7 kB
TableSortLabel -- 76.2 kB ▲ +3 B (+0.01% ) 23.9 kB
Button -- 78.3 kB ▲ +2 B (+0.01% ) 23.9 kB
CardHeader -- 64 kB ▲ +2 B (+0.01% ) 20 kB
Chip -- 81.4 kB ▲ +2 B (+0.01% ) 24.8 kB
DialogContentText -- 62.9 kB ▲ +2 B (+0.01% ) 19.7 kB
Divider -- 61.5 kB ▲ +2 B (+0.01% ) 19.2 kB
ExpansionPanelDetails -- 60.8 kB ▲ +2 B (+0.01% ) 19 kB
Fab -- 75.6 kB ▲ +2 B (+0.01% ) 23.5 kB
FormControlLabel -- 64.4 kB ▲ +2 B (+0.01% ) 20.1 kB
TableHead -- 61 kB ▲ +2 B (+0.01% ) 19 kB
Badge -- 64.2 kB ▲ +1 B (+0.01% ) 19.9 kB
BottomNavigation -- 61.3 kB ▼ -1 B (-0.01% ) 19.1 kB
Card -- 61.6 kB ▼ -1 B (-0.01% ) 19.2 kB
CardContent -- 60.9 kB ▼ -1 B (-0.01% ) 19 kB
CircularProgress -- 63 kB ▲ +1 B (+0.01% ) 19.8 kB
Container -- 62 kB ▼ -1 B (-0.01% ) 19.3 kB
Dialog -- 81.5 kB ▲ +1 B (0.00% ) 25.4 kB
DialogTitle -- 63.2 kB ▲ +1 B (+0.01% ) 19.7 kB
Grid -- 64 kB ▼ -1 B (-0.01% ) 20 kB
GridListTile -- 62.6 kB ▲ +1 B (+0.01% ) 19.5 kB
Icon -- 61.7 kB ▼ -1 B (-0.01% ) 19.2 kB
IconButton -- 75 kB ▲ +1 B (0.00% ) 23.3 kB
InputAdornment -- 64 kB ▲ +1 B (0.00% ) 20 kB
ListItem -- 75.9 kB ▼ -1 B (-0.00% ) 23.6 kB
ListItemText -- 63.8 kB ▲ +1 B (+0.01% ) 19.9 kB
MenuItem -- 77 kB ▲ +1 B (0.00% ) 23.9 kB
MobileStepper -- 66.6 kB ▲ +1 B (0.00% ) 20.8 kB
NoSsr -- 2.19 kB ▼ -1 B (-0.10% ) 1.03 kB
Radio -- 81.5 kB ▲ +1 B (0.00% ) 25.6 kB
RadioGroup -- 62.1 kB ▼ -1 B (-0.01% ) 19.4 kB
SpeedDialAction -- 116 kB ▼ -1 B (-0.00% ) 36.5 kB
Step -- 61.5 kB ▼ -1 B (-0.01% ) 19.2 kB
StepButton -- 81.1 kB ▼ -1 B (-0.00% ) 25.5 kB
StepContent -- 67.9 kB ▲ +1 B (0.00% ) 21.2 kB
Tab -- 75.2 kB ▲ +1 B (0.00% ) 23.7 kB
TableRow -- 61.4 kB ▼ -1 B (-0.01% ) 19.1 kB
@material-ui/styles -- 51 kB -- 15.3 kB
@material-ui/system -- 14.8 kB -- 4.06 kB
AppBar -- 62.7 kB -- 19.5 kB
Backdrop -- 66.6 kB -- 20.5 kB
BottomNavigationAction -- 74.3 kB -- 23.4 kB
Box -- 69.6 kB -- 21 kB
Breadcrumbs -- 66.9 kB -- 20.8 kB
ButtonGroup -- 80.9 kB -- 24.8 kB
CardActionArea -- 73.9 kB -- 23.2 kB
CardMedia -- 61.2 kB -- 19.2 kB
ClickAwayListener -- 3.87 kB -- 1.56 kB
Collapse -- 66.8 kB -- 20.6 kB
colorManipulator -- 3.85 kB -- 1.52 kB
CssBaseline -- 56.4 kB -- 17.6 kB
DialogActions -- 61 kB -- 19 kB
DialogContent -- 61.1 kB -- 19.1 kB
docs.landing -- 52.4 kB -- 11.4 kB
docs.main -- 609 kB -- 194 kB
Drawer -- 83.2 kB -- 25.2 kB
ExpansionPanel -- 70.1 kB -- 21.8 kB
ExpansionPanelActions -- 61 kB -- 19 kB
Fade -- 22.4 kB -- 7.71 kB
FilledInput -- 72.3 kB -- 22.4 kB
FormControl -- 63.3 kB -- 19.6 kB
FormGroup -- 60.9 kB -- 19 kB
FormHelperText -- 62.1 kB -- 19.4 kB
FormLabel -- 62.4 kB -- 19.2 kB
GridList -- 61.4 kB -- 19.2 kB
Grow -- 23.1 kB -- 7.83 kB
Hidden -- 64.8 kB -- 20.2 kB
Input -- 71.3 kB -- 22.1 kB
InputBase -- 69.4 kB -- 21.7 kB
InputLabel -- 64.2 kB -- 20 kB
LinearProgress -- 64.2 kB -- 20 kB
Link -- 65.5 kB -- 20.6 kB
List -- 61.3 kB -- 19 kB
ListItemAvatar -- 61 kB -- 19 kB
ListItemIcon -- 61.1 kB -- 19 kB
ListItemSecondaryAction -- 60.9 kB -- 19 kB
ListSubheader -- 61.6 kB -- 19.3 kB
Menu -- 87.1 kB -- 26.8 kB
MenuList -- 64.9 kB -- 20.2 kB
Modal -- 14.3 kB -- 5 kB
NativeSelect -- 75.6 kB -- 23.7 kB
OutlinedInput -- 72.8 kB -- 22.6 kB
Paper -- 61.1 kB -- 19 kB
Popover -- 81.5 kB -- 25.1 kB
Popper -- 28.7 kB -- 10.2 kB
Portal -- 2.9 kB -- 1.3 kB
Rating -- 68.9 kB -- 22.1 kB
RootRef -- 4.43 kB -- 1.67 kB
Select -- 113 kB -- 33.5 kB
Skeleton -- 61.4 kB -- 19.3 kB
Slide -- 24.5 kB -- 8.32 kB
Slider -- 74.5 kB -- 23.3 kB
SnackbarContent -- 64.5 kB -- 20.2 kB
SpeedDial -- 84.9 kB -- 26.7 kB
SpeedDialIcon -- 63.5 kB -- 19.9 kB
StepConnector -- 61.6 kB -- 19.3 kB
StepIcon -- 63.5 kB -- 19.7 kB
StepLabel -- 67.5 kB -- 21.1 kB
Stepper -- 63.6 kB -- 20 kB
styles/createMuiTheme -- 15.6 kB -- 5.47 kB
Table -- 61.4 kB -- 19.2 kB
TableBody -- 61 kB -- 19 kB
TableCell -- 62.9 kB -- 19.7 kB
TableFooter -- 61 kB -- 19 kB
Tabs -- 84.3 kB -- 26.5 kB
TextareaAutosize -- 5.06 kB -- 2.11 kB
ToggleButton -- 75 kB -- 23.7 kB
ToggleButtonGroup -- 62.1 kB -- 19.4 kB
Toolbar -- 61.2 kB -- 19.1 kB
Tooltip -- 99.7 kB -- 31.5 kB
TreeItem -- 72.5 kB -- 22.8 kB
TreeView -- 65.3 kB -- 20.4 kB
Typography -- 62.5 kB -- 19.5 kB
useAutocomplete -- 12.6 kB -- 4.63 kB
useMediaQuery -- 2.47 kB -- 1.04 kB
Zoom -- 22.5 kB -- 7.71 kB

Generated by 🚫 dangerJS against fa995f0

@oliviertassinari oliviertassinari added component: avatar This is the name of the generic UI component, not the React module! new feature New feature or request labels Dec 7, 2019
@oliviertassinari
Copy link
Member

@netochaves Thank for looking at it. I have added a new demo:

Capture d’écran 2019-12-07 à 13 12 23

@acroyear
Copy link
Contributor

acroyear commented Dec 7, 2019

oh nice. i was doing that (use the letter) explicitly in my code and thus having to watch for the image load error externally.

@andokai
Copy link
Contributor

andokai commented Dec 11, 2019

Hi guys, while I appreciate that this might be useful in this narrow albit popular use case for avatars it's a visual breaking change. I used the avatar component to build a swatch colour picker which should have no content when a colour isn't selected and a tick when they are.

Now I have person icons in every unselected colour as well as the selected swatch.

image

Shouldn't this be opt in so that people can either choose the fallback image or not have one at all?

@oliviertassinari
Copy link
Member

@andokai We haven't anticipated this possibility. Why are you using the Avatar for this use case? Would it be better to render a div with a border radius of 50%?

@andokai
Copy link
Contributor

andokai commented Dec 13, 2019

I was using the avatar because in addition to the border radius it centres the check icon in the circle. Of course I can do this all manually using a div but the same could be said for every component.

I also don't believe that the function is described correctly. It doesn't fall back when the image fails to load, it now defaults to the person icon unless you supply it with children or it successfully loads the image. It also claims to fall back to a generic avatar icon but it's only generic if the only thing you ever list is people.

@jeffy-g
Copy link

jeffy-g commented Dec 13, 2019

The background-color is now set in this change but it may cause problems when the background of the avatar image is transparent

@oliviertassinari
Copy link
Member

oliviertassinari commented Dec 13, 2019

For context, the Avatar component is optimized for displaying a representation of a person.

@andokai What if we scope the display of the fallback icon when the image has failed to load and that we have no other information to fallback to? What about the following diff?

diff --git a/packages/material-ui/src/Avatar/Avatar.js b/packages/material-ui/src/Avatar/Avatar.js
index 9505a20f6..f2147a917 100644
--- a/packages/material-ui/src/Avatar/Avatar.js
+++ b/packages/material-ui/src/Avatar/Avatar.js
@@ -62,12 +62,12 @@ function useLoaded({ src, srcSet }) {
   const [loaded, setLoaded] = React.useState(false);

   React.useEffect(() => {
+    setLoaded(false);
+
     if (!src && !srcSet) {
       return undefined;
     }

-    setLoaded(false);
-
     let active = true;
     const image = new Image();
     image.src = src;
@@ -130,7 +130,7 @@ const Avatar = React.forwardRef(function Avatar(props, ref) {
     children = childrenProp;
   } else if (hasImg && alt) {
     children = alt[0];
-  } else {
+  } else if (loaded === 'error') {
     children = <Person className={classes.fallback} />;
   }

@Jeff-G Interesting, we might want to add an opt-in prop to remove the skeleton background color. But maybe a style override is as simple?

@jeffy-g
Copy link

jeffy-g commented Dec 13, 2019

For context, the Avatar component is optimized for displaying a representation of a person.

Yes, that is indeed.

However, in my created component, an avatar image uses an image with a transparent background as an exception, and this change resulted in an unintended style.

As a countermeasure, I wrote a class that applies background-color: unset

@andokai
Copy link
Contributor

andokai commented Dec 13, 2019

For context, the Avatar component is optimized for displaying a representation of a person.

If that's true then perhaps you want a different component for list items? One of the list examples uses avatars for folders.

https://material-ui.com/components/lists/#folder-list

@andokai What if we scope the display of the fallback icon when the image has failed to load and that we have no other information to fallback to? What about the following diff?

This solves the particular problem I have but falling back to a person icon still seems like it's only satisfying one particular use case. One of the application have built is essentially an asset management product so one of the lists is people so this implementation could help but there's also lists of vehicles and locations and so on. In all of those circumstances I will need to setup a fallback icon when no image exists. So do I use this behaviour where I introduce inconsistent implementation depending on they type of item shown in the list or do I just generalise my approach and provide a fallback avatar regardless of the item type?

@oliviertassinari
Copy link
Member

oliviertassinari commented Dec 13, 2019

@andokai By optimized, I meant, as the primary use case it's used for.
In practice, when rendering an image, an alt will likely be used, the first letter of the alt is used as a fallback, before the person icon. If you don't want to see the letter fallback, you can provide a custom icon as a children. I don't think that images fail to load frequently.

@acroyear
Copy link
Contributor

It isn't necessarily that images fail to load.

For my app, I'm not in charge of the server API, and an avatar image is not a requirement (as in, there's not a default image that is sent if the user didn't configure one for themselves). Thus, in my case it is actually likely that there won't be one, because not everybody bothers to do that; I'll get the 404 from the server and an image onload error to handle. I intentionally leave one of my accounts without one in order to test my workaround of rendering the initial when the image isn't there.

As of yet, I have not attempted to update my code to use this new feature - I will be doing a major rewriting to be more Hooks based and Function components (and remove the alt flux store i've been using), likely coordinating to when MUI 5.x comes out, so I'll address it then.

@EloB
Copy link
Contributor

EloB commented Feb 20, 2020

An improvement to this is could be to use .decode() if available. From what I know loaded event only tells if the image is downloaded and decode promise tell if the image is ready (downloaded and decoded) for the DOM.

https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/decode
https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/decoding

Adding images to DOM previously was synchronous and the decoding (jpeg, png) of the image was actually blocking.

I ran across this PR because I was searching for smooth transition between image loads. I would be nice if there was a transition between images or using spinner or something. Everything in this library looks awesome except this thing :D

// I normally have a helper function like this.
const asyncEvent = (element, eventName, options) => {
  let cancelled;
  let teardown;
  const promise = new Promise((resolve) => {
    if (cancelled) return;
    const handle = (...args) => resolve(args);
    teardown = () => element.removeEventListener(eventName, handle);
    element.addEventListener(eventName, handle, { ...options, once: true });
  });
  promise.cancel = () => {
    if (cancelled) return;
    if (teardown) teardown();
    cancelled = true;
  };
  return promise;
};

// I use to write something like this in the useEffect
let cancelled;
let cancelablePromises;

(async () => {
  try {
    if (image.decode) {
      await image.decode();
    } else if (!image.complete) {
      cancelablePromises = [asyncEvent(image, 'load'), asyncEvent(image, 'error')];
      const [event] = await Promise.race(cancelablePromises);
      if (event.type !== 'load') throw new Error('Failed to load image');
    }
    if (!cancelled) setLoaded(true);
  } catch (e) {
    if (!cancelled) setError('Failed to load image');
  }
})();

// The teardown
return () => {
  cancelled = true;
  if (cancelablePromises) cancelablePromises.forEach(({ cancel: fn }) => fn());
};

This is how I normally write this. This code isn't tested.

@oliviertassinari
Copy link
Member

@EloB Interesting! I think that we need to render the image with the src server side. I have tried the following but it doesn't seem better:

diff --git a/packages/material-ui/src/Avatar/Avatar.js b/packages/material-ui/src/Avatar/Avatar.js
index ff28b170e..1c7bf0cc6 100644
--- a/packages/material-ui/src/Avatar/Avatar.js
+++ b/packages/material-ui/src/Avatar/Avatar.js
@@ -56,6 +56,16 @@ export const styles = theme => ({
   },
 });

+let firstRender = true;
+
+function useFirstRender() {
+  React.useEffect(() => {
+    firstRender = false;
+  }, []);
+
+  return firstRender;
+}
+
 function useLoaded({ src, srcSet }) {
   const [loaded, setLoaded] = React.useState(false);

@@ -71,10 +81,12 @@ function useLoaded({ src, srcSet }) {
     image.src = src;
     image.srcSet = srcSet;
     image.onload = () => {
-      if (!active) {
-        return;
-      }
-      setLoaded('loaded');
+      image.decode().then(() => {
+        if (!active) {
+          return;
+        }
+        setLoaded('loaded');
+      })
     };
     image.onerror = () => {
       if (!active) {
@@ -110,8 +122,12 @@ const Avatar = React.forwardRef(function Avatar(props, ref) {

   // Use a hook instead of onError on the img element to support server-side rendering.
   const loaded = useLoaded({ src, srcSet });
+  const isFirstRender = useFirstRender();
   const hasImg = src || srcSet;
-  const hasImgNotFailing = hasImg && loaded !== 'error';
+  const hasImgNotFailing = isFirstRender ? hasImg && loaded !== 'error' : loaded === 'loaded';

   if (hasImgNotFailing) {
     children = (

@EloB
Copy link
Contributor

EloB commented Feb 20, 2020

@oliviertassinari I think I can live with the initial render being off but after that it somehow makes these beautiful animation you guys done :) For instance that ripple effect on button is amazing :)

I've rewrote the code in the previous message to handle error.

Using that decode would not block as I understand it correctly.

Maybe use that "Fade" component could be handy here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: avatar This is the name of the generic UI component, not the React module! new feature New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Avatar] Improve image loading and fallback logic
7 participants