infinite re-rending when using custom hook to display notification with notistack

88 Views Asked by At
import { useEffect, useCallback } from 'react';
import { useIntl } from 'react-intl';
import { useDispatch, useSelector } from 'react-redux';
import { useSnackbar } from 'notistack';
import { removeSnackbar } from '../redux/notifier/slice';

let displayed = [];
const useNotifier = () => {
  const intl = useIntl();
  const dispatch = useDispatch();
  const notifications = useSelector((state) => state.notifier);
  console.log('notifications', notifications);
  const { enqueueSnackbar, closeSnackbar } = useSnackbar();

  const storeDisplayed = (id) => {
    displayed = [...displayed, id];
  };

  const removeDisplayed = (id) => {
    displayed = [...displayed.filter((key) => id !== key)];
  };

  const showNotification = useCallback(
    (
      key, 
      message, 
      options = {}, 
      values = {},
    ) => {
      enqueueSnackbar(
        intl.formatMessage(
          {
            id: message,
            defaultMessage: 'Something went wrong',
          },
          values
        ),
        {
          key,
          ...options,
          onClose: (event, reason, key) => {
            if (options.onClose) {
              options.onClose(event, reason, key);
            }
          },
          onExited: (event, key) => {
            dispatch(removeSnackbar({ key }));
            removeDisplayed(key);
          },
        }
      );
    },
    [dispatch, enqueueSnackbar, intl]
  );

  const closeNotification = useCallback(
    (key) => {
      closeSnackbar(key);
    },
    [closeSnackbar]
  );


  useEffect(() => {
    notifications.forEach(({ key, message, options, dismissed=false, values }) => {
      if (dismissed) {
        closeNotification(key);
        return;
      }
      if (displayed.includes(key)) return;

      showNotification(key, message, options, values);
      storeDisplayed(key);
    });
  }, [notifications, showNotification, closeNotification]);
};

export default useNotifier;

I am using to following custom hook to display snackbar notification with the notistack library but the snackbar keeps re-rendering indefinitely

My notifier slice looks like the following

import { createSlice } from '@reduxjs/toolkit';

const notifierSlice = createSlice({
  name: 'notifier',
  initialState: [],
  reducers: {
    enqueueSnackbar: (state, action) => {
      const key = action.payload.options && action.payload.options.key;
      state.push({
        key: key || new Date().getTime() + Math.random(),
        ...action.payload,
      });
    },
    closeSnackbar: (state, action) => {
      return state.map((notification) => (
        (action.dismissAll || notification.key === action.key)
          ? { ...notification, dismissed: true }
          : { ...notification }
      ));
    },
    removeSnackbar: (state, action) => {
      return state.filter(
        (notification) => notification.key !== action.key,
      );
    },
  },
});

export const { 
  enqueueSnackbar, 
  closeSnackbar, 
  removeSnackbar 
} = notifierSlice.actions;

export default notifierSlice.reducer;

Any idea how to modify the code to stop it from re-rendering when the notification is displayed. Thanks for your help

1

There are 1 best solutions below

0
On

The issue you're facing is due to how you're updating and managing the displayed array in your useNotifier hook. Here's the updated code ,try this :

import { useEffect, useCallback } from 'react';
import { useIntl } from 'react-intl';
import { useDispatch, useSelector } from 'react-redux';
import { useSnackbar } from 'notistack';
import { removeSnackbar } from '../redux/notifier/slice';

const useNotifier = () => {
  const intl = useIntl();
  const dispatch = useDispatch();
  const notifications = useSelector((state) => state.notifier);
  const { enqueueSnackbar, closeSnackbar } = useSnackbar();

  const showNotification = useCallback(
    (key, message, options = {}, values = {}) => {
      enqueueSnackbar(
        intl.formatMessage(
          {
            id: message,
            defaultMessage: 'Something went wrong',
          },
          values
        ),
        {
          key,
          ...options,
          onClose: (event, reason, key) => {
            if (options.onClose) {
              options.onClose(event, reason, key);
            }
          },
          onExited: (event, key) => {
            dispatch(removeSnackbar({ key }));
          },
        }
      );
    },
    [dispatch, enqueueSnackbar, intl]
  );

  useEffect(() => {
    notifications.forEach(({ key, message, options, dismissed = false, values }) => {
      if (dismissed) {
        closeSnackbar(key);
      } else {
        showNotification(key, message, options, values);
      }
    });
  }, [notifications, showNotification, closeSnackbar]);

  // it returns nothing or a specific value if needed for custom logic
  return null;
};

export default useNotifier;

In the above code I removed the separate functions (storeDisplayed and removeDisplayed) that manage the displayed array. Instead, the logic for displaying notifications is simplified by directly calling enqueueSnackbar in the useEffect based on the notifications received from Redux.