How do I use effects and refs to set an automatic scroll behavoir when a user selects a product?

17 Views Asked by At

I'm working in Next using tailwind CSS. The goal is to setup a shop page that behaves like an instagram feed where the user clicks a product photo in the grid and the page renders all the same content on FeaturedProduct cards that take up the entire viewport and can be scrolled through to view the entire catalogue in the same order it is displayed in the grid view defined in renderProductList.

This component is almost perfect. To finish it I need for the selected product to be scrolled into view when the layout changes in response to the user clicking a product.

import React, { useContext, useState, useEffect, useRef } from 'react';
import FeaturedProduct from '@/components/featured-product';
import { ProductsContext } from '@/context/products-context';

const Home = () => {
  const [selectedProduct, setSelectedProduct] = useState(null);
  const products = useContext(ProductsContext);

  const containerRef = useRef(null);
  const productRefs = useRef([]);

  const handleProductClick = (product) => {
    setSelectedProduct(product);
  };

  useEffect(() => {
    console.log('useEffect called');

    if (selectedProduct && productRefs.current[selectedProduct.id]) {
      const productElement = productRefs.current[selectedProduct.id];
      const containerElement = containerRef.current;
      if (productElement && containerElement) {
        const containerRect = containerElement.getBoundingClientRect();
        const productRect = productElement.getBoundingClientRect();
        const offset = productRect.top - containerRect.top - (containerRect.height - productRect.height) / 2;

        containerElement.scrollBy({
          top: offset,
          behavior: 'auto',
        });
      }
    }
  }, [selectedProduct]);

  const renderProductList = () => {
    return (
      <div className='grid grid-cols-3 gap-4' ref={containerRef}>
        {products.map((product, index) => (
          <div 
          key={product.id} 
          ref={(el) => (productRefs.current[product.id] = el)}
          onClick={() => handleProductClick(product)}
          >
            <img
            src={product.imageUrl}
            alt={product.title} 
            className='w-full h-auto cursor-pointer'
            />
          </div>
        ))}
      </div>
    );
  };

  const renderFullScreenProduct = () => {
    if (!selectedProduct) return null;

    const selectedProductIndex = products.findIndex((product) => product.id === selectedProduct.id);

    const allProducts = products.map((product, index) => (
    <div key={product.id} ref={(el) => index === selectedProductIndex && el}>
      <FeaturedProduct { ...product } key={product.id} />
    </div>
  ));

    return (
      <div className='fixed top-0 left-0 w-full h-full overflow-y-auto bg-white'>
        <div className='max-w-screen-md mx-auto p-4' >
          {allProducts}
          <button
            className='absolute top-4 right-4 text-gray-600'
            onClick={() => setSelectedProduct(null)}
          >
            Close
          </button>
        </div>
      </div>
    );
  };

  return (
    <div className='mx-4'>
      {selectedProduct ? renderFullScreenProduct() : renderProductList()}
    </div>
  );
};

export default Home;


This is the best I've got. I don't have enough experience using useRef and useEffect to make this work out in my favor. I'd like to reiterate that this component is behaving almost exactly as I need it to. It would seem to be a very easy thing to do to set an automatic scroll behavior and I'm sure the solution is much more elegant than this absolute mess I've come up with.

0

There are 0 best solutions below