The component for route 'ActivityFeed' must be a React component

夙愿已清 提交于 2020-01-03 21:17:46

问题


I have looked through various similar posts here on SO regarding similar issue, but none of the answers solved it for me.

This is the full error:

So in my src/navigation/feed/stack.js its being defined like so:

import React from 'react';
import {StackNavigator} from 'react-navigation';
import ActivityFeed from 'activity-feed/session-user/screens/Main';
import HamburgerButton from 'navigation-components/HamburgerButton';
import HeaderTitle from 'navigation-components/HeaderTitle';
import ActionAlertIndicator from 'navigation-components/ActionAlertIndicator';
import * as navConfig from '../config';
import * as cache from 'utils/cache';

const stack = StackNavigator(
  {
    ActivityFeed: {
      screen: ActivityFeed,
      navigationOptions: ({navigation}) => ({
        header: (
          <HeaderTitle
            headerLeft={() => (
              <HamburgerButton
                onPress={() => navigation.navigate('DrawerOpen')}
              />
            )}
            headerRight={() => (
              <ActionAlertIndicator
                onPress={() => {
                  cache.setRouteStarter('MainDrawer');
                  navigation.navigate('ActionAlertsStack');
                }}
              />
            )}
          />
        ),
      }),
    },
  },
  {
    navigationOptions: {
      ...navConfig.defaultStackConfig,
    },
  }
);

export default stack;

The actual component or screen is defined like so inside of src/activity-feed/session-user/screens/Main.js:


import React, {PureComponent} from 'react';
import {
  FlatList,
  StyleSheet,
  AppState,
  Platform,
  Dimensions,
  View,
  Alert,
} from 'react-native';
import PropTypes from 'prop-types';
import OneSignal from 'react-native-onesignal';
import {Loading, SwippableCard, BottomAlert} from 'common-components';
import EmptyState from 'activity-feed/session-user/components/EmptyState';
import EventFeedCard from 'events/components/EventFeedCard';
import SurveyBallotFeedCard from 'surveys-ballots/components/FeedCard';
import MicroSurvey from 'surveys-ballots/components/MicroSurvey';
import ActionAlertFeedCard from 'action-alerts/components/ActionAlertFeedCard';
import MissingAddressCard from 'action-alerts/components/MissingAddressCard';
import ArticleFeedCard from 'articles/components/ArticleFeedCard';
import GetInvolvedFeedCard from 'account-settings/components/GetInvolvedFeedCard';
import {connect} from 'react-redux';
import {
  fetchFeed,
  handleContentSwipe,
  undoSwipeAction,
  hideUndoAlert,
} from 'activity-feed/actions';
import {setSelectedEvent} from 'events/actions';
import {setSelectedSurvey} from 'surveys-ballots/actions';
import {setSelectedAlert, getCampaignDetails} from 'action-alerts/actions';
import * as cache from 'utils/cache';
import {setSelectedArticle} from 'articles/actions';
import {
  handleUpdateTopics,
  handleUpdateGetInvoved,
} from 'account-settings/preferencesActions';
import {scale} from 'react-native-size-matters';
import {emptyStateStyles} from 'theme';

const {height} = Dimensions.get('window');

export class ActivityFeed extends PureComponent {
  static propTypes = {
    displayAlert: PropTypes.bool,
    feed: PropTypes.array,
    fetchFeed: PropTypes.func,
    getCampaignDetails: PropTypes.func,
    handleContentSwipe: PropTypes.func,
    handleUpdateGetInvoved: PropTypes.func,
    handleUpdateTopics: PropTypes.func,
    hideUndoAlert: PropTypes.func,
    lastSwippedElement: PropTypes.object,
    loading: PropTypes.bool,
    navigation: PropTypes.object,
    setSelectedAlert: PropTypes.func,
    setSelectedArticle: PropTypes.func,
    setSelectedEvent: PropTypes.func,
    setSelectedSurvey: PropTypes.func.isRequired,
    undoSwipeAction: PropTypes.func,
    userEmailIsValidForVoterVoice: PropTypes.bool,
  };

  constructor(props) {
    super(props);
    this.prompted = false;

    this.state = {
      refreshing: false,
      appState: AppState.currentState,
    };
  }

  async componentDidMount() {
    AppState.addEventListener('change', this._handleAppStateChange);
    if (!this.props.loading) {
      const doRefresh = await cache.shouldRefresh('feed');
      if (this.props.feed.length === 0 || doRefresh) {
        this.props.fetchFeed();
      }
      cache.incrementAppViews();
    }
  }

  componentWillUnmount() {
    AppState.removeEventListener('change', this._handleAppStateChange);
  }

  _handleAppStateChange = async appState => {
    if (
      this.state.appState.match(/inactive|background/) &&
      appState === 'active'
    ) {
      cache.incrementAppViews();
      const doRefresh = await cache.shouldRefresh('feed');
      if (doRefresh) {
        this.props.fetchFeed();
      }
    }
    this.setState({appState});
  };

  _keyExtractor = ({Entity}) =>
    (Entity.Key || Entity.Id || Entity.CampaignId || Entity.Code).toString();

  _gotoEvent = event => {
    cache.setRouteStarter('MainDrawer');
    this.props.setSelectedEvent(event);
    const title = `${event.LegislatureType} Event`;
    this.props.navigation.navigate('EventDetails', {title});
  };

  _gotoSurveyBallot = survey => {
    cache.setRouteStarter('MainDrawer');
    this.props.setSelectedSurvey(survey);
    this.props.navigation.navigate('SurveyDetails');
  };

  _gotoArticle = article => {
    cache.setRouteStarter('MainDrawer');
    this.props.setSelectedArticle(article);
    this.props.navigation.navigate('ArticleDetails');
  };

  _onAlertActionButtonPress = async item => {
    cache.setRouteStarter('MainDrawer');
    await this.props.setSelectedAlert(item.Entity);
    this.props.getCampaignDetails();
    if (this.props.userEmailIsValidForVoterVoice) {
      this.props.navigation.navigate('Questionnaire');
    } else {
      this.props.navigation.navigate('UnconfirmedEmail');
    }
  };

  _onSwipedOut = (swippedItem, index) => {
    this.props.handleContentSwipe(this.props, {swippedItem, index});
  };

  _handleGetInvolved = (response, entity) => {
    if (response !== entity.IsSelected) {
      const isTopic = entity.Category !== 'GetInvolved';
      const items = [
        {
          ...entity,
          IsSelected: response,
        },
      ];
      if (isTopic) {
        this.props.handleUpdateTopics({topics: items});
      } else {
        this.props.handleUpdateGetInvoved({involved: items});
      }
    }
  };

  renderItem = ({item, index}) => {
    const {Type, Entity} = item;
    if (Type === 'EVENT') {
      return (
        <SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
          <EventFeedCard
            style={styles.push}
            mainActionButtonPress={() => this._gotoEvent(Entity)}
            event={Entity}
          />
        </SwippableCard>
      );
    }

    if (['SURVEY_SURVEY', 'SURVEY_BALLOT'].includes(Type)) {
      return (
        <SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
          <SurveyBallotFeedCard
            style={styles.push}
            survey={Entity}
            handleViewDetails={() => this._gotoSurveyBallot(Entity)}
          />
        </SwippableCard>
      );
    }

    if (Type === 'SURVEY_MICRO') {
      return (
        <SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
          <MicroSurvey style={styles.push} selectedSurvey={Entity} />
        </SwippableCard>
      );
    }

    if (Type === 'ALERT') {
      return (
        <SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
          <ActionAlertFeedCard
            datePosted={Entity.StartDateUtc}
            style={styles.push}
            title={Entity.Headline}
            content={Entity.Alert}
            mainActionButtonPress={() => this._onAlertActionButtonPress(item)}
            secondaryActionButtonPress={() => {
              this.props.setSelectedAlert(Entity);
              // eslint-disable-next-line
              this.props.navigation.navigate("ActionAlertDetails", {
                content: Entity.Alert,
                id: Entity.CampaignId,
                title: Entity.Headline,
              });
            }}
          />
        </SwippableCard>
      );
    }

    if (Type === 'ARTICLE') {
      return (
        <SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
          <ArticleFeedCard
            content={Entity}
            style={styles.push}
            mainActionButtonPress={() => this._gotoArticle(Entity)}
          />
        </SwippableCard>
      );
    }

    //prettier-ignore
    if (Type === 'NOTIFICATION' && Entity.Code === 'INDIVIDUAL_ADDRESS_HOME_MISSING') {
      return (
        <MissingAddressCard
          style={styles.push}
          navigate={() => this.props.navigation.navigate('HomeAddress')}
        />
      );
    }

    if (['PREFERENCE_TOPIC', 'PREFERENCE_INVOLVEMENT'].includes(Type)) {
      return (
        <SwippableCard onSwipedOut={() => this._onSwipedOut(item, index)}>
          <GetInvolvedFeedCard
            style={styles.push}
            title={Entity.DisplayText}
            onPress={response => this._handleGetInvolved(response, Entity)}
          />
        </SwippableCard>
      );
    }

    return null;
  };

  _onRefresh = async () => {
    try {
      this.setState({refreshing: true});
      this.props
        .fetchFeed()
        .then(() => {
          this.setState({refreshing: false});
        })
        .catch(() => {
          this.setState({refreshing: false});
        });
    } catch (e) {
      this.setState({refreshing: false});
    }
  };

  _trackScroll = async event => {
    try {
      if (this.prompted) {
        return;
      }

      const y = event.nativeEvent.contentOffset.y;
      const scrollHeight = height * 0.8;
      const page = Math.round(Math.floor(y) / scrollHeight);
      const alert = await cache.shouldPromtpPushNotificationPermissions();
      const iOS = Platform.OS === 'ios';

      if (alert && iOS && page > 1) {
        this.prompted = true;
        this._openPromptAlert();
      }
    } catch (e) {
      return false;
    }
  };

  _openPromptAlert = () => {
    Alert.alert(
      'Push Notifications Access',
      'Stay engaged with NFIB on the issues and activities you care about by allowing us to notify you using push notifications',
      [
        {
          text: 'Deny',
          onPress: () => {
            cache.pushNotificationsPrompted();
          },
          style: 'cancel',
        },
        {
          text: 'Allow',
          onPress: () => {
            OneSignal.registerForPushNotifications();
            cache.pushNotificationsPrompted();
          },
        },
      ],
      {cancelable: false}
    );
  };

  _getAlertTitle = () => {
    const {lastSwippedElement} = this.props;
    const {Type} = lastSwippedElement.swippedItem;

    if (Type.startsWith('PREFERENCE')) {
      return 'Preference Dismissed';
    }

    switch (Type) {
      case 'EVENT':
        return 'Event Dismissed';
      case 'SURVEY_BALLOT':
        return 'Ballot Dismissed';
      case 'SURVEY_SURVEY':
        return 'Survey Dismissed';
      case 'SURVEY_MICRO':
        return 'Micro Survey Dismissed';
      case 'ARTICLE':
        return 'Article Dismissed';
      case 'ALERT':
        return 'Action Alert Dismissed';
      default:
        return 'Dismissed';
    }
  };

  render() {
    if (this.props.loading && !this.state.refreshing) {
      return <Loading />;
    }

    const contentStyles =
      this.props.feed.length > 0 ? styles.content : emptyStateStyles.container;

    return (
      <View style={styles.container}>
        <FlatList
          contentContainerStyle={contentStyles}
          showsVerticalScrollIndicator={false}
          data={this.props.feed}
          renderItem={this.renderItem}
          keyExtractor={this._keyExtractor}
          removeClippedSubviews={false}
          onRefresh={this._onRefresh}
          refreshing={this.state.refreshing}
          ListEmptyComponent={() => (
            <EmptyState navigation={this.props.navigation} />
          )}
          scrollEventThrottle={100}
          onScroll={this._trackScroll}
        />
        {this.props.displayAlert && (
          <BottomAlert
            title={this._getAlertTitle()}
            onPress={this.props.undoSwipeAction}
            hideAlert={this.props.hideUndoAlert}
          />
        )}
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  content: {
    paddingHorizontal: scale(8),
    paddingTop: scale(16),
    paddingBottom: scale(20),
  },
  push: {
    marginBottom: 16,
  },
});

const mapState2Props = ({
  activityFeed,
  auth: {userEmailIsValidForVoterVoice},
  navigation,
}) => {
  return {
    ...activityFeed,
    userEmailIsValidForVoterVoice,
    loading: activityFeed.loading || navigation.deepLinkLoading,
  };
};

export default connect(mapState2Props, {
  fetchFeed,
  getCampaignDetails,
  handleUpdateGetInvoved,
  handleUpdateTopics,
  setSelectedAlert,
  setSelectedArticle,
  setSelectedEvent,
  setSelectedSurvey,
  handleContentSwipe,
  undoSwipeAction,
  hideUndoAlert,
})(ActivityFeed);

I don't see anything apparent with my code and I am wondering if its some change that the react-navigation team did.

I am using react-navigation version 1.5.11 with react-native version 0.60.4.

Is this a compatibility issue with the RN version? Do I have no choice but to upgrade?

And this problem seems prevalent throughout my application. I also get the error here:

This is the src/auth/screens/ResetLinkConfirmationAlert.js file:

import React from 'react';
import {connect} from 'react-redux';
import ResetPasswordLinkConfirmationAlert from 'auth/components/ResetPasswordLinkConfirmationAlert';
import PropTypes from 'prop-types';

const ResetLinkConfirmationAlert = ({resetEmail, navigation}) => {
  const {params} = navigation.state;
  return <ResetPasswordLinkConfirmationAlert email={resetEmail} {...params} />;
};

ResetLinkConfirmationAlert.propTypes = {
  navigation: PropTypes.object,
  resetEmail: PropTypes.string,
};

const mapStateToProps = ({registrations}) => {
  const {resetEmail} = registrations.resetPasswordData;
  const email = resetEmail || registrations.verificationEmail;
  return {resetEmail: email};
};

export default connect(mapStateToProps)(ResetLinkConfirmationAlert);

and its being imported here in src/navigation/auth/stack.js:

import React from "react";
import { StackNavigator, NavigationActions } from "react-navigation";
import { Intro } from "auth/screens/Intro";
import { Login } from "auth/screens/Login";
import { PasswordReset } from "auth/screens/PasswordReset";
import { RegisterNoEmail } from "auth/screens/RegisterNoEmail";
import AskForMembership from "auth/screens/AskForMembership";
import { CompleteAccount } from "auth/screens/CompleteAccount";
import { ConfirmMemberAccount } from "auth/screens/ConfirmMemberAccount";
import { Register } from "auth/screens/Register";
import SetNewPassword from "auth/screens/SetNewPassword";
import { RegisterEmailPassword } from "auth/screens/RegisterEmailPassword";
import ResetLinkConfirmationAlert from "auth/screens/ResetLinkConfirmationAlert";
import DetailsConfirmation from "auth/screens/DetailsConfirmation";
import AccountCreated from "auth/screens/AccountCreated";

import BackButton from "navigation-components/BackButton";
import CustomHeader from "navigation-components/CustomHeader";
import HeaderTitle from "navigation-components/HeaderTitle";
import { v2Colors } from "theme";
import { defaultStackConfig, defaultHeaderStyles } from "../config";

const leftRegiterNavOptions = {
  title: "Register",
  headerStyle: defaultStackConfig.authHeaderStyle
};

const stack = StackNavigator(
  {
    Intro: {
      screen: Intro,
      navigationOptions: {
        header: null
      }
    },
    Register: {
      screen: Register,
      navigationOptions: ({ navigation }) => ({
        header: <CustomHeader onPress={() => navigation.goBack(null)} />,
        headerStyle: defaultStackConfig.authHeaderStyle
      })
    },
    RegisterNoEmail: {
      screen: RegisterNoEmail,
      navigationOptions: leftRegiterNavOptions
    },
    RegisterEmailPassword: {
      screen: RegisterEmailPassword,
      navigationOptions: leftRegiterNavOptions
    },
    AskForMembership: {
      screen: AskForMembership,
      navigationOptions: {
        header: <HeaderTitle />,
        headerStyle: defaultStackConfig.authHeaderStyle
      }
    },
    ConfirmMemberAccount: {
      screen: ConfirmMemberAccount,
      navigationOptions: ({ navigation }) => ({
        header: (
          <HeaderTitle
            headerLeft={() => (
              <BackButton onPress={() => navigation.goBack(null)} />
            )}
          />
        ),
        headerStyle: defaultStackConfig.authHeaderStyle
      })
    },
    CompleteAccount: {
      screen: CompleteAccount,
      navigationOptions: {
        header: <HeaderTitle />,
        headerStyle: defaultStackConfig.authHeaderStyle
      }
    },
    Login: {
      screen: Login,
      navigationOptions: ({ navigation }) => ({
        title: "Log In",
        headerLeft: <BackButton onPress={() => navigation.goBack(null)} />,
        headerStyle: defaultStackConfig.authHeaderStyle
      })
    },
    PasswordReset: {
      screen: PasswordReset,
      navigationOptions: ({ navigation }) => ({
        title: "Password Reset",
        headerLeft: <BackButton onPress={() => navigation.goBack(null)} />,
        headerStyle: defaultStackConfig.authHeaderStyle
      })
    },
    ResetLinkConfirmationAlert: {
      screen: ResetLinkConfirmationAlert,
      navigationOptions: ({ navigation }) => ({
        title: "Password Reset",
        headerLeft: (
          <BackButton
            onPress={() => {
              const resetNavAction = NavigationActions.reset({
                index: 0,
                key: null,
                actions: [NavigationActions.navigate({ routeName: "Intro" })]
              });
              navigation.dispatch(resetNavAction);
            }}
          />
        ),
        headerStyle: defaultStackConfig.authHeaderStyle
      })
    },

Upgrading to react-navigation 2.0.0 is not the answer because I already tried that and if you are going to suggest upgrading to 3.x please explain how that will solve this issue.

It was suggested that in the changelog for react-redux 7.1.0 they mention a note stating PropTypes.func has to be changed to PropTypes.elementType if an element is being passed as a prop

github.com/reduxjs/react-redux/releases/tag/v7.0.1

So in the case where I am getting the error for SetNewPassword, I refactored it like so:

export class CompleteAccount extends PureComponent {
  static propTypes = {
    loading: PropTypes.bool,
    newConfirmResetPassword: PropTypes.string,
    newResetPassword: PropTypes.string,
    resetUserPassword: PropTypes.elementType.isRequired,
    setConfirnResetPassword: PropTypes.elementType.isRequired,
    setNewResetPassword: PropTypes.elementType.isRequired,
    validationErrors: PropTypes.object
  };

and then in navigation/auth/stack.js I added curly braces to the import statement like so:

import { SetNewPassword } from "auth/screens/SetNewPassword";

but I am still getting that error message, although I am not sure if I applied that correctly. At the same time I have noticed that SetNewPassword.js file only has the named export of CompleteAccount:

export class CompleteAccount extends PureComponent {
  static propTypes = {
    loading: PropTypes.bool,
    newConfirmResetPassword: PropTypes.string,
    newResetPassword: PropTypes.string,
    resetUserPassword: PropTypes.elementType.isRequired,
    setConfirnResetPassword: PropTypes.elementType.isRequired,
    setNewResetPassword: PropTypes.elementType.isRequired,
    validationErrors: PropTypes.object
  };
.......

export default connect(
  mapStateToProps,
  {
    resetUserPassword,
    setNewResetPassword,
    setConfirnResetPassword
  }
)(CompleteAccount);

Not sure how this file was working before in that manner. I usually name my files the same name as the class or functional screen and import it with the same name.

On further inspection I see that there are two screens with the same class name function.

CompleteAccount.js:

export class CompleteAccount extends PureComponent {
  static propTypes = {
    cellPhone: PropTypes.string,
    cellPhoneChanged: PropTypes.func.isRequired,
    city: PropTypes.string,
    cityChanged: PropTypes.func.isRequired,
    errors: PropTypes.object,
    firstName: PropTypes.string.isRequired,
    homeAddress: PropTypes.string,
    homeAddressChanged: PropTypes.func.isRequired,
    homePhone: PropTypes.string,
    homePhoneChanged: PropTypes.func.isRequired,
    registeredUser: PropTypes.object,
    registerUser: PropTypes.func.isRequired,
    state: PropTypes.string,
    stateChanged: PropTypes.func.isRequired,
    zipCode: PropTypes.string.isRequired,
    zipCodeChanged: PropTypes.func.isRequired,
  };

which is exported as:

export default connect(mapStateToProps, {
  cityChanged,
  homeAddressChanged,
  homePhoneChanged,
  cellPhoneChanged,
  stateChanged,
  zipCodeChanged,
  registerUser,
})(CompleteAccount);

and then there is SetNewPassword.js:

which is also named:

export class CompleteAccount extends PureComponent {
  static propTypes = {
    loading: PropTypes.bool,
    newConfirmResetPassword: PropTypes.string,
    newResetPassword: PropTypes.string,
    resetUserPassword: PropTypes.func.isRequired,
    setConfirnResetPassword: PropTypes.func.isRequired,
    setNewResetPassword: PropTypes.func.isRequired,
    validationErrors: PropTypes.object
  };
.....

export default connect(
  mapStateToProps,
  {
    resetUserPassword,
    setNewResetPassword,
    setConfirnResetPassword
  }
)(CompleteAccount);

even though the file name is completely different. That is confusing, why didn't they just give the second one the class name of SetNewPassword?


回答1:


The immediate issue looks like the multiple exports in the component files. Try removing the export before the class definition and only keep export default at the end.

Regarding the confusion about SetNewPassword.js and CompleteAccount.js having the same exports, that'll not cause an issue as long you import the default exported component.

To put it simply,

If you export a component as default, then you can import it without the {} curly braces, like

import CompleteAccount from '.../CompleteAccount.js'

Here you can name the import anything you want.

If you use the curly braces, that'll import the named export, like

import {CompleteAccount} from '.../CompleteAccount.js'



回答2:


After a long grueling 6 days at this, and attempting fixes that went against our understanding of using curly braces when you got named exports, I always suspected that the problem was with react-navigation because I did not mess with the react-navigation version or the codebase.

The problem is how react-navigation works or does not work with react-redux version 7.

React Navigation does not recognize the object returned by React-Redux version 7.

The solution was to downgrade to React-Redux version 5.1.1.



来源:https://stackoverflow.com/questions/57189982/the-component-for-route-activityfeed-must-be-a-react-component

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!