React Native elements list item switch not working - javascript

I have a SectionList with a RN elements ListItem, I have hidden the chevron and set the switch state to a state variable. the onSwitch method is not working, or at least i cant get it to work.
I have tried a few different ways of implementing it to no avail.
<SectionList
sections={[
{title: 'Company Focus', data: this.state.focusNodes},
]}
renderSectionHeader={({section}) => <Text style=
{styles.sectionHeader}>{section.title}</Text>}
renderItem={({item}) => <ListItem style={styles.subTitle} key=
{item.id} title={item.name}
switchButton hideChevron switched=
{this.state.isNodeSelected} switchOnTintColor={'#00BCD4'}
onSwitch={(value) => {
this._handleSwitch(value);
}}/>} />
_handleSwitch = (item) => {
this.state.isNodeSelected = true;
console.log(item);
console.log(this.state.isNodeSelected);
}
The other way:
onSwitch={(value) => {
this.setState(previousState => {
return{previousState, isNodeSelected: value}})
}}/>} />
The switch in the first example just moves then moves back. In the second example it moves all the switches in the list to on or off regardless of which one i switch on/off
EDIT - THE WHOLE COMPONENT :)
'use strict';
import React from 'react';
import { StyleSheet,
Text,
View,
NavigatorIOS,
ScrollView,
Button,
PickerIOS,
Dimensions,
SectionList,
TouchableHighlight,
} from 'react-native';
import {Header, ListItem, List} from 'react-native-elements';
import StepIndicator from 'react-native-step-indicator';
export default class FocusPage extends React.Component {
constructor(props){
super(props);
this.state ={
data : [],
roleNodes: [],
skillNodes: [],
focusNodes: [],
// roleSelection: '',
// childSelection: '',
isVisible: false,
currentPosition: 0,
selectedNodes: [],
isFocusNodeSelected: false
}
}
_createFocusNodesArray = (array) => {
var nodeArray =[];
for(var i = 0; i < array.length; i++){
if(array[i].orderNumber === 2)
nodeArray.push(array[i]);
}
return nodeArray;
}
_getFocusData = () => {
fetch('http://10.0.0.58:8082/api/getfocusnodes', {
method: 'GET'
})
.then((response) => response.json())
.then((responseJson) => {
// this.setState({
// data: responseJson
// })
var simpleArray =
this._simplifyJsonArray(responseJson);
// console.log(simpleArray);
this.setState({
focusNodes:
this._createFocusNodesArray(simpleArray),
})
})
.catch((error) => {
console.log(error);
// this.setState({
// isLoading: false,
// message: 'Woops! ' + error
// })
})
}
//'http://10.0.0.58:8082/api/gettreenodes'
//'http://192.168.6.217:8082/api/gettreenodes'
componentDidMount = () => {
this._getFocusData();
}
_simplifyJsonArray = (array) => {
var tempArray = [];
for(var i = 0; i < array.length; i++)
{
var tempNode = {
id: array[i].id,
name: array[i].name,
orderNumber: array[i].orderNumber,
isSelected: false
}
tempArray.push(tempNode);
}
return tempArray;
}
getSelection = (selectedItem) => {
console.log(selectedItem);
// selectedItem.isSelected = true;
// this.state.isNodeSelected = true;
for (let index = 0; index < this.state.focusNodes.length;
index++) {
const element = this.state.focusNodes[index];
if(element.name === selectedItem){
console.log(element.name);
this.state.isFocusNodeSelected = true;
}
}
if (selectedItem.isSelected) {
this.state.isFocusNodeSelected = true;
}
// console.log(this.state.isNodeSelected);
// for (let i = 0; i < this.state.focusNodes.length; i++) {
// const element = this.state.focusNodes[i];
// if (element.name === selectedItem) {
// element.isSelected = true;
// this.state.isNodeSelected = element.isSelected;
// this.state.selectedNodes.push(element);
// console.log(this.state.isNodeSelected);
// }
// else{
// this.state.isNodeSelected = false;
// }
// }
// console.log('The selected item: ' + selectedItem);
// console.log('The selected Array: ' + this.state.selectedNodes);
}
_handleSwitch = (item) => {
this.state.isFocusNodeSelected = true;
console.log(item);
console.log(this.state.isFocusNodeSelected);
}
render() {
return (
<View style={styles.container}>
<SectionList
sections={[
{title: 'Company Focus', data: this.state.focusNodes},
]}
renderSectionHeader={({section}) => <Text style={styles.sectionHeader}>{section.title}</Text>}
renderItem={({item}) => <ListItem style={styles.subTitle} key={item.id} title={item.name}
switchButton hideChevron switched={item.isSelected} switchOnTintColor={'#00BCD4'}
onSwitch={(value) => {
this.setState(previousState => {
return{previousState, isFocusNodeSelected: value}
})
}}/>} />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
pageTitle:{
fontSize: 27,
textAlign: 'center',
//margin: 20
},
sectionHeader:{
fontSize: 20,
backgroundColor: '#673AB7',
height: 40,
textAlign: 'center',
padding: 10,
color: 'white'
},
subTitle: {
fontSize: 16,
// textAlign: 'center',
// marginTop: 20,
backgroundColor: '#00BCD4'
},
popButton: {
borderRadius: 10,
padding: 5,
// marginLeft: 5,
marginRight: 5,
backgroundColor: '#00BCD4',
borderColor: 'black',
borderWidth: 1,
},
subtitleView: {
flexDirection: 'row',
paddingLeft: 10,
paddingTop: 5,
textAlign: 'right'
},
});

In your first example, the reason why your switches get flipped back is because you are always setting your switches to true, so they can never get set to false:
this.state.isNodeSelected = true;
That's not the only problem though. As you see in your second example, you fix this by setting the value to what get's passed into onSwitch as value. That's good. Now the issue of having all switches flip is because you are using the same state value for all switches.
switched={this.state.isNodeSelected}
So what is happening is that when you update isNodeSelected, you are updating it for every switch.
To fix this, you need to hold the switched value for each ListItem somewhere; probably the most straightforward would be in what you pass to sections.
sections={[
{title: 'Company Focus', data: this.state.focusNodes},
]}
The data you pass into your sections prop should be kept in your state so you can update the specific item that whose switch is flipped. However, without seeing your state code, it's hard to tell what you're doing and how exactly to fix it. The above explanation should be enough to get you to a solution though. Just remember that renderItem also gets an index argument which are shown in the simple examples from the docs and explained further in the prop docs.
Edit: With the edited in info and changes, we now have a renderItem where each ListItem has its own switched value stored in item.isSelected. So given that, our goal is to have a onSwitch that updates just that value for just that item. So what do we update?
Well, the SectionList's sections prop is getting that data from this.state.focusNodes (what data is set to). So updating the correct value in focusNodes is what needs to happen. As I alluded to above, one way to do this is to leverage renderItem's index argument:
renderItem={({item, index}) =>
<ListItem
style={styles.subTitle}
key={item.id}
title={item.name}
switchButton
hideChevron
switched={item.isSelected}
switchOnTintColor={'#00BCD4'}
onSwitch={(value) => {
let focusNodes = [...this.state.focusNodes];
focusNodes[index].isSelected = value;
this.setState({ focusNodes, isFocusNodeSelected: value });
}}
/>
}
Notes:
I used index to figure out which of the focusNodes needs to get updated. Since you are using SectionList, read the docs to understand how this index value gets determined. Be careful and don't make assumptions once you start using multiple sections. I say this because...
I noticed that data in your state is unused. If you eventually refactor and change your sections prop to use this instead or you move focusNodes into that, you'll have to refactor what is being updated. Take care and understand how your data is structured and not make a bad assumption about index.
In case the missing previousState threw you off, I used a shorthand in setState to make it cleaner.
I'm making the assumption that the value being passed to onSwitch is the correct boolean value that isSelected needs to be updated to. I don't have a test project setup to run this to confirm.

Related

React Hook "useState" is called inside condition, but not condition is inside

After to read this page:
https://typeofnan.dev/fix-the-react-hook-is-called-conditionally-error-in-react/
I try to find the error but i keep getting the error:
React Hook "useState" is called conditionally
here the code:
export const PageSettings = (props) => {
const {
table,
listviewFields,
t,
tableCrud,
updatePageSettingsFields,
templates,
visibleFields,
visibleFieldsToggle,
} = props;
let aFields = [];
let aFieldsForSortable = [];
for (const [keyName, fieldSet] of Object.entries(listviewFields)) {
const listViewField = table.listview.fields[keyName];
let fieldSource;
if (listViewField.virtual) {
// virtual don't throw error, must have always label
fieldSource = listViewField;
} else {
fieldSource = table.fields[keyName];
}
const field = table.fields[keyName];
const { aColHeaderToAdd, titleFieldFull, printCell } = getHeaderToAdd(
listViewField,
keyName,
fieldSource,
table,
props,
t,
templates,
tableCrud,
fieldSet,
listviewFields
);
if (printCell) {
aFieldsForSortable.push({ id: keyName, name: titleFieldFull });
aFields.push(
<div
key={keyName}
style={{
fontSize: "12px",
minWidth: "90px",
maxWidtht: "150px",
bgColor:
listviewFields && listviewFields[keyName].invisible === true
? "black"
: "green",
}}
>
<input
type="checkbox"
name={keyName}
style={{}}
checked={
!listviewFields[keyName].invisible ||
listviewFields[keyName].invisible === false
? true
: false
}
onChange={updatePageSettingsFields}
/>
<label htmlFor={keyName}>{titleFieldFull}</label>
</div>
);
}
}
const [stateList, setStateList] = useState([aFieldsForSortable]);
return (
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
color: "#FFFFFF",
paddingTop: "20px",
width: "100%",
backgroundColor: visibleFields ? "#555555" : null,
}}
>
<a onClick={visibleFieldsToggle}>
<ShowIcon icon="eyecheck" color="#5f97c1" />
</a>
<ReactSortable list={stateList} setList={setStateList}>
{state.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</ReactSortable>
{visibleFields && aFields}
</div>
);
};
const getHeaderToAdd = (
listViewField,
keyName,
fieldSource,
table,
props,
t,
templates,
tableCrud,
fieldSet,
listviewFields
) => {
let colsHeader = [];
let printCell = true;
// Logic Twin TT103
let titleField;
let titleFieldFull;
// fieldSource can be not exist, by example , virtual field: customers.listview.addresses2
if (
fieldSource &&
fieldSource.fieldDisplayedOptions &&
fieldSource.fieldDisplayedOptions.separatorsubfield === "col"
) {
/*
not call main field, but the subfields to get label
*/
for (const [nameSubField, objField] of Object.entries(
fieldSource.fieldDisplayedOptions.subfields
)) {
titleField = resolveListLabel(table.related[keyName].table, nameSubField); // look for related table
titleFieldFull =
titleFieldFull + (titleFieldFull ? " " : "") + titleField;
colsHeader.push(
<div className="cell" key={nameSubField + keyName}>
{t(titleField)}
</div>
);
}
} else {
if (listViewField.module) {
if (
!resolvePathObj(
props,
"myState.app.appSettings.modules." + listViewField.module,
true
)
) {
//if (keyName === 'dateaccounting') console.log('module not compat', props);
printCell = false;
}
}
if (listViewField.templates) {
if (
!intersection(
listViewField.templates,
templates,
props.tableCrud,
keyName
)
) {
//if (keyName === 'dateaccounting') console.log('no template');
printCell = false;
}
}
}
/*if (keyName === 'dateaccounting') {
console.log('module, propstemplates, templates', listViewField.module, templates, listViewField.templates);
}*/
if (printCell) {
titleFieldFull = resolveListLabel(tableCrud, keyName);
// try find short word, if not exist then get normal word
let title = t(titleFieldFull + "_short");
if (title === titleFieldFull + "_short") title = t(titleFieldFull);
titleFieldFull = title;
colsHeader.push(
<div className="cell" key={keyName}>
{titleFieldFull}
</div>
);
}
return { titleFieldFull, printCell, colsHeader };
};
thanks
You're passing the argument aFieldsForSortable which is initialized as [] and then conditionally (inside the if statement), you're writing information for that value.
I'd suggest starting the use state as empty and in case you're changing it, call the setStateList method. For example,
const [stateList, setStateList] = useState([]);
....
if (printCell) {
const aFieldsForSortable = stateList;
aFieldsForSortable.push({ id: keyName, name: titleFieldFull });
setStateList(aFieldsForSortable);
You are breaking the Rules of Hooks.
Rules of Hooks say:
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions
The useState() call should be at the top of your function. If your initial state is based on a complex calculation, then you should do that calculation inside useEffect() (setting the value using setStateList()), otherwise the complex calculation will be re-run every time your Component is re-rendered.

How do I pass the answer from application state as a prop and not the internal state of the card

I have a component that is giving me the following issues:
TEST STEPS:
Login to Mobile.
On the Activity Feed, answer the first Get Involved question.
Scroll to the last Get Involved question and answer it.
Scroll back to the first Get Involved question that was answered.
EXPECTED RESULTS:
The answer for the first Get Involved question should still be selected.
ACTUAL RESULTS:
It is not selected.
So it seems the issue is the callback that updates some state on the parent, but the parent is not passing the yes/no prop to this component and it's unmounting to spare me rendering.
On the backend, in the database, the answers are being recorded, but it's not persisting in the UI. I noticed that the helper functions below, specifically the this.props.onPress() return 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>
);
}
}
So it appears that the state is being managed by the parent component as opposed to Redux.
What is unclear is, whether or not it is unmounting the component that defines onPress. It sounds like it is.
Just scrolling, so there is an activity feed composed of cards some of them render boolean type questions
do you want to get involved? no yes
When a user selects either response, then scrolls down a certain amount and then scrolls back up to that same question, it's like the user never answered it. I noticed when the user selects NO, the this.props.handleUpdateGetInvolved({ involved: items }) from _handleGetInvolved function is not fired only when the user selects YES. Referring to this:
_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 });
}
}
};
the helper functions themselves for each answer always returns undefined inside the GetInvolvedFeedCard component:
_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>
If I am not mistaken all this is being rendered overall by the ActivityFeed component:
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);
This is what the Get Involved card looks like:
So when the user clicks NO or YES, then scrolls that card away from view, the expectation is when they scroll that card back into view NO or YES, whichever one selected, should still be there.
Also, the answer selected only disappears when the user scrolls all the way to the bottom of the activity feed and then back up, but if the user only scrolls halfway through the activity feed cards and returns to this Get Involved card, the answer selected does not go away.
I believe the below SO article answer is exactly what is happening to me:
React Native - FlatList - Internal State.
So it seems the answer here would be to pass the answer from the application state as a prop and render based on that prop, and not the internal state of the card but I am not entirely sure what that looks like or how to put it together.
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.};
}
but then I realized I am not sure what I am passing in as there are no action creators involved in this component, therefore no reducers
so can I use mapStateToProps if there is no action creator/reducer involved in this component?
utilizing mapStateToProps is the only way I know or its the way with the most experience I have in passing the user's input from the application state as a prop and render based on that prop, and not the internal state.
I can guess the problem. The problem is you are using something like FlatList. It will recreate the card every time you scroll it over screen, then scroll back.
So there are 2 ways to fix it:
Using ScrollView. It won't recreate cards
Move card states out of the Card component. Move them to List state. So in List state you will have something like:
this.state = {
cardSelectedStatus: [], // array [true, false, true, ...]. Mean card 0: selected, card 1: no selected,...
...
};
Then you can pass state to each card
So after several weeks of hammering away at this and posting the above question, here is what I finally honed in on:
By default the data for the preference is set to isSelected: false in the Redux state.
If I hit Yes, the Redux state is updated. This changes the isSelected to true.
If I hit No after hitting Yes, the Redux state is NOT updated, it stays isSelected: true.
So based on this, because isSelected does not have a neutral value, that is, starts out as false, which would equate to No, I cannot rely on Redux state for rendering the selection, which could have solved this.
Instead, in the interest of time, the only solution I could think of with much input from others includint tuledev, was to set <FlatList windowSize={500} />.
This is not an elegant solution but it will get FlatList to behave more like ScrollView in that preserve all the components so my component state will not get wiped out when a user scrolls to far away from it. This works without actually replacing it to ScrollView since there are properties and methods with FlatList that would not work by just dropping it into ScrollView same as it is in FlatList, for example, there is no renderItem property in ScrollView.

Why am I getting Invariant Violation: Invariant Violation: Maximum update depth exceeded. on setState?

I have this component:
// imports
export default class TabViewExample extends Component {
state = {
index: 0,
routes: [
{ key: 'first', title: 'Drop-Off', selected: true },
{ key: 'second', title: 'Pick up', selected: false },
],
};
handleIndexChange = index => this.setState({ index });
handleStateIndexChange = () => { // FUNCTION WITH THE ERROR
const { index } = this.state;
this.setState(({ routes }) => ({
routes: routes.map((route, idx) => ({
...route,
selected: idx === index,
})),
}));
};
renderTabBar = props => {
const { routes } = this.state;
this.handleStateIndexChange(); // HERE I GET THE ERROR
return (
<View style={tabViewStyles.tabBar}>
{props.navigationState.routes.map((route, i) => {
return (
<>
<TouchableOpacity
key={route.key}
style={[
tabViewStyles[`tabStyle_${i}`],
]}
onPress={() => this.setState({ index: i })}
>
<Text>
{route.title}
</Text>
// THE FUNCTION ATTEMPTS TO SHOW AN ELEMENT WHEN
// THE INDEX OF A ROUTE IS selected true
{routes[i].selected && (
<View
style={{
flex: 1,
}}
>
<View
style={{
transform: [{ rotateZ: '45deg' }],
}}
/>
</View>
)}
</TouchableOpacity>
</>
);
})}
</View>
);
};
renderScene = SceneMap({
first: this.props.FirstRoute,
second: this.props.SecondRoute,
});
render() {
return (
<TabView
navigationState={this.state}
renderScene={this.renderScene}
renderTabBar={this.renderTabBar}
onIndexChange={this.handleIndexChange}
/>
);
}
}
Full error:
Invariant Violation: Invariant Violation: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
This is the function giving the error:
handleStateIndexChange = () => { // FUNCTION WITH THE ERROR
const { index } = this.state;
this.setState(({ routes }) => ({
routes: routes.map((route, idx) => ({
...route,
selected: idx === index,
})),
}));
};
All I need is to set the state of selected to true so I can toggle the visibility of a component.
As stated in the comment above, you cannot call setState directly inside your render function.
I do not see any reason to keep the selected value in your state as it only depends on another value already in it, this information is redundant.
You should remove selected from all your objects in routes and change your JSX condition :
{routes[i].selected &&
To the following :
{i === this.state.index &&
To produce the same result.
You can now remove handleStateIndexChange from your code
Because of the way React will batch state updates, if you need to set state based off previous state, you should do all the destructuring in the callback:
handleStateIndexChange = () => {
this.setState(({ index, routes }) => ({
routes: routes.map((route, idx) => ({
...route,
selected: idx === index,
})),
}));
};
Also, as #Adeel mentions, you should not call this.setState from inside the render execution context. Move that call the componentDidUpdate instead.
Calling setState in render is the actual reason for infinite rerender loop,
because setting state calls rerender, which calls another setState which calls another rerender etc.
So I would suggest to move the setState part into componentDidUpdate and control it there. Here is the approximation of what I mean with little changes to describe the point.
class App extends React.Component {
state = {
index: 0,
routes: [
{
key: "first",
title: "Drop-Off",
selected: true
},
{
key: "second",
title: "Pick up",
selected: false
}
]
};
componentDidUpdate(_, prevState) {
if (prevState.index !== this.state.index) {
this.handleStateIndexChange();
}
}
handleIndexChange = () => {
const randomIndexBetweenZeroAndOne = (Math.random() * 100) % 2 | 0;
this.setState({
index: randomIndexBetweenZeroAndOne
});
};
getMappedRoutes = (routes, index) =>
routes.map((route, idx) => ({
...route,
selected: idx === index
}));
handleStateIndexChange = () => {
this.setState(({ routes, index }) => ({
routes: this.getMappedRoutes(routes, index)
}));
};
render() {
const { routes } = this.state;
return (
<>
<button onClick={this.handleIndexChange}>Change Index</button>
{this.state.routes.map(JSON.stringify)}
</>
);
}
}

React Native how to use FlatList inside nativebase Tabs onEndReach keep fire non-stop

I'm new to react native so i use some component from 3rd party library and try to use react native component as possible.
ReactNative: 0.54
NativeBase: 2.3.10
....
i had problem with FlatList inside Tabs from Nativebase base on scrollView
onEndReachedThreshold not working correctly as Doc say 0.5 will trigger haft way scroll of item but when i set 0.5 it not trigger haft way to last item it wait until scroll to last item and it trigger onEndReach.
i had problem with onEndReach if i use ListFooterComponent to render loading when data not delivery it keep firing onEndReach non-stop.
here is my code
check props and init state
static getDerivedStateFromProps(nextProps) {
const { params } = nextProps.navigation.state;
const getCategoryId = params ? params.categoryId : 7;
const getCategoryIndex = params ? params.categoryIndex : 0;
return {
categoryId: getCategoryId,
categoryIndex: getCategoryIndex,
};
}
state = {
loadCategoryTab: { data: [] },
loadProduct: {},
storeExistId: [],
loading: false,
refreshing: false,
}
loadCategory
componentDidMount() { this.onLoadCategory(); }
onLoadCategory = () => {
axios.get(CATEGORY_API)
.then((res) => {
this.setState({ loadCategoryTab: res.data }, () => {
setTimeout(() => { this.tabIndex.goToPage(this.state.categoryIndex); });
});
}).catch(error => console.log(error));
}
Check onChange event when Tabs is swip or click
onScrollChange = () => {
const targetId = this.tabClick.props.id;
this.setState({ categoryId: targetId });
if (this.state.storeExistId.indexOf(targetId) === -1) {
this.loadProductItem(targetId);
}
}
loadProductItem = (id) => {
axios.get(`${PRODUCT_API}/${id}`)
.then((res) => {
/*
const {
current_page,
last_page,
next_page_url,
} = res.data;
*/
this.setState({
loadProduct: { ...this.state.loadProduct, [id]: res.data },
storeExistId: this.state.storeExistId.concat(id),
});
})
.catch(error => console.log(error));
}
loadMoreProduct when onEndReach is trigger
loadMoreProductItem = () => {
const { categoryId } = this.state;
const product = has.call(this.state.loadProduct, categoryId)
&& this.state.loadProduct[categoryId];
if (product.current_page !== product.last_page) {
axios.get(product.next_page_url)
.then((res) => {
const {
data,
current_page,
last_page,
next_page_url,
} = res.data;
const loadProduct = { ...this.state.loadProduct };
loadProduct[categoryId].data = product.data.concat(data);
loadProduct[categoryId].current_page = current_page;
loadProduct[categoryId].last_page = last_page;
loadProduct[categoryId].next_page_url = next_page_url;
this.setState({ loadProduct, loading: !this.state.loading });
}).catch(error => console.log(error));
} else {
this.setState({ loading: !this.state.loading });
}
}
render()
render() {
const { loadCategoryTab, loadProduct } = this.state;
const { navigation } = this.props;
return (
<Container>
<Tabs
// NB 2.3.10 not fix yet need to use `ref` to replace `initialPage`
ref={(component) => { this.tabIndex = component; }}
// initialPage={categoryIndex}
renderTabBar={() => <ScrollableTab tabsContainerStyle={styles.tabBackground} />}
onChangeTab={this.onScrollChange}
// tabBarUnderlineStyle={{ borderBottomWidth: 2 }}
>
{
loadCategoryTab.data.length > 0 &&
loadCategoryTab.data.map((parentItem) => {
const { id, name } = parentItem;
const dataItem = has.call(loadProduct, id) ? loadProduct[id].data : [];
return (
<Tab
key={id}
id={id}
ref={(tabClick) => { this.tabClick = tabClick; }}
heading={name}
tabStyle={styles.tabBackground}
activeTabStyle={styles.tabBackground}
textStyle={{ color: '#e1e4e8' }}
activeTextStyle={{ color: '#fff' }}
>
<FlatList
data={dataItem}
keyExtractor={subItem => String(subItem.prod_id)}
ListEmptyComponent={this.onFirstLoad}
// ListFooterComponent={this.onFooterLoad}
refreshing={this.state.refreshing}
onRefresh={this.handleRefresh}
onEndReachedThreshold={0.5}
onEndReached={() => {
this.setState({ loading: !this.state.loading }, this.loadMoreProductItem);
}}
renderItem={({ item }) => {
const productItems = {
item,
navigation,
};
return (
<ProductItems {...productItems} />
);
}}
/>
// this OnLoadFooter is my tempory show loading without ListFooterComponent but i don't want to show loading outside FlatList hope i will get a help soon
<OnLoadFooter loading={this.state.loading} style={{ backgroundColor: '#fff' }} />
</Tab>
);
})
}
</Tabs>
</Container>
);
}
Loading Component
function OnLoadFooter(props) {
if (props.loading) return <Spinner style={{ height: 50, paddingVertical: 10 }} />;
return null;
}
Let me explain my process
init CategoryId and CategoIndex for Tabs active
after axios fire will get all category and render Tab item because nativebase Tabs bug when initailPage bigger than 0 it show blank page and i use ref trigger it when category complete load when this.tabIndex.goToPage is trigger it call onChange
onChage event start to check if tabClick Ref exist in StoreExistId that save category when they click if true we load product else we do nothing. i need ref in this because React state is async making my product fire loading duplicate data for 1st time so Ref come in to fix this.
when scroll down to last item it will loadMoreProduct by paginate on API
my data in state like below
StoreExistId: [1,2,3,4]
loadProduct: {
1: {data: [.....]},
2: {data: [.....]},
etc....
}
Thank in advanced
Some of NativeBase components use scrollView inside. I guess it could be ScrollableTab component which uses ScrollView? You should not use FlatList inside ScrollView, onReachEnd will not work then.
I was facing the same problem, solution is to use <FlatList> inside <Content> . For more information see https://stackoverflow.com/a/54305517/8858217

ReactNative null is not an object (evaluating 'this.state.dataSource')

I am running the following code in Android emulator but I am getting null is not an object (evaluating 'this.state.dataSource') error.
Please, could you help me to see what I am doing wrong? For some reason the line dataSource={this.state.dataSource} is getting null.
import React, {
Component
} from 'react';
import {
AppRegistry,
ActivityIndicator,
ListView,
Text,
View,
StyleSheet
} from 'react-native';
import Row from './Row';
import Header from './Header';
import SectionHeader from './SectionHeader';
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 20,
},
separator: {
flex: 1,
height: StyleSheet.hairlineWidth,
backgroundColor: '#8E8E8E',
},
});
export default class NoTocarList extends Component {
constructor(props) {
super(props);
const getSectionData = (dataBlob, sectionId) => dataBlob[sectionId];
const getRowData = (dataBlob, sectionId, rowId) =>
dataBlob[`${rowId}`];
fetch('http://xxxxx.mybluemix.net/get')
.then((response) => response.json())
.then((responseJson) => {
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2,
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
getSectionData,
getRowData
});
const {
dataBlob,
sectionIds,
rowIds
} =
this.formatData(responseJson);
this.state = {
dataSource: ds.cloneWithRowsAndSections(dataBlob, sectionIds,
rowIds)
}
})
}
formatData(data) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const dataBlob = {};
const sectionIds = [];
const rowIds = [];
for (let sectionId = 0; sectionId < alphabet.length; sectionId++) {
const currentChar = alphabet[sectionId];
const users = data.filter((user) =>
user.calle.toUpperCase().indexOf(currentChar) === 0);
if (users.length > 0) {
sectionIds.push(sectionId);
dataBlob[sectionId] = {
character: currentChar
};
rowIds.push([]);
for (let i = 0; i < users.length; i++) {
const rowId = `${sectionId}:${i}`;
rowIds[rowIds.length - 1].push(rowId);
dataBlob[rowId] = users[i];
}
}
}
return {
dataBlob,
sectionIds,
rowIds
};
}
render() {
return (
<View style={{flex: 1, paddingTop: 20}}>
<ListView
style={styles.container}
dataSource={this.state.dataSource}
renderRow={(rowData) => <Row {...rowData} />}
renderSeparator={(sectionId, rowId) => <View key={rowId} />}
style={styles.separator}
renderHeader={() => <Header />}
renderSectionHeader={(sectionData) => <SectionHeader {...sectionData} />}
/>
</View>
);
}
}
AppRegistry.registerComponent('NoTocar', () => NoTocarList);
From your example you have passed the dataSource as null to the ListView. So you need to initialize it first by using
this.state({
dataSource: {}
})
After getting the response from the Api call you need to set the dataSource state by using
this.setState({
dataSource: ds.cloneWithRowsAndSections(dataBlob, sectionIds,
rowIds)
})
The issue is that you're trying to update the state asynchronously, after a render, but are expecting the result on the first render. Another issue is that you're overwriting the state instead of updating it.
The fetch call in your constructor is async, meaning the constructor is finished and the component (along with its state) is created before that call resolves.
constructor() {
fetch('http://xxxxx.mybluemix.net/get')
// ...
.then(() => {
// ...
// this code gets called after the component is created
// this state is also overwriting already created state
this.state = {
dataSource: ds.cloneWithRowsAndSections(dataBlob, sectionIds,
rowIds)
}
})
}
Since your data is obtained asynchronously, you can add a check and show a progress indicator while its loading (you should also use setState instead of overwriting the state):
constructor() {
this.state = {
dataSource: null // initialize it explicitly
}
fetch('http://xxxxx.mybluemix.net/get')
// ...
.then(() => {
// ...
// use set state
this.setState({
dataSource: ds.cloneWithRowsAndSections(dataBlob, sectionIds,
rowIds)
})
})
}
render(){
// on initial render, while the state hasn't been fetched yet
// show the spinner
if (!this.state.dataSource) {
return <ActivityIndicator />
}
return(
<View style={{flex: 1, paddingTop: 20}}>
...
</View>
);
}

Categories

Resources