javascriptreactjsjquery-isotope

Why Isotope reloadItems() duplicates elements when deleting them?


I am currently using React, Firebase Real-Time Database, Material UI, and the Isotope library.

The issue I'm currently facing is related to adding a modal to confirm the deletion of a note. When I click "sure!" to confirm, the note is deleted correctly from DB, and Isotope reloads the items, but it exhibits a duplication effect in the UI. However, upon verification in my database, I confirmed that these duplicated values are not being stored.

Here is my code:

This is the list of notes

List.jsx

import '@/assets/styles/noteList.css'
import RenderNotes from '@/components/RenderNotes'
import MenuButton from '@/components/buttons/MenuButton'
import SearchButton from '@/components/buttons/SearchButton'
import { auth, db } from '@/services/firebase.config'
import { sortByCreatedAtDate } from '@/utils/helpers'
import AddIcon from '@mui/icons-material/Add'
import { Box, Fab, Stack } from '@mui/material'
import { ref } from 'firebase/database'
import { useEffect, useState } from 'react'
import { useAuthState } from 'react-firebase-hooks/auth'
import { useListVals } from 'react-firebase-hooks/database'
import { Link } from 'react-router-dom'
import { useDebounce } from 'use-debounce'

export default function NoteList () {
  const [notes, setNotes] = useState([])

  const [user] = useAuthState(auth)

  const [search, setSearch] = useState('')
  const [values] = useListVals(ref(db, `note-it-db/${user.uid}/notes`))

  const [debouncedSearch] = useDebounce(search, 300)

  useEffect(() => {
    const filterNotes = values.filter(
      note => note.content.toLowerCase().includes(debouncedSearch.toLowerCase())
    )

    setNotes(
      sortByCreatedAtDate(filterNotes)
    )
  }, [debouncedSearch, values])

  return (
    <Stack>
      <Box className='header'>
        <MenuButton />
        <SearchButton updateSearch={setSearch} />
      </Box>

      <Box className='render-notes-container'>
        <RenderNotes notes={notes} />
      </Box>

      <Fab
        aria-label='add'
        size='medium'
        component={Link}
        to='/notes'
        sx={{
          position: 'fixed',
          bottom: '2rem',
          right: '1rem',
          background: 'linear-gradient(145deg, #5123f8, #8d70f5, #b8a4fe)',
          boxShadow: '5px 5px 10px #3517a1, -5px -5px 10px #bdacfc',
          color: '#ffffff',
          ':hover': {
            background: '#b8a4fe',
            boxShadow: '0 0 5px white'
          },
          transition: 'none',
          zIndex: 1
        }}
      >
        <AddIcon />
      </Fab>
    </Stack>
  )
}

DeleteModal.jsx

import { auth, db } from '@/services/firebase.config'
import DeleteIcon from '@mui/icons-material/Delete'
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Slide } from '@mui/material'
import { ref, remove } from 'firebase/database'
import { forwardRef } from 'react'
import { useAuthState } from 'react-firebase-hooks/auth'

const Transition = forwardRef(function Transition (props, ref) {
  return <Slide direction='up' ref={ref} {...props} />
})

export default function DeleteModal ({ isModalOpen, handleOnClose, elementId }) {
  const [user] = useAuthState(auth)

  const handleDelete = () => {
    const noteRef = ref(db, `note-it-db/${user.uid}/notes/${elementId}`)
    remove(noteRef)
  }

  return (
    <Dialog
      aria-describedby='alert-dialog-slide-description'
      open={isModalOpen}
      TransitionComponent={Transition}
      keepMounted
      onClose={handleOnClose}
      sx={{
        '& .MuiPaper-root': {
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'space-between',
          alignItems: 'center',
          borderRadius: 'var(--card-border-radius)',
          backgroundColor: 'var(--color-secondary)',
          padding: '1rem'
        }
      }}
    >
      <DialogTitle>
        <DeleteIcon fontSize='large' color='error' />
      </DialogTitle>

      <DialogContent>
        <DialogContentText id='alert-dialog-slide-description' align='center'>
          Are you sure you want to delete this note?
        </DialogContentText>
      </DialogContent>

      <DialogActions className='delete-modal__buttons'>
        <Button
          variant='outlined'
          onClick={handleOnClose}
          sx={{
            color: 'var(--color-primary)',
            borderColor: 'var(--color-primary)',
            borderRadius: 'var(--card-border-radius)',
            textTransform: 'capitalize',
            fontWeight: 'bold',

            '&:hover': {
              borderColor: 'var(--color-primary)',
              backgroundColor: 'rgba(161, 141, 243, 0.15)'
            }
          }}
        >
          No, keep it
        </Button>
        <Button
          variant='contained'
          color='error'
          onClick={handleDelete}
          sx={{
            borderRadius: 'var(--card-border-radius)',
            textTransform: 'capitalize',
            fontWeight: 'bold'
          }}
        >
          Yes, I'm sure
        </Button>
      </DialogActions>
    </Dialog>
  )
}

RenderNotes.jsx

import NoNotesFoundIcon from '@/assets/icons/NoNotesFoundIcon'
import '@/assets/styles/noteList.css'
import Isotope from 'isotope-layout'
import 'isotope-packery'
import { useEffect, useRef } from 'react'
import { useSearchParams } from 'react-router-dom'
import NoteCard from './NoteCard'

const GridLayout = ({ notes }) => {
  const [searchParams] = useSearchParams()
  const filterBy = searchParams.get('filterBy')

  const gridRef = useRef(null)
  const isotopeRef = useRef(null)

  useEffect(() => {
    if (gridRef.current && notes) {
      if (!isotopeRef.current) {
        isotopeRef.current = new Isotope(gridRef.current, {
          layoutMode: 'packery',
          itemSelector: '.grid-item',
          packery: {
            gutter: 8
          },
          filter: filterBy !== null ? `.${filterBy}` : '*'
        })
      } else {
        isotopeRef.current.arrange({ filter: filterBy ? `.${filterBy}` : '*' })
      }
    }

    return () => {
      if (isotopeRef.current) {
        isotopeRef.current.reloadItems()
        isotopeRef.current.arrange({ filter: filterBy ? `.${filterBy}` : '*' })
      }
    }
  }, [filterBy, notes, gridRef, isotopeRef])

  return (
    <div ref={gridRef} className='grid'>
      {
        notes.map(note => (
          <div
            key={note.id}
            className={`grid-item ${note.favorite ? 'favorites' : ''}`}
          >
            <NoteCard note={note} />
          </div>
        ))
      }
    </div>
  )
}

export default function RenderNotes ({ notes }) {
  return notes && notes.length
    ? <GridLayout notes={notes} />
    : <NoNotesFoundIcon />
}

I'd appreciate your help, I'm stuck here more than a month, fortunately this is a personal project.

I already tried using all reloadItems() and remove() methods from the Isotope library.


Solution

  • Nevermind guys! I was looking for some help with ChatGPT and trying a lot of solutions, the issue was the <React.StrictMode> component, it was causing unnecessary re-renders.

    This is the explanation:

    I'm glad to hear that removing <React.StrictMode> fixed the issue! React Strict Mode is a development mode feature that helps catch common mistakes and provides improved behavior for certain functions. It performs additional checks and warnings to help you write better React code.

    However, there are cases where these additional checks might trigger warnings or affect the behavior of certain libraries, causing unexpected issues. This is especially true for third-party libraries or code that relies on specific behaviors that are modified in Strict Mode.

    In your case, it seems that Isotope or other parts of your code might be interacting with React in a way that triggers warnings or conflicts with the additional checks performed by Strict Mode.

    When you remove Strict Mode, those additional checks and warnings are no longer applied, and your application might behave differently. While Strict Mode is valuable for catching issues during development, in some cases, it might be necessary to temporarily disable it or find a workaround for specific scenarios where it interferes with the expected behavior.

    If you need to keep using Strict Mode for development, you can consider leaving it enabled and addressing the specific warnings or issues it points out. However, if removing Strict Mode doesn't negatively impact your development workflow, it's acceptable to keep it disabled.

    Keep in mind that Strict Mode is primarily a development tool, and its absence won't affect the production build of your application. It's mainly meant to catch potential problems early in the development process.