jossmac / react-images

🌄 A mobile-friendly, highly customizable, carousel component for displaying media in ReactJS
http://jossmac.github.io/react-images
MIT License
2.35k stars 439 forks source link

When openning the <Modal />, it loads every files from <Carousel views={[...]} /> #300

Open Skylsmoi opened 5 years ago

Skylsmoi commented 5 years ago

On such implementation

<ModalGateway>
  {modalIsOpen ? (
    <Modal onClose={this.toggleModal}>
      <Carousel views={images} />
    </Modal>
  ) : null}
</ModalGateway>

When modalIsOpen is true, the modal opens and starts downloading every elements of images.

If there is a lot of elements, it slows down the browser a lot, make the animation feeze and cost a lot of memory, especially on mobile.

Is there a way to disable the animation and download only the visible image or maybe lazy load only the few next images ?

VolodymyrGoysan commented 5 years ago

Possible workaround is a custom View renderer. But this solution using much more traffic in case of reverse scrolling. E.g.

const ViewRenderer = (props) => {
  const overScanCount = 1;
  const {
    data, getStyles, index, currentIndex,
  } = props;
  const { alt, src } = data;

  return Math.abs(currentIndex - index) <= overScanCount ? (
    <div style={getStyles('view', props)}>
      <img
        alt={alt || `Image ${index}`}
        src={src}
        style={{
          height: 'auto',
          maxHeight: '100vh',
          maxWidth: '100%',
          userSelect: 'none',
        }}
      />
    </div>
  ) : null;
};

<Carousel
  views={images}
  components={{ View: ViewRenderer }}
/>
Skylsmoi commented 5 years ago

Mmh interesting solution.

I personally have tried to give only a subset of the image list to Carousel and update that subset on click on next and previous buttons.

It worked but updating the subset list on previous/next click removed the swipe left/right animations which wasn't great.

By the way, react-images uses react-view-pager as dependency which has a lazyLoad option tagged as "Comming soon". So we might have to wait for it to be implemented

dadamssg commented 4 years ago

I solved this in a similar way but wrapped the base View and set the source to render a transparent pixel instead of the actual img url.

import React from 'react'
import {carouselComponents} from 'react-images'

const BaseView = carouselComponents.View

// transparent pixel: http://png-pixel.com/
const pixel = {source: 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='}
const overscan = 3

export default function View (props) {
  const {currentIndex, index, data} = props
  const inBounds = Math.abs(currentIndex - index) <= overscan

  return <BaseView {...props} data={inBounds ? data : pixel} />
}

I did notice the animation is affected though.

ngbrown commented 4 years ago

Doing something with a delay helps the animations keep going:

  const overscan = 3;
  const animationTimeout = 1000; //ms

  const nextInBounds = Math.abs(currentIndex - index) <= overscan;
  const [inBounds, setInBounds] = useState(nextInBounds);
  useEffect(() => {
    if (currentIndex === index) {
      // got behind, so do it now
      setInBounds(true);
      return;
    }

    if (nextInBounds === inBounds) return;

    const timer = setTimeout(() => setInBounds(nextInBounds), animationTimeout);
    return () => {
      clearTimeout(timer);
    };
  }, [inBounds, nextInBounds, currentIndex, index]);

Edit: Even better is using an SVG placeholder, then no animations are affected by loading, and loading is not delayed.

import React from "react";
import { Div, Img } from "react-images/lib/primitives";
import { className } from "react-images/lib/utils";
import { getSource } from "react-images/lib/components/component-helpers";

function svgPlaceholder(width, height) {
  return `data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"%3E%3C/svg%3E`;
}

export function CustomView(props) {
  const {
    data,
    formatters,
    getStyles,
    index,
    currentIndex,
    isFullscreen,
    isModal
  } = props;
  const overscan = 3;
  const inBounds = Math.abs(currentIndex - index) <= overscan;

  const innerProps = {
    alt: formatters.getAltText({ data, index }),
    src: inBounds
      ? getSource({ data, isFullscreen })
      : svgPlaceholder(data.width, data.height)
  };

  return (
    <Div
      css={getStyles("view", props)}
      className={className("view", { isFullscreen, isModal })}
    >
      <Img
        {...innerProps}
        className={className("view-image", { isFullscreen, isModal })}
        css={{
          height: "auto",
          maxHeight: "100vh",
          maxWidth: "100vw",
          userSelect: "none"
        }}
      />
    </Div>
  );
}