Setting the results of RTK query with local state in useEffect gives Maximum update depth exceeded warning

277 views Asked by At

I am trying to set the results of RTK query to the local state so that I can change the list based on user actions. See my full code below: but the relevant code is these two statements:

const { data: notifications = [], isLoading, isError } = useGetUserNotificationsQuery();
  const [userNotificationRead] = useUserNotificationReadMutation();
  const [notificationList, setNotificationList] = useState([]);

and

 useEffect(() => {
    setNotificationList(notifications);
  }, [notifications]);

warning I am seeing is this: Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render. at NotificationList

and here is the full code

**import React, { useState, useEffect } from 'react';
import {
  Avatar,
  Paper,
  Typography,
  IconButton,
  Menu,
  MenuItem,
  CircularProgress,
  Box,
} from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import VolumeOffIcon from '@mui/icons-material/VolumeOff';
import NotificationsOffIcon from '@mui/icons-material/NotificationsOff';
import DeleteIcon from '@mui/icons-material/Delete';
import { Link, useNavigate } from 'react-router-dom';

import { useGetUserNotificationsQuery  } from '../../redux/thunks/users/usernotifications';
import { useUserNotificationReadMutation  } from '../../redux/thunks/users/usernotificationsmarkread'

const NotificationList = () => {
  const [anchorEl, setAnchorEl] = useState(null);
  const [selectedNotification, setSelectedNotification] = useState(null);
  

  const { data: notifications = [], isLoading, isError } = useGetUserNotificationsQuery();
  const [userNotificationRead] = useUserNotificationReadMutation();
  const [notificationList, setNotificationList] = useState([]);

  useEffect(() => {
    setNotificationList(notifications);
  }, [notifications]);

  const handleMenuOpen = (event, notification) => {
    setAnchorEl(event.currentTarget);
    setSelectedNotification(notification);
  };

  const handleMenuClose = () => {
    setAnchorEl(null);
    setSelectedNotification(null);
  };

  const handleDeleteNotification = () => {
    // Implement your logic to delete the selected notification here
    console.log(`Deleted notification: ${selectedNotification.notice}`);
    const updatedNotifications = notificationList.filter(
        (notification) => notification._id !== selectedNotification._id
      );
      setNotificationList(updatedNotifications);
      handleMenuClose();
  };

  const handleTurnOffNotifications = () => {
    // Implement your logic to turn off notifications of this type here
    console.log(`Turned off notifications from ${selectedNotification.senderDisplayName}`);
    handleMenuClose();
  };

  const handleMuteNotifications = () => {
    // Implement your logic to mute notifications from this sender here
    console.log(`Muted notifications from ${selectedNotification.senderDisplayName}`);
    handleMenuClose();
  };

  if (isLoading ) {
    // Show a loading spinner if either user data or activities data is loading
    return (
      <Box display="flex" justifyContent="center" alignItems="center" minHeight="200px">
        <CircularProgress color="primary" />
      </Box>

    )
  }
  console.log('in notifications: notificationList', notificationList)
  console.log('in notifications: notifications', notifications)

  if(!notificationList.length) {
    return (
        <Paper variant="outlined" justifyContent="center"  sx={{
            
            alignItems: 'center',
            justifyContent: 'space-between',
            padding: '8px',
            marginBottom: '8px',
          }}>
            {/* Content for if notification is empty*/}
            <Typography variant="h6" gutterBottom justifyContent="center" style={{ cursor: 'pointer' }}>
                No notifications in your list
            </Typography>
            <Link
              to={'/mynetwork'}
              style={{ textDecoration: 'none', color: 'inherit' }}
            >
              <Typography variant="subtitle2" color="textSecondary">
                Build your network
              </Typography>
            </Link>
            
        </Paper>

    )
  }

  return (
    <div>
      {notificationList.map((notification) => (
        <Paper
          key={notification._id}
          variant="outlined"
          sx={{
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'space-between',
            padding: '8px',
            marginBottom: '8px',
          }}
        >
          {/* Left Column: User Photo */}
          <Avatar
            src={notification.senderPhoto}
            alt={notification.senderDisplayName}
          />

          {/* Middle Column: Notification */}
          <Typography sx={{ flex: 1 }}>{notification.notice}</Typography>

          {/* Right Column: Options */}
          <IconButton
            aria-label="Options"
            aria-controls="notification-menu"
            aria-haspopup="true"
            onClick={(e) => handleMenuOpen(e, notification)}
          >
            <MoreVertIcon />
          </IconButton>
        </Paper>
      ))}

      {/* Options Menu */}
      <Menu
        id="notification-menu"
        anchorEl={anchorEl}
        open={Boolean(anchorEl)}
        onClose={handleMenuClose}
      >
        <MenuItem onClick={handleDeleteNotification}>
          <DeleteIcon /> Delete
        </MenuItem>
        <MenuItem onClick={handleTurnOffNotifications}>
          <NotificationsOffIcon /> Turn Off Notifications
        </MenuItem>
        <MenuItem onClick={handleMuteNotifications}>
          <VolumeOffIcon /> Mute Notifications from {selectedNotification?.senderDisplayName}
        </MenuItem>
      </Menu>
    </div>
  );
};

export default NotificationList;**
1

There are 1 answers

1
Drew Reese On BEST ANSWER

notifications appears to be an array. The issue here is that it seems the array is redeclared each time the component renders even though the contents may not have changed. You really only want to update the local notificationList state when the notifications array content changes. Tricks like using the array length also don't work in all cases, e.g. when the content changes but the arrays are the same length. An "acceptable hack" is to JSON stringify the array as a dependency.

Example:

const {
  data: notifications = [],
  isLoading,
  isError
} = useGetUserNotificationsQuery();
const [notificationList, setNotificationList] = useState([]);

useEffect(() => {
  setNotificationList(notifications);
}, [JSON.stringify(notifications)]);

You might need to disable any react-hooks linting rules for this line if it complains about missing dependencies.

An alternative might be to cache the "previous" version of notifications in a React ref and check if the array contents have changed. Again, JSON stringifying the array is a "quick hack", otherwise you could implement a true "deep equals" utility function for arrays.

const {
  data: notifications = [],
  isLoading,
  isError
} = useGetUserNotificationsQuery();
const [notificationList, setNotificationList] = useState([]);
const notificationsRef = React.useRef();

useEffect(() => {
  const notificationsString = JSON.stringify(notifications);

  if (notificationsRef.current !== notificationsString) {
    // Update the notifications list state
    setNotificationList(notifications);

    // Cache the stringified notifications array
    notificationsRef.current = notificationsString;
  }
}, [notifications]);