Skip to content

Compatibility Issue: react-leaflet Causes Image Rendering Failure in react-material-ui-carousel #239

@andrer0cha

Description

@andrer0cha

I'm building a SaaS with the following stack:

  • Ruby 3.4.1
  • Ruby on Rails 8.0.0
  • React 19.0.0
  • Vite 5.0.0
  • Typescript 5.3.2

In my application, I was using react-material-ui-carousel (version 3.4.2) on a page without any issues. However, after adding react-leaflet (version 5.0.0-rc.2), the carousel I had and another one I tried to add stopped working. When I remove react-leaflet and its dependencies, the carousel functions correctly again.

The issue I observe is that the first image appears in the carousel, but all the other images render as blank divs.

This problem is known, and I found it mentioned in a few places(i.e.: here), but none of the solutions work when I have leaflet (version 1.9.4) installed. Since leaflet works when having the react-material-ui-carousel installed, I'm creating the issue here, but I'm unsure where I should report it first.

// package.json
{
  "name": "marteleiro",
  "private": true,
  "type": "module",
  "license": "MIT",
  "dependencies": {
    "@emotion/react": "^11.14.0",
    "@emotion/styled": "^11.14.0",
    "@fontsource/roboto": "^5.1.1",
    "@fortawesome/fontawesome-free": "6.1.1",
    "@mui/icons-material": "^7.0.2",
    "@mui/material": "^6.4.6",
    "@mui/styles": "^6.4.8",
    "@mui/x-date-pickers": "^7.27.1",
    "@popperjs/core": "2.11.8",
    "@rails/webpacker": "5.4.4",
    "@react-oauth/google": "^0.12.1",
    "@stripe/stripe-js": "^5.7.0",
    "@types/leaflet": "^1.9.17",
    "@vitejs/plugin-react-swc": "^3.8.0",
    "axios": "^1.8.1",
    "bootstrap": "5.3.2",
    "dayjs": "^1.11.13",
    "jquery": "3.7.1",
    "js-cookie": "^3.0.5",
    "leaflet": "^1.9.4",
    "react": "^19.0.0",
    "react-currency-mask": "^1.3.3",
    "react-dom": "^19.0.0",
    "react-facebook": "^9.0.12",
    "react-hook-form": "^7.54.2",
    "react-leaflet": "^5.0.0-rc.2",
    "react-material-ui-carousel": "^3.4.2",
    "react-router-dom": "^7.2.0",
    "react-toastify": "^11.0.5",
    "toastr": "2.1.4"
  },
  "version": "0.1.0",
  "devDependencies": {
    "@eslint/js": "^8.52.0",
    "@types/react": "^19.0.10",
    "@types/react-dom": "^19.0.4",
    "@typescript-eslint/eslint-plugin": "^6.13.0",
    "@typescript-eslint/parser": "^6.13.0",
    "@vitejs/plugin-react": "^4.3.4",
    "eslint": "^8.52.0",
    "eslint-plugin-react": "^7.33.2",
    "eslint-plugin-react-hooks": "^4.6.0",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "prettier": "^3.0.3",
    "ts-jest": "^29.2.6",
    "typescript": "^5.3.2",
    "vite": "^5.0.0",
    "vite-plugin-ruby": "^5.1.0",
    "vite-tsconfig-paths": "^5.1.4",
    "webpack-dev-server": "^4.15.1"
  },
  "scripts": {
    "test": "jest",
    "lint": "eslint . --ext .ts,.tsx",
    "format": "prettier --write .",
    "format:check": "prettier --check .",
    "format:fix": "prettier --write .",
    "lint-format": "yarn format && yarn lint"
  }
}
// Component with both leaflet and react-material-ui-carousel
import React, { useEffect, useState } from 'react';
import { MapContainer, Marker, TileLayer, useMap } from 'react-leaflet'
import { useParams, useNavigate } from 'react-router-dom';
import {
  Box,
  Typography,
  Grid,
  Paper,
  Chip,
  Divider,
  Button,
  Card,
  CardContent,
  Table,
  TableBody,
  TableCell,
  TableContainer,
  TableRow,
  useTheme,
  useMediaQuery,
  IconButton,
  Tabs,
  Tab,
  List,
  ListItem,
  ListItemIcon,
  ListItemText,
  Tooltip,
} from '@mui/material';
import {
  Home,
  BedOutlined,
  ShowerOutlined,
  DirectionsCar,
  SquareFoot,
  Favorite,
  FavoriteBorder,
  CalendarToday,
  Gavel,
  Description,
  LocationOn,
  DocumentScanner,
  ArrowBack,
  Balance,
  LockPerson,
} from '@mui/icons-material';
import { formatCurrency } from '@utils/utils';
import { propertyService } from '@services/PropertyService';
import Loading from '@shared/Loading';
import Carousel from 'react-material-ui-carousel';
import '@styles/property_detail.scss';

// Interface para TabPanel
interface TabPanelProps {
  children?: React.ReactNode;
  index: number;
  value: number;
}
...


function a11yProps(index: number) {
  return {
    id: `property-tab-${index}`,
    'aria-controls': `property-tabpanel-${index}`,
  };
}

export default function PropertyDetail() {
  const { id } = useParams<{ id: string }>();
  const navigate = useNavigate();
  const theme = useTheme();
  const isMobile = useMediaQuery(theme.breakpoints.down('md'));
  const [property, setProperty] = useState<any>(null);
  const [loading, setLoading] = useState(true);
  const [isFavorite, setIsFavorite] = useState(false);
  const [tabValue, setTabValue] = useState(0);
  const [opportunityId, setOpportunityId] = useState<string | null>(null);
  const [currentImageIndex, setCurrentImageIndex] = useState(0);

  useEffect(() => {
    const fetchPropertyDetails = async () => {
      try {
        setLoading(true);
        const propertyData = await propertyService.getPropertyById(id as string);
        setProperty(propertyData);
        
        // // Verificar se já é favorito
        // const userOpportunities = await opportunityService.getUserOpportunities();
        // const existingOpportunity = userOpportunities.find(
        //   (opp) => opp.property.id === id
        // );
        
        // if (existingOpportunity) {
        //   setIsFavorite(true);
        //   setOpportunityId(existingOpportunity.id);
        // }
      } catch (error) {
        console.error('Erro ao buscar detalhes da propriedade:', error);
      } finally {
        setLoading(false);
      }
    };

    if (id) {
      fetchPropertyDetails();
    }
  }, [id]);

  const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
    setTabValue(newValue);
  };

  // const handleToggleFavorite = async () => {
  //   try {
  //     if (isFavorite && opportunityId) {
  //       // Remover dos favoritos
  //       await opportunityService.deleteOpportunity(opportunityId);
  //       setIsFavorite(false);
  //       setOpportunityId(null);
  //     } else {
  //       // Adicionar aos favoritos
  //       const response = await opportunityService.createOpportunity(id as string);
  //       setIsFavorite(true);
  //       setOpportunityId(response.id);
  //     }
  //   } catch (error) {
  //     console.error('Erro ao atualizar favoritos:', error);
  //   }
  // };

  const handleBack = () => {
    navigate(-1);
  };

      ...

      <Grid container spacing={3}>
        {/* Coluna da esquerda com imagens e informações principais */}
        <Grid item xs={12} md={8}>
          {/* Carrossel de imagens */}
          {property.images && !!property.images.length ? (
            <Paper elevation={0} sx={{ mb: 3, borderRadius: 2, overflow: 'hidden', position: 'relative' }}>
              <Carousel
                autoPlay={false}
                animation="slide"
                indicators={true}
                navButtonsAlwaysVisible={true}
                cycleNavigation={true}
                index={currentImageIndex}
                onChange={(index) => setCurrentImageIndex(index)}
                navButtonsProps={{
                  style: {
                    backgroundColor: 'rgba(0, 0, 0, 0.5)',
                    padding: '10px',
                    margin: '0 20px',
                    zIndex: 2,
                  }
                }}
                indicatorContainerProps={{
                  style: {
                    position: 'absolute',
                    bottom: '16px',
                    zIndex: 999999,
                  }
                }}
                className="property-carousel"
              >
                {property.images.map((img_link: string, index: number) => (
                  <Box 
                    key={index}
                    sx={{
                      height: isMobile ? '250px' : '400px',
                      width: '100%',
                      backgroundImage: `url(${img_link})`,
                      backgroundPosition: 'center',
                      backgroundSize: 'cover',
                    }}
                  >
                    {/* <img
                      src={image}
                      alt={`Imagem ${index + 1} do imóvel ${property.name}`}
                      style={{
                        width: '100%',
                        height: '100%',
                        objectFit: 'cover',
                      }}
                    /> */}
                  </Box>
                ))}
              </Carousel>
            </Paper>
          ) : (
            <Paper elevation={0} sx={{ mb: 3, borderRadius: 2, p: 5, textAlign: 'center', bgcolor: 'grey.100' }}>
              <Typography variant="body1" color="text.secondary">
                Nenhuma imagem disponível para este imóvel.
              </Typography>
            </Paper>
          )}

          {/* Miniaturas de imagens adicionais */}
          {property.images && property.images.length > 1 && (
            <Box sx={{ display: 'flex', gap: 1, mb: 3, overflowX: 'auto', pb: 1 }}>
              {property.images.map((img: string, index: number) => (
                <Box 
                  key={index}
                  component="img"
                  src={img}
                  alt={`Imagem ${index + 1}`}
                  sx={{ 
                    height: 80,
                    width: 120,
                    objectFit: 'cover',
                    borderRadius: 1,
                    cursor: 'pointer',
                    border: currentImageIndex === index ? '2px solid #1976d2' : 'none',
                    opacity: currentImageIndex === index ? 1 : 0.7,
                    transition: 'all 0.2s ease-in-out',
                    '&:hover': {
                      opacity: 1,
                      transform: 'scale(1.05)',
                    }
                  }}
                  onClick={() => setCurrentImageIndex(index)}
                />
              ))}
            </Box>
          )}

          {/* Informações principais */}


            ...


            <Grid container spacing={2}>
              {property.numBedrooms !== null && (
                <Grid item xs={6} sm={3}>
                  <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
                    <BedOutlined color="primary" />
                    <Typography variant="body2" color="text.secondary">Quartos</Typography>
                    <Typography variant="h6">{property.numBedrooms}</Typography>
                  </Box>
                </Grid>
              )}
              
              {property.numBathrooms !== null && (
                <Grid item xs={6} sm={3}>
                  <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
                    <ShowerOutlined color="primary" />
                    <Typography variant="body2" color="text.secondary">Banheiros</Typography>
                    <Typography variant="h6">{property.numBathrooms}</Typography>
                  </Box>
                </Grid>
              )}
              
              {property.numParkingSpots !== null && (
                <Grid item xs={6} sm={3}>
                  <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
                    <DirectionsCar color="primary" />
                    <Typography variant="body2" color="text.secondary">Vagas</Typography>
                    <Typography variant="h6">{property.numParkingSpots}</Typography>
                  </Box>
                </Grid>
              )}
              
              {property.area !== null && (
                <Grid item xs={6} sm={3}>
                  <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
                    <SquareFoot color="primary" />
                    <Typography variant="body2" color="text.secondary">Área</Typography>
                    <Typography variant="h6">{property.area} m²</Typography>
                  </Box>
                </Grid>
              )}
            </Grid>
          </Paper>

          {/* Tabs com detalhes, leilão, documentos, e mapa */}
          <Paper sx={{ borderRadius: 2 }}>
            <Tabs 
              value={tabValue} 
              onChange={handleTabChange} 
              variant={isMobile ? "scrollable" : "fullWidth"}
              scrollButtons={isMobile ? "auto" : undefined}
              sx={{ borderBottom: 1, borderColor: 'divider' }}
            >
              <Tab label="Detalhes" {...a11yProps(0)} />
              <Tab label="Leilão" {...a11yProps(1)} />
              <Tab label="Documentos" {...a11yProps(2)} />
              <Tab label="Processos" {...a11yProps(3)} />
            </Tabs>

            <TabPanel value={tabValue} index={0}>
              <Typography variant="h6" gutterBottom>Descrição</Typography>
              <Typography variant="body1" paragraph>
                {property.description || 'Descrição não disponível'}
              </Typography>
              
              <Typography variant="h6" gutterBottom>Características</Typography>


            ...


            <TabPanel value={tabValue} index={2}>
              <Typography variant="h6" gutterBottom>Documentos</Typography>
              
              {property.documents && property.documents.length > 0 ? (
                <List>
                  {property.documents.map((doc: any, index: number) => (
                    <ListItem 
                      key={index}
                      button
                      component="a"
                      href={doc.url}
                      target="_blank"
                    >
                      <ListItemIcon>
                        <DocumentScanner />
                      </ListItemIcon>
                      <ListItemText 
                        primary={doc.name || `Documento ${index + 1}`} 
                        secondary={doc.description} 
                      />
                    </ListItem>
                  ))}
                </List>
              ) : (
                <Typography variant="body1">
                  Nenhum documento disponível
                </Typography>
              )}
              
              {property.propertyRegistrationUrl && (
                <Button 
                  variant="outlined" 
                  startIcon={<DocumentScanner />}
                  href={property.propertyRegistrationUrl}
                  target="_blank"
                  sx={{ mt: 2, mr: 2 }}
                >
                  Matrícula do imóvel
                </Button>
              )}
              
              {property.propertyReportUrl && (
                <Button 
                  variant="outlined" 
                  startIcon={<Description />}
                  href={property.propertyReportUrl}
                  target="_blank"
                  sx={{ mt: 2 }}
                >
                  Laudo do imóvel
                </Button>
              )}
            </TabPanel>

          
          ...

          
          {(property.address?.latitude && property.address?.longitude) || (property.latitude && property.longitude) && (
            <Paper sx={{ p: 3, mb: 3, borderRadius: 2 }}>
              <Typography variant="h6" gutterBottom>
                Localização
              </Typography>
              <MapContainer center={[property.address?.latitude || property.latitude, property.address?.longitude || property.longitude]} zoom={13} scrollWheelZoom={false} style={{ height: "200px" }}>
                <TileLayer
                  url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
                />
                <Marker position={[property.address?.latitude || property.latitude, property.address?.longitude || property.longitude]}>
                  {/* <Popup>
                    A pretty CSS3 popup. <br /> Easily customizable.
                  </Popup> */}
                </Marker>
              </MapContainer>
            </Paper>
            )}
          
          {
            property.marketValue && (
              <Paper sx={{ p: 3, mb: 3, borderRadius: 2, bgcolor: 'grey.100' }}>
                <Typography variant="h6" gutterBottom color="text.secondary">
                  Valor de mercado estimado
                </Typography>
                
                <Typography variant="body2" color="text.secondary">
                  Esta funcionalidade estará disponível em breve. O valor será calculado com 
                  base na média de diversos portais imobiliários.
                </Typography>
              </Paper>
            )
          }
        </Grid>
      </Grid>
    </Box>
  );
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions