wix / react-native-navigation

A complete native navigation solution for React Native
https://wix.github.io/react-native-navigation/
MIT License
13.04k stars 2.67k forks source link

App crashing due 'Attemped to remove more RNNEvenEmitter listeners than added' #6171

Closed MakhouT closed 4 years ago

MakhouT commented 4 years ago

TLDR: https://streamable.com/w4aqeu After discussing with @guyca , we agreed upon creating an issue with as many details as possible as we currently don't know where this issue is coming from exactly.

I will also try to reproduce this in the playground app.

To give a bit more context about the components I have: I have an app with this layout:

Navigation.setRoot({
    root: sideMenu: {
      right: {
        stack: {
          children: [
            {
              component: {
                name: CONSTANTS.DRAWER_COMPONENT,
              },
            },
          ],
          id: CONSTANTS.DRAWER_COMPONENT,
        },
      },
      center: {
        bottomTabs: {
          id: CONSTANTS.BOTTOMS_TAB,
          children: tabs,
        },
      },
      options: {
        sideMenu: {
          animationType: 'slide-and-scale',
          right: {
            animationVelocity: 2500,
            width: Dimensions.get('screen').width / 1.25,
            visible: false,
            enabled: false,
          },
        },
      },
    },
});

Then every screen is wrapped with an HOC, where I am rendering a selfmade FAB. Which when pressing that button, an overlay is being shown and we are displaying the options of that FAB. In terms of code it looks something like this (stripped a lot of unneeded details)

export const FABOptions = props => {
// 2 --------------------
  const openScreen = (action) => {
    props.toggle();
    Navigation.dismissOverlay(componentId);
    const rightButtons = [{ id: ActionButton.SAVE, text: 'save' }];
    const leftButtons = [{ id: ActionButton.CANCEL, text: 'cancel' }];
    const requestId = generateLocalUUID();
    const modalId = requestId;
    showModal({
      component: action.newScreenKey,
      title: action.text,
      passProps: {
        createMode: true,
        isNewItem: true,
        modalId,
        noAvatar: true,
        requestId,
      },
      leftButtons,
      rightButtons,
      noDefaultCancelListener: true,
      modalId,
    });
  };

  return (
    <TouchableHighlight onPress={openScreen}>
      <View>
        {actions.map(action => (
            <View>
              <Text>{action.text}</Text>
              <View>
                <Image source={action.icon} />
              </View>
            </View>
        ))}
      </View>
    </TouchableHighlight>
  );
};

let isOpen = false;
export const FAB = props => {
  const { screenKey } = props;
  const toggle = () => {
    isOpen = !isOpen;
  };
// 1 ------------
  const showFABGroup = () => {
    toggle();
    Navigation.showOverlay({
      component: {
        name: CONSTANTS.FAB_OPTIONS,
        passProps: {
          toggle,
          underlayingScreenKey: screenKey,
        },
      },
    });
  };

  return (
    <TouchableWithoutFeedback onPress={showFABGroup}>
        <View>
            <FABIcon />
        </View>
    </TouchableWithoutFeedback>
  );
};

So as you can see I am showing an overlay when pressing on the FAB (1) and when pressing an option I am dissmising the overlay and opening a modal (2).

This opens up a modal where I need to create an item in my app.

To open a model, we've written a wrapper around it that also contains a sidemenu. It looks like this:

export const showModal = (props) => {
    const id = modalId || CONSTANTS.DEFAULT_MODAL_ID;
    if (!noDefaultCancelListener) {
      const modalCancelListener = Navigation.events().registerNavigationButtonPressedListener(
        ({ buttonId }) => {
          if (buttonId === ActionButton.CANCEL) {
            const modalIdToRemove = modalStack[modalStack.length - 1];
            modalStack = modalStack.slice(0, -1);
            // only dismiss modal in case of default modal id
            if (modalIdToRemove === CONSTANTS.DEFAULT_MODAL_ID) {
              Navigation.dismissModal(modalIdToRemove || id);
              modalCancelListener.remove();
            }
          }
        },
      );
    }

    Navigation.showModal({
      sideMenu: {
        id,
        right: {
          stack: {
            children: [
              {
                component: {
                  name: CONSTANTS.DRAWER_COMPONENT,
                },
              },
            ],
            id: CONSTANTS.DRAWER_COMPONENT,
          },
        },
        center: {
          stack: {
            children: [
              {
                component: {
                  name: component,
                  passProps,
                  options: {
                    topBar: {
                      leftButtons,
                      rightButtons,
                    },
                  },
                },
              },
            ],
          },
        },
      },
    });
  };

So the modal contains a sidemenu which has the drawer component, which looks pretty simple like

import React, { useEffect } from 'react';
import { StyleSheet, View } from 'react-native';

import { Navigation } from 'react-native-navigation';

import * as S from '../assets/style/style.constants';
import { IDrawerProps } from '../types/drawer.types';
import { DRAWER } from '../constants/components.constants';
import { unsubscribeDrawerListeners } from '../utils/navigation.utils';

export const Drawer = (props: IDrawerProps) => {
  const { component, onDrawerDidDisappear } = props;

  useEffect(() => {
    const componentAppearListener = Navigation.events().registerComponentDidDisappearListener(
      ({ componentId: compId }) => {
        if (compId === DRAWER) {
          unsubscribeDrawerListeners();
          if (onDrawerDidDisappear) {
            onDrawerDidDisappear();
          }
        }
      },
    );
    return () => {
      componentAppearListener.remove();
    };
  }, []);

  return (
    <View>
      <View>
        <View>{component && component()}</View>
      </View>
    </View>
  );
};

So the modal opens up I fill in a form. To complete my form I need to select an item from something like a 'treeview' component. So in my form I press a button which just opens up my sidemenu in the modal, I select my item then press the save button(which is a right button of the sidemenu). This closes the sidemenu back to the form.

Now form in the modal is completely filled in. Then I press the save button which is a right button of the model. My item is saved (and my modal is still open, as this is expected) But now I press the FAB again and select to create a new item again. This will open a new modal and close the previous one. Repeat this process 6+ this in a row, then there will be this warning and then error: unknown

I was able to capture the last part just before the crash: https://streamable.com/w4aqeu

From our side we have been debugging this around 2 weeks now in our codebase and we can't find the issue. So this is our last resort at the moment, because we can't find how this is happening currently...

I will try to recreate this issue in the playground app in the meantime. Also another point to note out, is that we had this issue at least since 4.6 - I just upgraded to 6.4 to check if this issue was by any chance fixed.


Environment

stale[bot] commented 4 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you believe the issue is still relevant, please test on the latest version and report back. Thank you for your contributions.

stale[bot] commented 4 years ago

The issue has been closed for inactivity.