繁体   English   中英

React Native:如何将应用程序 state 的答案作为道具而不是卡的内部 state 传递

[英]React Native: How do I pass the answer from application state as a prop and not the internal state of the card

所以我有一个组件给我以下问题:

测试步骤: 1. 登录手机。 2. 在 Activity Feed 上,回答第一个 Get Involved 问题。 3. 滚动到最后一个 Get Involved 问题并回答。 4. 滚动回第一个回答的 Get Involved 问题。

预期结果:仍应选择第一个“参与”问题的答案。

实际结果:未选中

所以似乎问题是更新父级上的一些 state 的回调,但父级没有将 yes/no 属性传递给该组件,并且卸载它以节省我的渲染

在后端,在数据库中,答案被记录下来,但它没有保留在 UI 中。 我注意到下面的辅助函数,特别是this.props.onPress()返回undefined

class GetInvolvedFeedCard extends PureComponent {
  static propTypes = {
    onPress: PropTypes.func.isRequired,
    style: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
    title: PropTypes.string.isRequired
  };

  constructor(props) {
    super(props);
    const helper = `${
      this.props.title.endsWith("?") ? "" : "."
    } Your NFIB preferences will be updated.`;
    this.state = {
      noSelected: false,
      yesSelected: false,
      helper
    };
  }

  _accept = () => {
    this.setState({ yesSelected: true, noSelected: false }, () => {
      this.props.onPress(true);
    });
  };
  _decline = () => {
    this.setState({ noSelected: true, yesSelected: false }, () => {
      this.props.onPress(false);
    });
  };

  render() {
    return (
      <Card style={this.props.style}>
        <View style={feedContentStyles.header}>
          <View style={feedContentStyles.contentType}>
            <Text style={feedContentStyles.title}>{"GET INVOLVED"}</Text>
          </View>
        </View>
        <Divider />
        <View style={feedContentStyles.content}>
          <View style={styles.content}>
            <Text style={styles.blackTitle}>
              {this.props.title}
              <Text style={styles.italicText}>{this.state.helper}</Text>
            </Text>
          </View>
          <View style={styles.footer}>
            <TouchableOpacity onPress={this._decline}>
              <Text
                style={[
                  styles.btnText,
                  styles.noBtn,
                  this.state.noSelected ? styles.selected : null
                ]}
              >
                {"NO"}
              </Text>
            </TouchableOpacity>
            <TouchableOpacity onPress={this._accept}>
              <Text
                style={[
                  styles.btnText,
                  this.state.yesSelected ? styles.selected : null
                ]}
              >
                {"YES"}
              </Text>
            </TouchableOpacity>
          </View>
        </View>
      </Card>
    );
  }
}

因此,state 似乎由父组件管理,而不是 Redux。

尚不清楚的是,它是否正在卸载定义onPress的组件。 听起来是这样。

只是滚动,所以有一个由卡片组成的活动提要,其中一些呈现 boolean 类型的问题你想参与吗? 否 是,当用户选择任一响应,然后向下滚动一定数量,然后向上滚动回到同一个问题时,就像用户从未回答过它一样。 我注意到,当用户选择 NO 时,_handleGetInvolved function 中的 this.props.handleUpdateGetInvolved({tained: items }) 仅在用户选择 YES 时才会触发:

_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.handleUpdateGetInvolved({ involved: items });
      }
    }
  };

每个答案的辅助函数本身总是在GetInvolvedFeedCard组件内返回 undefined :

_accept = () => {
    this.setState({ yesSelected: true, noSelected: false }, () => {
      this.props.onPress(true);
      console.log(
        "this is the accept helper function: ",
        this.props.onPress(true)
      );
    });
  };
  _decline = () => {
    this.setState({ noSelected: true, yesSelected: false }, () => {
      this.props.onPress(false);
      console.log(
        "this is the decline helper function: ",
        this.props.onPress(false)
      );
    });
  };

  render() {
    return (
      <Card style={this.props.style}>
        <View style={feedContentStyles.header}>
          <View style={feedContentStyles.contentType}>
            <Text style={feedContentStyles.title}>{"GET INVOLVED"}</Text>
          </View>
        </View>
        <Divider />
        <View style={feedContentStyles.content}>
          <View style={styles.content}>
            <Text style={styles.blackTitle}>
              {this.props.title}
              <Text style={styles.italicText}>{this.state.helper}</Text>
            </Text>
          </View>
          <View style={styles.footer}>
            <TouchableOpacity onPress={this._decline}>
              <Text
                style={[
                  styles.btnText,
                  styles.noBtn,
                  this.state.noSelected ? styles.selected : null
                ]}
              >
                {"NO"}
              </Text>
            </TouchableOpacity>
            <TouchableOpacity onPress={this._accept}>
              <Text
                style={[
                  styles.btnText,
                  this.state.yesSelected ? styles.selected : null
                ]}
              >
                {"YES"}
              </Text>
            </TouchableOpacity>
          </View>

如果我没记错的话,所有这些都是由ActivityFeed组件整体呈现的:

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);

这就是 Get Involved 卡的样子:

在此处输入图像描述

因此,当用户单击NOYES ,然后将该卡片滚动离开视图时,期望当他们将该卡片滚动回视图NOYES时,无论选择哪一个,都应该仍然存在。

此外,只有当用户一直滚动到活动提要的底部然后返回时,选择的答案才会消失,但如果用户只滚动到活动提要卡的一半并返回到此“参与其中”卡,则选择的答案不会不是 go。

我相信下面的 SO 文章答案正是发生在我身上的事情: React Native - FlatList - Internal State

因此,这里的答案似乎是将应用程序 state 的答案作为道具传递并基于该道具进行渲染,而不是卡的内部 state 但我不完全确定它的外观或如何将它们组合在一起.

constructor(props) {
    super(props);
    // const helper = `${
    //   this.props.title.endsWith("?") ? "" : "."
    // } Your NFIB preferences will be updated.`;
    this.state = {
      noSelected: false,
      yesSelected: false,
      helper

    };
  }

  _accept = () => {
    this.setState({ yesSelected: true, noSelected: false }, () => {
      this.props.onPress(true);
    });
  };
  _decline = () => {
    this.setState({ noSelected: true, yesSelected: false }, () => {
      this.props.onPress(false);
    });
  };
with:
this.state = {
      // noSelected: false,
      // yesSelected: false,
      // helper
      cardSelectedStatus: []
    };
with the idea of then implementing cardSelectedStatus in my mapStateToProps
function mapStateToProps(state) {
  return {cardSelectedStatus: state.};
}

但后来我意识到我不确定我传入了什么,因为这个组件中没有动作创建者,因此没有减速器,所以如果这个组件中没有动作创建者/减速器,我可以使用mapStateToProps吗?

使用 mapStateToProps 是我知道的唯一方法,或者是我最有经验的方法,将来自应用程序 state 的用户输入作为道具传递并基于该道具进行渲染,而不是内部 state。

我能猜到问题所在。 问题是您使用的是FlatList类的东西。 每次您在屏幕上滚动时,它都会重新创建卡片,然后向后滚动。

所以有两种方法可以解决它:

  1. 使用ScrollView 它不会重新创建卡片
  2. 将卡片状态移出卡片组件。 将它们移至列表 state。 因此,在列表 state 中,您将拥有如下内容:
    this.state = {
      cardSelectedStatus: [], // array [true, false, true, ...]. Mean card 0: selected, card 1: no selected,...
      ...
    };

然后你可以将 state 传递给每张卡

因此,经过几周的努力并发布了上述问题,这就是我最终磨练出来的内容:

  • 默认情况下,首选项的数据在 Redux state 中设置为isSelected: false
  • 如果我点击Yes ,则更新 Redux state。 这会将isSelected更改为true
  • 如果我在点击Yes后点击No ,则 Redux state 没有更新,它保持isSelected: true

因此,基于此,因为isSelected没有中性值,即以false开头,这相当于No ,我不能依靠 Redux state 来呈现选择,这可以解决这个问题。

相反,为了节省时间,我能想到的唯一解决方案是设置<FlatList windowSize={500} />

这不是一个优雅的解决方案,但它会使FlatList的行为更像ScrollView ,因为它保留了所有组件,因此当用户滚动到远离它时,我的组件 state 不会被清除。 这无需实际将其替换为ScrollView即可工作,因为FlatList的某些属性和方法仅通过将其放入ScrollView就无法正常工作,就像在FlatList中一样,例如,在ScrollView中没有renderItem属性。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM