【问题标题】:React Native: How do I pass the answer from application state as a prop and not the internal state of the cardReact Native:如何将应用程序状态的答案作为道具而不是卡片的内部状态传递
【发布时间】:2019-11-20 19:27:14
【问题描述】:

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

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

预期结果: 第一个 Get Involved 问题的答案仍应被选中。

实际结果: 没有被选中

所以看来问题是更新父级上的某些状态的回调,但父级没有将 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>
    );
  }
}

所以看起来状态是由父组件而不是 Redux 管理的。

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

只是滚动,所以有一个由卡片组成的活动提要,其中一些卡片呈现布尔类型的问题 你想参与吗?不 是 当用户选择任一响应,然后向下滚动一定量,然后向上滚动回到同一个问题时,就像用户从未回答过它一样。 我注意到当用户选择 NO 时,_handleGetInvolved 函数中的 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);

这就是“参与”卡片的样子:

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

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

我相信下面的 SO 文章答案正是发生在我身上的事情: React Native - FlatList - Internal 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 是我知道的唯一方法,或者是我在将用户输入从应用程序状态作为道具传递并基于该道具而不是内部状态进行渲染方面经验最丰富的方法。

【问题讨论】:

  • 能否提供更多源代码和更多信息?我试图阅读您的问题,但仍然无法清楚地了解情况。
  • @tuledev,感谢您的关注。如果我提供 UI 的屏幕截图会有帮助吗?

标签: javascript reactjs react-native


【解决方案1】:

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

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

  1. 使用ScrollView。它不会重新创建卡片
  2. 将卡片状态移出卡片组件。将它们移动到列表状态。所以在 List 状态下你会得到类似的东西:
    this.state = {
      cardSelectedStatus: [], // array [true, false, true, ...]. Mean card 0: selected, card 1: no selected,...
      ...
    };

然后你可以将状态传递给每张卡片

【讨论】:

  • 你可以看看我最近在 OP 中添加的内容。我认为您的答案可以帮助我,但我提供的信息可能有助于使您的答案更具体,从而更好地帮助我。提前谢谢你。
  • 所以用ScrollView 替换Flatlist 不幸的是,我在设备上出现了一个空白的灰色屏幕,没有可滚动的卡片。
  • 替换后需要重构ScrollView的props来渲染children。 ScrollView 没有作为 FlatList 的方法。你应该看一下文档
【解决方案2】:

因此,经过数周的努力并发布上述问题后,我终于磨练了以下内容:

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

所以基于此,因为isSelected没有中性值,即以false开头,相当于No,我不能依赖Redux状态进行渲染选择,本来可以解决这个问题的。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-02-28
    • 2017-02-23
    • 1970-01-01
    • 1970-01-01
    • 2018-11-05
    • 2023-03-30
    • 1970-01-01
    • 2019-02-20
    相关资源
    最近更新 更多