carbon-design-system / ibm-products

A Carbon-powered React component library for IBM Products
https://carbon-for-ibm-products.netlify.app/
Apache License 2.0
87 stars 120 forks source link

fix(EmptyState): illustrations should render correctly now #332

Closed matthewgallo closed 3 years ago

matthewgallo commented 3 years ago

Contributes to #324

This PR fixes the asset loading issue in the EmptyState component because of how we were loading the illustration. Thanks @davidmenendez for suggestion some solutions to this issue. I tried webpack's require.context however, it did not seem to be working within storybook so I went with the approach of importing the assets from a separate file.

UPDATE (03/02/21): The <EmptyState /> component is now broken up into several components so we avoid the issue of importing every illustration until we can get dynamic imports working.

Changelog

New packages/experimental/src/components/EmptyState/EmptyStateIllustrations.js

Changed packages/experimental/src/components/EmptyState/EmptyState.js packages/experimental/src/components/EmptyState/EmptyState.stories.js

netlify[bot] commented 3 years ago

Deploy preview for ibm-cloud-cognitive ready!

Built with commit 549e273408dfb6a0219a6eea050c5126ff4192c2

https://deploy-preview-332--ibm-cloud-cognitive.netlify.app

areddon commented 3 years ago

This will result in all SVGs to be loaded by the consumer without the possibility of tree shaking. This is something that Carbon has already solved with the use of Icons and I am sure that there is a pattern to be reused.

davidmenendez commented 3 years ago

This will result in all SVGs to be loaded by the consumer without the possibility of tree shaking. This is something that Carbon has already solved with the use of Icons and I am sure that there is a pattern to be reused.

if that's the case i'll go ahead i'll go ahead and remove my approval. i'm glad to hear this has been figured out already. i don't know enough about storybook to have had any better solutions in mind. @matthewgallo you should reach out to the carbon team to get their input on this and either update this PR or open up a new one. @areddon thank you for the heads up 👍

areddon commented 3 years ago

Carbon does this by having the package icons-react export a React component for each icon SVG in their icons package. Then various React components in their react package such as Button accept a renderIcon prop to draw the SVG and allow for proper tree shaking.

SimonFinney commented 3 years ago

Carbon does this by having the package icons-react export a React component for each icon SVG in their icons package. Then various React components in their react package such as Button accept a renderIcon prop to draw the SVG and allow for proper tree shaking.

@areddon Good point - How does this work for components like this where the illustrations are internal and predetermined based on consumer selection? Wouldn't this be unavoidable unless they're conditionally imported, or is the nature of an SVG-as-a-module what plays nice with tree-shaking?

/cc @joshblack Would you have some additional context here?

areddon commented 3 years ago

I think that the point of these components as building blocks is to be as lean as possible and push those decisions down to the consumer. We can decide whether this library exports a React component for each SVG or that is done by a separate package for all of them. Probably easier to maintain if it is a separate library like icons-react as it allows other developers to just drop an SVG in there. Since the code for the EmptyState is rather straight forward, we could have it constructed so that it could be consumed like the example below.

import { error } from 'pal-icons-react/dark'

<EmptyState renderIcon={error} />

In most cases, the illustration to use will be known ahead of time and if not, then it is left up to the consumer regarding how they want handle conditional cases. They would either load multiple and use conditional rendering or use dynamic imports, but the end result is that we give them the choice.

I see that we also have SVGs for both light and dark, is it not possible for us to instead have a single SVG that is modified via styles? Otherwise at the very least the consumer will have to load both or using dynamic imports for the correct one.

davidmenendez commented 3 years ago

I'm personally not a fan of this because I think it's antithetical to what we're trying to do. We're trying to build out these components with specific use cases in mind to make it easier for the consumer. It should be as easy as <EmptyState illustration="notfound" /> instead of having the user have to import all the illustrations and set the props themselves.

I'm ok with accepting that a better solution for this just hasn't come up yet. I just wanted to express my concern with this. We can't really compare this with carbon when we're building specific flows and specific use cases so that the user doesn't have to.

SimonFinney commented 3 years ago

I'm personally not a fan of this because I think it's antithetical to what we're trying to do. We're trying to build out these components with specific use cases in mind to make it easier for the consumer. It should be as easy as <EmptyState illustration="notfound" /> instead of having the user have to import all the illustrations and set the props themselves.

I'm ok with accepting that a better solution for this just hasn't come up yet. I just wanted to express my concern with this. We can't really compare this with carbon when we're building specific flows and specific use cases so that the user doesn't have to.

I agree, and I wonder what the middle-ground is here between enforcing illustration consistency and being conscious of bundle size. The renderIcon approach leaves it vulnerable to abuse, but that could happen anyway given the component already supports custom illustrations.

davidmenendez commented 3 years ago

@matthewgallo can you explain why require.context wasn't working? seems like it might be a solution, but i understand storybook might be causing issues https://devdesigner.xyz/dynamic-image-import-with-create-react-app/read

matthewgallo commented 3 years ago

Sure @davidmenendez, when I tried using webpack's require.context I kept getting the following error, require.context is not a function seems related to the following issue. I attempted to change some of our babel settings but wasn't able to get it working.

areddon commented 3 years ago

I don't think that the solution I proposed is rather difficult for consumers to use and they would be familiar with it as it is the same for Carbon and Icons. It can be taken a step further such that we export a component per illustration. <NotFoundEmptyState />, <EmptyStateNotFound /> or <NotFound />

An implementation as is in this PR becomes a breaking change in the future and will only result in bundle size increases as more SVGs get added. (Not just for empty states but other usage of SVGs)

davidmenendez commented 3 years ago

@matthewgallo that's strange because i'm not seeing that locally 🤔

here's a refactor i did that's working

  const requestImageFile = require.context('./assets', true, /.svg$/);
  const defaultIllustrationOptions = [
    'nodata',
    'error',
    'unauthorized',
    'notags',
    'notfound',
    'notifications',
  ];
  const src = typeof illustration === 'string' && defaultIllustrationOptions.includes(illustration)
    ? requestImageFile(`./${illustrationTheme}/${illustration}.svg`)
    : illustration;
  const classes = cx([
    `${pkgPrefix}-empty-state-illustration`,
    `${pkgPrefix}-empty-state-illustration--${illustrationSize}`,
  ]);
...
<img src={src} alt="Empty state illustration" className={classes} />
matthewgallo commented 3 years ago

Right, I see your point @areddon. But I also agree with @SimonFinney and @davidmenendez in that these component designs have strict guidance as to which illustrations should be used and also that these illustrations aren't meant to be used as part of an icon library. Maybe we should just look into getting require.context working, then we can just be able to use the dynamic import.

areddon commented 3 years ago

Neither implementation has an impact on their usage, only how they are bundled and consumed. We also don't have to move the SVGs to a separate package if we were to create a component for each illustration.

matthewgallo commented 3 years ago

@davidmenendez that was able to work for me too. Maybe the issue was that I was using require.context within the return of this component. I remove the extra file that imported all of the illustrations and now this uses a dynamic import.

joshblack commented 3 years ago

@SimonFinney I think what was explained totally nails it, in the past we ran into problems with the <Icon name="icon-name"> pattern and tree-shaking. In that case, the dynamic lookup from a module leads to the whole thing being included. This resulted in the component-per-icon strategy and the renderIcon prop (which tends to be called icon in other systems, it seems).

Those types of props are definitely open-ended, and if there was ever a situation to limit them to only a specific set we might try and approach it through either prop type validation, making components-per-variant, or adding a variant prop to specify which icon to use.

davidmenendez commented 3 years ago

@joshblack @areddon I think you guys have a better understanding of tree shaking and webpack optimization than me. thoughts on using require.context in this scenario?

joshblack commented 3 years ago

@davidmenendez I think require.context can definitely work if the project knows that downstream users can leverage it correctly (like with webpack picking up .svg files). On our we tend to avoid bundler-specific conventions but this definitely can cause headaches for us :joy:

Separately, not sure how helpful this is but I did make a Terser sandbox for testing code snippets for tree shaking. It helped out a ton for stuff like: https://github.com/carbon-design-system/carbon/issues/5442 when we ran into weird tree shaking problems. Wasn't sure if it'd help out a ton for this conversation but figured I'd share just in case!

SimonFinney commented 3 years ago

@SimonFinney I think what was explained totally nails it, in the past we ran into problems with the <Icon name="icon-name"> pattern and tree-shaking. In that case, the dynamic lookup from a module leads to the whole thing being included. This resulted in the component-per-icon strategy and the renderIcon prop (which tends to be called icon in other systems, it seems).

Those types of props are definitely open-ended, and if there was ever a situation to limit them to only a specific set we might try and approach it through either prop type validation, making components-per-variant, or adding a variant prop to specify which icon to use.

The variant prop sounds closest to the current implementation, but that's still packaging up 12 SVGs (6 if theming can be delegated within the SVG itself) which is not quite on par with Carbon's iconography assets but based on the discussion still a cause for concern, especially if the number grows.

Maybe prop type validation is enough to deter folks from abusing the open-ended nature of the prop, and as an added bonus, we wouldn't have to worry about locking people into webpack features with require.context.

matthewgallo commented 3 years ago

Thank you everyone for all of the feedback here! For now, I went with @areddon's suggestion of having these empty state illustrations be React components with each component taking a theme prop which will in turn render the correct theme illustration. I'm completely open to any additional feedback on this.