I have a bug where a user clicks on a survey and then opens up what is called supporting information that expands the UI further, then the user selects his or her answer and clicks on the NEXT QUESTION button, at that point the whole top part of the screen drops down exposing this huge gap. This is the code I believe governs all that behavior:
class BallotSurveyDetails extends PureComponent {
componentDidUpdate(prevProps) {
if (prevProps.currentWizardPage !== this.props.currentWizardPage) {
this.scroll.props.scrollToPosition(0, 0, true);
}
}
render() {
const {
currentWizardPage,
selectedSurvey,
handleNextQuestionButtonPress,
handleResponseChanged,
loading,
responses,
handleSubmitButtonPress,
saving,
wizardPages
} = this.props;
if (!saving && loading) {
return <Loading />;
}
const isWizard = selectedSurvey.Layout !== "Wizard";
const isList = selectedSurvey.Layout !== "List";
const displayNextQ = isWizard && currentWizardPage < wizardPages;
const displaySubmit =
isList || (isWizard && currentWizardPage === wizardPages);
const sortedGroups = (selectedSurvey.QuestionGroups || []).sort(
(a, b) => a.Order - b.Order
);
const wizardGroup = isWizard ? sortedGroups[currentWizardPage - 1] : null;
return (
<SafeAreaView style={styles.container}>
{isWizard && wizardPages.length > 1 && (
<Card style={styles.pagination}>
<RadioPagination
numberOfPages={wizardPages}
currentPage={currentWizardPage}
/>
</Card>
)}
<KeyboardAwareScrollView
showsVerticalScrollIndicator={false}
extraScrollHeight={45}
innerRef={ref => {
this.scroll = ref;
}}
enableOnAndroid={true}
contentContainerStyle={{ paddingBottom: 90 }}
>
<View style={styles.headerContainer}>
<Text style={styles.ballotTitle}>{selectedSurvey.Name}</Text>
<Text style={styles.ballotSubtitle}>
{selectedSurvey.Description}
</Text>
</View>
{isList &&
What I tried to do to resolve this was add automaticallyAdjustContentInsets={false} inside the KeyboardAwareScrollView, did nothing to resolve the bug. Any ideas anyone?
I'm not sure what's causing this for you, but here are a few things that have corrected similar problems I've had in the past:
It can help to wrap every screen in a container with flex:1.
I had a similar case with conditionally rendering a search bar above a FlatList and I used this to fix it:
I added this to the top of my file.
import { Dimensions, other stuff you need} from 'react-native';
const deviceHieght = Dimensions.get('window').height;
and then I wrapped my FlatList in a view like this
<View style={this.state.showBar === false ? styles.containFlatlist : styles.containSearchFlatlist}>
and this is the styling it was referencing
containFlatlist: {
height: deviceHieght
},
containSearchFlatlist: {
height: deviceHieght-100
},
In a different similar case I had an issue like this with a screen that displayed photos on click within a scrollview. In that case I did this:
<ScrollView
ref={component => this._scrollInput = component}
>
Then in componentDidMount I put
setTimeout(() => {
this._scrollInput.scrollTo({ x: 0, animated: false })
}, 100)
I was also using react navigation in this case so I also did
return(<View style={styles.mainFlex}>
<NavigationEvents
onWillBlur={payload => this._scrollInput.scrollTo({x:0})}
/>
Followed by the rest of my code.
I hope one of those helps. Given that you're also dealing with a scrollview, my best guess is that the third fix is most likely to work in your situation.
So the appear is with this code snippet here:
componentDidUpdate(prevProps) {
if (prevProps.currentWizardPage !== this.props.currentWizardPage) {
this.scroll.props.scrollToPosition(0, 0, true);
}
}
In particular, this.scroll.props.scrollToPosition(0, 0, true);. In removing the whole component lifecycle method, the bug went away.
Related
I am working on a project that uses Google autocomplete to set locations. The project allows users to set pickup and destination location, and then they can also enter stop-by places up to additional 3, making it a total of 5.
Here's my sample code:
const placesRef = useRef([]);
const [stopspots, setStopSpots] = useState([]);
const [state, setState] = useState({
defaultPlacesInput: 'flex',
//and others
});
useEffect(() => {
placesRef.current = placesRef.current.slice(0, 5);
}, []);
const placesComponent = (i, placeholder) => {
return (<PlacesFrame key={i}>
...
<GooglePlacesAutocomplete
placeholder={placeholder}
minLength={2}
ref={el => placesRef.current[i] = el}
onPress={(data, details = null) => {
placesRef.current[i]?.setAddressText(data?.structured_formatting?.main_text);
setState({...state, defaultPlacesInput: 'flex'})
}}
enablePoweredByContainer={false}
fetchDetails
styles={{
textInput: [styles.input1,{paddingLeft:30}],
container: [styles.autocompleteContainer,{display:placesRef.current[i]?.isFocused() ? 'flex' : state.defaultPlacesInput}],
listView: styles.listView,
listView: styles.listView,
row: styles.row,
predefinedPlacesDescription: {
color: '#1faadb',
},
}}
query={{
key: GOOGLE_PLACES_API_KEY,
language: profile.language,
components: 'country:' + profile.iso,
}}
textInputProps={{
//value: '',
onChangeText: alterOtherFields
}}
renderRow={(data) => <PlaceRow data={data} />}
/>
...
</PlacesFrame>)
}
const stopByLocation = () => {
var counter = stopspots.length, obj = placesComponent(counter + 2, 'Drop off location');
setStopSpots([...stopspots, {
id: counter,
place: obj
}
])
}
And here is how the autocomplete component is rendered
return(
...
<View>
{placesComponent(0, 'Pick up location')}
{placesComponent(1, 'Drop off location')}
</View>
...
)
The output look like this
Everything works perfect when I call the placesComponent() function directly. But like I mentioned earlier, I want the users to be able to add up to 3 additional stop by locations, and because it is optional, additional fields is added by appending to hook, and then rendered. the code looks like this.
return(
...
<View>
{placesComponent(0, 'Pick up location')}
{placesComponent(1, 'Drop off location')}
//This will append more placed fields
{stopspots != '' ?
stopspots.map((item : {}) => ((item.place)))
: null}
<ClickableButton>
<TouchableOpacity activeOpacity={0.6} onPress={() => stopByLocation()}><AddPlaces><AntDesign name="plus" size={10} color="#444" /> Add</AddPlaces></TouchableOpacity>
</ClickableButton>
</View>
...
)
The outcome looks like this
I observed that each component binded to the hooks takes the early properties, and does not effect additional changes. While the first two fields rendered by calling the function directly does.
When I make changes to state.defaultPlacesInput (observe this in styles property of GooglePlacesAutocomplete), the changes only effect on the two components called directly.
Is there a module, or a systematic way to append the renderer function call, without using useState hooks to append the 3 additional fields?
Is it possible to expose stored properties in useState hooks to respond as the other two which observe the state changes? If yes, how?
Any contribution, suggestion will be accepted
I am writing a small ReactNative application that allows users to invite people to events.
The design includes a list of invitees, each of which is accompanied by a checkbox used to invite/uninvite said invitee. Another checkbox at the top of the list that performs a mass invite/uninvite on all invitees simultaneously. Finally a button will eventually be used to send out the invites.
Because the state of each of these elements depends changes made by the other I often need to re-render my entire UI whenever the user takes action on one of them. But while this works correctly it is causing me quite a few performance issues, as shown in this video
Here's the code I'm using:
import React, { Component } from 'react';
import { Container, Header, Title,
Content, Footer, FooterTab,
Button, Left, Right,
Center, Body, Text, Spinner, Toast, Root , CheckBox, ListItem, Thumbnail} from 'native-base';
import { FlatList, View } from 'react-native';
export default class EventInviteComponent extends Component {
constructor(props) {
super(props);
console.disableYellowBox = true;
this.state = {
eventName: "Cool Outing!",
invitees:[]
}
for(i = 0; i < 50; i++){
this.state.invitees[i] = {
name: "Peter the " + i + "th",
isSelected: false,
thumbnailUrl: 'https://is1-ssl.mzstatic.com/image/thumb/Purple111/v4/62/08/7e/62087ed8-5016-3ed0-ca33-50d33a5d8497/source/512x512bb.jpg'
}
}
this.toggelSelectAll = this.toggelSelectAll.bind(this)
}
toggelSelectAll(){
let invitees = [...this.state.invitees].slice();
let shouldInviteAll = invitees.filter(invitee => !invitee.isSelected).length != 0
let newState = this.state;
newState = invitees.map(function(invitee){
invitee.isSelected = shouldInviteAll;
return invitee;
});
this.setState(newState);
}
render() {
let invitees = [...this.state.invitees];
return (
<Root>
<Container>
<Content>
<Text>{this.state.eventName}</Text>
<View style={{flexDirection: 'row', height: 50, marginLeft:10, marginTop:20}}>
<CheckBox
checked={this.state.invitees.filter(invitee => !invitee.isSelected).length == 0}
onPress={this.toggelSelectAll}/>
<Text style={{marginLeft:30 }}>Select/deselect all</Text>
</View>
<FlatList
keyExtractor={(invitee, index) => invitee.name}
data={invitees}
renderItem={(item)=>
<ListItem avatar style={{paddingTop: 20}}>
<Left>
<Thumbnail source={{ uri: item.item.thumbnailUrl}} />
</Left>
<Body>
<Text>{item.item.name}</Text>
<Text note> </Text>
</Body>
<Right>
<CheckBox
checked={item.item.isSelected}/>
</Right>
</ListItem>}/>
</Content>
<Footer>
<FooterTab>
<Button full
active={invitees.filter(invitee => invitee.isSelected).length > 0}>
<Text>Invite!</Text>
</Button>
</FooterTab>
</Footer>
</Container>
</Root>);
}
}
In your code, in class method toggelSelectAll() {...} you modify the state directly by using this.state = ..., which is something to be avoided. Only use this.state = ... in your class constructor() {...} to initialize the state, and you should only use this.setState({...}) to update the state anywhere else.
Not sure if this should help your performance issues, but try replacing toggelSelectAll() with the following:
toggelSelectAll() {
const {invitees} = this.state;
const areAllSelectedAlready = invitees.filter(({isSelected}) => !isSelected).length === 0;
this.setState({
invitees: invitees.map(invitee => ({
...invitee,
isSelected: !areAllSelectedAlready
}))
});
}
Good luck! And, let me know if you would like me to refactor your above code to remove the 2nd this.state = ... in your constructor (which, once again, should be avoided when writing React).
I suggest:
Dividing your code by creating multiple components, so you won't have a massive render()
Using Redux to store invitee / global state, so you can choose which components should re-render in case of modifications
That's a good way to learn React Native!
I'm trying to call a function that will fire upon onFoucs on TextInput that will scroll the scrollView all the way down (using scrollToEnd())
so this is my class component
class MyCMP extends Component {
constructor(props) {
super(props);
this.onInputFocus = this.onInputFocus.bind(this);
}
onInputFocus() {
setTimeout(() => {
this.refs.scroll.scrollToEnd();
console.log('done scrolling');
}, 1);
}
render() {
return (
<View>
<ScrollView ref="scroll">
{ /* items */ }
</ScrollView>
<TextInput onFocus={this.onInputFocus} />
</View>
);
}
}
export default MyCMP;
the component above works and it does scroll but it takes a lot of time ... I'm using setTimeout because without it its just going down the screen without calculating the keybaord's height so it not scrolling down enough, even when I keep typing (and triggering that focus on the input) it still doesn't scroll all the way down.
I'm dealing with it some good hours now, I did set the windowSoftInputMode to adjustResize and I did went through some modules like react-native-keyboard-aware-scroll-view or react-native-auto-scroll but none of them really does the work as I need it.
any direction how to make it done the right way would be really appreciated. thanks!
Rather than using a setTimeout you use Keyboard API of react-native. You add an event listener for keyboard show and then scroll the view to end. You might need to create some logic on which input is focused if you have more than one input in your component but if you only have one you can just do it like the example below.
Another good thing to do is changing your refs to functional ones since string refs are considered as legacy and will be removed in future releases of react. More info here.
class MyCMP extends Component {
constructor(props) {
super(props);
this.scroll = null;
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow.bind(this));
}
componentWillUnmount () {
this.keyboardDidShowListener.remove();
}
_keyboardDidShow() {
this.scroll.scrollToEnd();
}
render() {
return (
<View>
<ScrollView ref={(scroll) => {this.scroll = scroll;}}>
{ /* items */ }
</ScrollView>
<TextInput />
</View>
);
}
}
export default MyCMP;
If you have a large dataset React Native docs is telling you to go with FlatList.
To get it to scroll to bottom this is what worked for me
<FlatList
ref={ref => (this.scrollView = ref)}
onContentSizeChange={() => {
this.scrollView.scrollToEnd({ animated: true, index: -1 }, 200);
}}
/>
Currently I am using a <ScrollView /> component to handle the scrolling for my items, as I will only ever have a maximum of three items I feel this is appropriate instead of introducing a <FlatList />. This component receives a prop called collapsed and onCollapseToggle which is used to modify the collapse prop that is passed to the child. I have also experimented with the child having it's collapsed variable in state, but it seems that modifying the child's state from the parent would be near-impossible.
When scrolling through the <ScrollView /> when a component is passed up (The user scrolls down far enough that the component is no longer displayed on the screen) I want to execute a function that could potentially change the collapsed value that's passed to the item being rendered. This way if a user as expanded an item to view more information about it, and then continues scrolling down the <ScrollView /> the item would be self-collapsing, without the user having the manually close it through some form of input.
I'm not currently sure about any way to go about this, and any help would be greatly appreciated. I will provide an example of the structure that I am working with, which may help someone come up with a solution. I do not mind restructuring my components.
class ContentInformation extends React.Component {
state = { content: [ ... ] };
onCollapseToggle = (index, displayed=true) => {
const { content } = this.state;
const arr = content.slice();
const item = arr[index];
if(!item) return;
if(!displayed) {
if(!item.collapsed) {
item.collapsed = true;
}
} else {
item.collapsed = !item.collapsed;
}
arr[index] = item;
this.setState({ content: arr });
}
render() {
return (
<ScrollView>
{ this.state.content.map((content, index) => (
<Item
key={content._id}
index={index}
onCollapseToggle={this.onCollapseToggle}
{...content} />
); }
</ScrollView>
);
}
}
So basically, as you can see, I only need to figure out when the <Item /> goes off-screen so I can call onCollapseToggle(index, false) to automatically re-collapse the component if it's open.
This is an example of how you can detect when an item is offscreen when scrolling.
state = { toggleDistance: 0 }
_handleScroll({nativeEvent: {contentOffset: {y}}}) {
const {toggleDistance} = this.state;
if (y >= toggleDistance) {
// The item is offscreen
}
}
render() {
<ScrollView
onScroll={this._handleScroll.bind(this)}>
<Item
onLayout={({nativeEvent: {layout: {y, height}}}) => this.setState({toggleDistance: (y + height)})}/>
...
</ScrollView>
}
Seems to me you need a FlatList or a ListView and ScrollView simply doesn't support this use case.
The hint is in the ScrollView description:
FlatList is also handy if you want ... any number of other features it supports out of the box.
I solve the problem improving the answer from ivillamil with some adjusts:
const [toggleDistance, setToggleDistance] = useState(0);
const [showBottom, setShowBottom] = useState(false);
const onScrollMoveFooter = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const currentOffset = event.nativeEvent.contentOffset.y;
if (event.nativeEvent.layoutMeasurement.height - currentOffset <
toggleDistance) {
setShowBottom(true);
} else {
setShowBottom(false);
}
};
And in the another component:
<Component
onLayout={({
nativeEvent: {
layout: { y, height },
},
}) => setToggleDistance(y + height)}
/>
and so I can conditionally hide what I wanted:
{showBottom && (<AnotherComponent/>)}
I want to display on my app a list of meal 'tags'. So based on the code below, I was able to do that. So as a result of the code, I will get a list or a set of mealTags displayed.
Question: I want to only show the first 2 tags, hide the rest and put a link 'show more where the rest will appear when I click it . How can I do this in ReactJS?
return (
<View {...otherprops} style={styles.mainContainer} elevation={3}>
<View style={styles.contentContainer}>
<MealTagsSection mealTags={post.mealTags} />
</View>
type MealTagsProps = {
mealTags: Array<MealTag>;
};
export function MealTagsSection(props: MealTagsProps) {
let {mealTags} = props;
return (
<View style={styles.mealTagsContainer}>
{
mealTags.map((mealTag) => {
let tagStyle = '';
if (mealTag.category === 1) {
tagStyle = styles.tag_healthy;
} else {
tagStyle = styles.tag_improve;
}
return (
<View style={tagStyle}>
<Text style={styles.tagText}>{mealTag.description}</Text>
</View>
);
})
}
</View>
);
}
You can use set visible count in state
this.state= {
visibleCount:2
}
and use slice function before map, for example
mealTags.slice(0, this.state.visibleCount).map(...)
Then you can increase visible count as you want in button onClick funtion.
Another option is to track the index in your .map block
mealTags.map((mealTag, idx) => ...
and style accordingly e.g. display:none when idx >= 2