bsidelinger912 / shiitake

React Line clamp that won't get you fired
MIT License
100 stars 22 forks source link

Shiitake doesn't play well with lazy/Suspense #36

Open zioth opened 4 years ago

zioth commented 4 years ago

If you lazy-load a component including a Shiitake, the content sometimes doesn't render. This isn't your fault; it's due to a design flaw in React that the developers have said they aren't going to fix. Here's why it happens:

  1. The lazy-loaded component is downloaded, and the virtual DOM is constructed, with display: none on the topmost element. While this is happening, the Suspense fallback is being displayed.
  2. The DOM is rendered, and all JS is executed.
  3. Depending on timing, this is when Shiitake measures itself to figure out how much vertical space is being used. Since the parent has display: none, the measured height is zero.
  4. React hides the Suspense fallback, and removes display: none. This action is not linked to the React lifecycle, and doesn't fire any sort of event, so there's no way to detect that this is happening.
  5. If the user doesn't trigger a Shiitake rerender by resizing the window or something, the content is rendered inside a height: 0 element, which makes it invisible to the user.

In my project, I worked around the issue by creating a wrapper component around Shiitake which goes up the DOM tree looking for display: none, and polling for that style's removal. It's very hacky, but it works.

If you fix this, you'll have the only popular line-clamping component which can safely be lazy-loaded. I checked the others. :) If you like, I can ask my boss whether my workaround can be open sourced so you can use it as a dependency.

bsidelinger912 commented 4 years ago

Hey @zioth this is interesting I haven't really dug into suspense yet, however I have a couple ideas on how to fix it using a combination of tricks to detect whether the element is visible (https://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom) and possible a MutationObserver in order to avoid polling. If you wanted to help, you could give me a repro by forking this pen: https://codepen.io/bsidelinger912/pen/zBgwmd. Otherwise I'll take a look when I can, it seems pretty straight forward.

zioth commented 4 years ago

I looked into MutationObserver, but you'd have to observe every parent node, and possibly refresh observers based on changes to the ReactDOM. Fortunately, my polling algorithm never ran more than once. If you find a reliable and efficient way to do it without any polling, I'd be very interested.

bsidelinger912 commented 4 years ago

@zioth I can't seem to reproduce this. I'm lazy loading with suspense and set my network to mimic slow 3G with dev tools, that allows me to see the fallback component consistently for a decent amount of time but shiitake renders correctly for me every time, and I tried a bunch of time because it seems intermittent. Any tips on reproducing?

zioth commented 4 years ago

I never tried to make the minimum reproducible case. We use Shiitake for two UIs. One failed intermittently (and very rarely), and one failed every time. In both cases, I'm loading the page from my local machine, so network speed probably isn't a factor. The one big difference I see on the consistent failure page is that the JS chunk is pre-loaded (using javascript_pack_tag -- this is a Rails app).

Another difference is that in the intermittent failure, the component is very small (Shiitake is only a few levels deep in the ReactDOM), while in the consistent failure, it's much larger.

zioth commented 4 years ago

I just tried to find the minimum reproducible case. It looks something like this:

// Located in lazy chunk #1
const MyComponent = () =>
  <React.Fragment>
    <Shiitake lines={2}>Hello2</Shiitake>
    <OtherComponent/>
  </React.Fragment>;

// Located in lazy chunk #2
const OtherComponent = () => '';

Shiitake consistently renders with a height of 0px.

My other failure case is intermittent, so I can't reduce it to a reproducible case, but both cases seem to be fixed by polling offsetParent to wait for Shiitake to be visible.