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.
I am developing a 'Google Forms' style application where you can create many different forms.
I am having a problem in TextInput while answering a form.
First let me explain how the form response system works. When the user opens the screen to answer a form, he / she receives through redux an array of objects containing the form's questions, so I use a map () to scroll through each question and create a new array with the answers, which begin empty. After that I render a TextInput for each text type question. The problem is that when I try to change TextInput's text, it doubles the value.
Example: If I type 'rrk' it will show 'rrkrrk'.
The odd thing is that if I put any console.warn () inside the text change function, the problem goes away and TextInput works normally.
I'm putting two links from two videos that I recorded showing what happens.
With console.warn: https://www.youtube.com/watch?v=6AZE1R46zFA
Without console.warn: https://www.youtube.com/watch?v=2bAId-cP8eY
//Create array with answers based on question type
export const createInputs = (forms, formOpenKey) => {
return dispatch => {
const formData = forms[formOpenKey]
const responses = []
formData.quizes.map((quiz, index) => {
switch(quiz.type) {
case 'TEXT': responses.push(''); break
case 'YES_NO': responses.push(false); break
case 'MULTIPLE_CHOICE': responses.push(0); break
case 'SELECTION': responses.push(Array(quiz.options.length).fill(false)); break
}
})
dispatch({ type: ADD_RESPONSE_SET_INPUTS, payload: responses })
}
}
export const changeResponse = (newValue, indexQuiz, itemIndex, itensArray) => {
return dispatch => {
console.warn('With this warn, TextInput works normally')
let selectionArray
if (itensArray) {
selectionArray = itensArray.slice()
selectionArray[itemIndex] = newValue
}
dispatch({ type: ADD_RESPONSE_CHANGE_ITEM, payload: itensArray ? selectionArray : newValue, id: indexQuiz })
}
}
case ADD_RESPONSE_CHANGE_ITEM:
return { ...state,
formResponse: state.formResponse.map((response, index) => index === action.id
? action.payload
: response
)}
//My class that renders each of the questions to the user
class ResponseInput extends React.Component {
shouldComponentUpdate(nextProps, nextState){
return (JSON.stringify(this.props.formResponse[this.props.index]) !== JSON.stringify(nextProps.formResponse[this.props.index]) || nextProps.formResponseError.includes(this.props.index))
}
render() {
const { quizInfo, index, formResponse, changeResponse, formResponseError, disabled, response } = this.props
console.warn('render input')
return (
<Surface style={ styles.backgroundFormItem }>
<Text style={ styles.title }>{ quizInfo.title }</Text>
{ quizInfo.required ? <Text style={ styles.required }>Resposta Obrigatória</Text> : null }
<View style={{ marginBottom: 10 }}/>
{ getResponseType(quizInfo, index, disabled ? response : formResponse[index], changeResponse, disabled) }
<View style={{ marginBottom: 12 }}/>
{ !disabled && formResponseError.includes(index) ? <View style={ styles.errorLine }/> : null }
</Surface>
)
}
}
mapStateToProps = state => ({
formResponse: state.addResponse.formResponse,
formResponseError: state.addResponse.formResponseError,
})
export default connect(mapStateToProps, { changeResponse })(ResponseInput)
//The function that returns the answer component type based on the question type (I just left the text, which is what matters)
const getResponseType = (info, indexQuiz, response, changeResponse, disabled) => {
switch(info.type) {
case 'TEXT':
return (
<TextInput editable={ !disabled } selectTextOnFocus value={ response } onChangeText={ text => changeResponse(text, indexQuiz) } selectionColor={ colors.primary } style={ styles.responseInputText } placeholder='Escreva a resposta'/>
)
}
}
I am trying to use Redux to display data from firebase DB but for some reason this.props is undefined
I used redux to display another data in another pages from my DB with no problems.But here I stuck and can't find my mistake
Here is my code.I hope someone can give me advice what is wrong here
in my invoice.js I have :
const mapStateToProps = (state) => {
return {
startFireData: state.startFireData,
};
}
const mapDispatchToProps = (dispatch) => {
return {
watchInvoiceStartFire: () => { dispatch(watchInvoiceStartFire()) },
};
}
constructor(props) {
super(props);
this.props.watchInvoiceStartFire();
}
and in Render:
return (
<View style={styles.container}>
<LinearGradient
colors={['#3399ff', '#66b3ff', '#cccccc']}
style={styles.pageStyle}>
<Text style={styles.buttonText} >{this.props.startFireData.נפיץ}</Text>
<Text style={styles.buttonText} >{this.props.startFireData.עשן_חדש}</Text>
<Text style={styles.buttonText} >{this.props.startFireData.עשן_ישן}</Text>
<Text style={styles.buttonText} >דוחות</Text>
<Text style={styles.buttonText} >דוחות</Text>
</LinearGradient>
</View>
in my redux file:
const watchInvoiceStartFire = () =>{
return function(dispatch) {
firebase.database().ref("Invoices/startfire/ammo").on("value", function(snapshot) {
var startFireData = snapshot.val();
dispatch(setStartFireData(startFireData));
}, function(error) { });
};
}
const setStartFireData = (startFireData) =>{
return {
type: "setStartFireData",
value: startFireData
}
}
const reducer = (state = initialState, action) => {
switch(action.type) {
case "setStorageData":
return { ...state, storageData: action.value };
case "setOperationData":
return {...state, operationData: action.value};
case "setStartFireData":
return {...state, startFireData: action.value};
default:
return state;
}
};
My mistake,I forgot to add to initial state
const initialState = {
personData: { },
storageData:{ },
operationData:{ },
startFireData:{ },
};
Sorry for bothering u here
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
Help me please solve this issue.
I use redux and react-redux to control state in my app.
But when I try to change styles in my Component depending in the value from redux store, it react with delay. When I add new Item and click the list and expect its color being changed, it does this only after I add another item, so that it always delays.
Here is my reducer
export const items = (state = [], action) => {
switch(action.type) {
case 'ADD_ITEM':
const newItem = {
title: action.title,
id: action.id,
selected: action.selected,
comments: action.comments
};
return [
...state,
newItem
];
case 'REMOVE_ITEM':
return state.filter(({ id }) => id !== action.id);
case 'SELECT_ITEM':
state.map((item) => {
if (item.id === action.id) {
return [
...state,
item.selected = true
];
} else {
return [
...state,
item.selected = false
];
}
});
default:
return state;
}
};
Here is my component which I want to react on every change of the redux store
import React from 'react';
import { connect } from 'react-redux';
import { removeItem, selectItem } from '../actions/items';
import { Badge, ListGroup, ListGroupItem, Button } from 'reactstrap';
const stylesForActiveItem = {
borderLeft: '4px solid red',
transition: 'all .5s',
marginLeft: '-4px',
borderRadius: '0'
}
class Item extends React.Component {
constructor(props) {
super(props);
}
render() {
const { removeItem, selectItem, id, title, selected } = this.props;
return (
<ListGroup className="Item">
<ListGroupItem
onClick={() => selectItem({ id })}
className={selected ? 'Item__text active-item' :
'Item__text'}
>{title} <Badge className="Item__badge">14</Badge>
</ListGroupItem>
<Button className="Item__btn" onClick={() => removeItem({ id
})}>Delete</Button>
</ListGroup>
);
}
}
const mapDispatchToProps = (dispatch) => ({
removeItem: (id) => dispatch(removeItem(id)),
selectItem: (id) => dispatch(selectItem(id))
})
export default connect(null, mapDispatchToProps)(Item);
state.map((item) => {
if (item.id === action.id) {
return [
...state,
item.selected = true
];
} else {
return [
...state,
item.selected = false
];
}
});
//I guess you need to do something like this
state.map((item) => {
if (item.id === action.id) {
return {...item, selected:true}
} else {
return {...item, selected:false}
}
});
Since even though map returns new array, internal object should also not get mutated. That is why we spread and create a new item object inside.
There is no need to create arrays again in map with entire state. That will just change your state structure instead of just changing a boolean.