Winterest / Pinterest Grid + Infinite Scroll

윈터레스트 핵심 로직 및 기능

How does Pinterest implement its grid?

Inspiration

pinterest actual page

Here is how Pinterest makes the grid in their website. As a part of the cloning project, I thought it could be better to follow how they make the grid even though there were methods to use CSS.

Basic idea

Remove all images css of positions( position:absolute; left:0; top:0 ) and make grid only use the translateX and Y on css.

Winterest grid (aka Magic grid)

//./pages/Main.js 
<WinterestGrid url={API.main} query={query} />
//./components/WinterestGrid.js 
const WinterestGrid = ({ url, query }) => {
  const [page, setPage] = useState(0);
  const { pins, loading, error } = useScrollFetch(url, page, query);
  const gridLoader = useRef(null);
  const handleObserver = useCallback(entries => {
    const target = entries[0];
    if (target.isIntersecting) {
      setPage(prev => prev + 1);
    }
  }, []);

//Always refresh the grid loader
  useEffect(() => {
    const option = {
      root: null,
      rootMargin: '20px',
      threshold: 0,
    };
    const observer = new IntersectionObserver(handleObserver, option);
    if (gridLoader.current) observer.observe(gridLoader.current);
  }, [handleObserver]);

  return (
    <>
      <Grid>
        {pins && <GridPost pins={pins} />}
        {loading && <FetchInform message="그리드 로딩 중" />}
        {error && <FetchInform message="에러" />}
      </Grid>
      <LoaderBox windowHeight={document.body.scrollHeight} ref={gridLoader} />
    </>
  );
};

//LoaderBox must keep on the bottom of the window.innerHeight every time.
const LoaderBox = styled.div.attrs(props => ({
  windowHeight: `${
    props.windowHeight === 0 ? window.innerHeight : props.windowHeight
  }px`,
}))`
  position: absolute;
  top: ${props => props.windowHeight};
  width: 20px;
  height: 20px;
`;

Fetch

//./fetch/useScrollFetch.js 
//custom hook for infinite scroll fetch

//How much request the amount of image data from the API.
const LIMIT = 10;

//I added logic to input the random height number of the image_height. This value uses for the height and offsets y of posts on the Winterest Grid.
const giveRandomHeight = pins => {
  const result = pins.message.map(pin => {
    pin.image_height += randomHeight();
    return pin;
  });

  return result;
};

const randomHeight = () => {
  const heightList = [100, 300, 600, 1200];
  const newHeight = heightList[Math.floor(Math.random() * heightList.length)];

  return newHeight;
};

const useScrollFetch = (url, page, query) => {
  const [pins, setPins] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const location = useLocation;

//Empty "pins" whenever location.pathname is changed.
  useEffect(() => {
    setPins([]);
  }, [location.pathname]);

// Updating of page, url, and query refreshes the fetch to happen pagination
  useEffect(() => {
    getImages(page, url, query);
  }, [page, url, query]);


//get data from API with query parameter
  const getImages = (page, url, query) => {
    const pagenation = `?limit=${LIMIT}&offset=${LIMIT * page}`;
    const isQuery = query === undefined;

    fetch(url + pagenation + `${isQuery ? '' : query}`, {
      headers: { Authorization: API.token },
    })
      .then(res => res.json())
      .then(pinsData => {
        setPins(prevPins => {
          if (prevPins === []) {
            return giveRandomHeight(pinsData);
          } else {
            return [...prevPins, ...giveRandomHeight(pinsData)];
          }
        });
        setLoading(false);
      })
      .catch(error => {
        setError(error);
        setLoading(false);
      });
  };
  return { pins, loading, error };
};

Things to improve

Resource