I am developing an app using React Native and I'm struggling to figure out why my component re-renders in iOS but not in Android. I'm expecting it not to re-render, which means that the Android behavior is the one I'm looking for. The problem is, I don't want this headerIcons to be re-rendered. I've tried useCallback, useMemo, React.memo but it keeps refreshing in iOS devices...
The re-rendering part is the headerIcons argument inside the headerRightIcons parameter of the <Header.NoTitle> component. And here is the thing, this <Header.NoTitle> shows two icons on the screen, one is the < goBack button and the other is the ? help center button (the headerRightIcons parameter) but the only icon being re-rendered is the ? icon. The other one stays fixed.
All the components are being wrapped in a Pull-to-refresh scheme, but the requisition is only being made inside the last Container. Which means that the only part of the screen I wanted to be refreshed is the Container.
const headerIcons = useMemo(
() => [
{
icon: 'question-circle',
onPress: () =>
goToSection(
navigation as never,
EnumInvoiceMainScreenArrivedFrom.MyInvoices,
),
},
],
[navigation],
);
return (
<Wrapper>
<ScrollView
refreshControl={
<RefreshControl onRefresh={onRefresh} refreshing={isRefresh} />
}
stickyHeaderIndices={[0]}
>
<HeaderContainer>
<Header.NoTitle onBackPress={goBack} headerRightIcons={headerIcons} />
<Title>{t(`${i18nPrefix}.title`)}</Title>
<TabRender
tabs={tabsToRender}
tabIndex={tabIndex}
onTabSelected={setTabIndex}
/>
</HeaderContainer>
<Container flex={1} key={`list-updated-${refreshCounter}-times`}>
{children}
</Container>
</ScrollView>
</Wrapper>
);
};
Does the same thing happen if you do it like this?
const handleRightIconPress = ()=>{
goToSection(navigation, EnumInvoiceMainScreenArrivedFrom.MyInvoices)
}
<Header.NoTitle onBackPress={goBack}
headerRightIcons={{
icon: 'question-circle',
onPress: handleRightIconPress,
}}
/>
Related
TouchableOpacity onPress is not working inside Flatlist but when I replaced onPress with onPressIn/onPressOut it is working fine, but in that case the reaction is too fast and having issue while scrolling. I don''t know what it is happening and haven't found any related issue. Below is my code:
renderItem = ({ item, index }: { item: any, index: number }) => {
const { type } = this.props;
const valueType = {
phone: item,
stage: item.title,
location: item.name
}
return (
<TouchableOpacity
onPressIn={() => this.onSelect(item, index)}
style={styles.modalListContainer}
>
<Icon name={icon[type]} height={20} width={20}/>
<Spacer width={10} />
<View style={styles.modelTextContainer}>
<Text style={styles.modelText}>{valueType[type]}</Text>
</View>
</TouchableOpacity>
)
}
<FlatList
data={item}
renderItem={this.renderItem}
keyExtractor={this.keyExtractor}
ItemSeparatorComponent={() => <><Spacer height={10} /><View style={styles.modelTextDevider} /><Spacer height={10} /></>}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.container}
/>
It is rendered inside a Modal using react-native-modals library. Any help would be appreciated.
Thank you.
react-native-modals, have a parent touchable component (PanResponder) which wraps your children's components. On some android devices, when you have a touchable component like a button, the touch event does not propagate down to child component instead capture by react-native-modals parent component.
The ideal solution should be absolute positioning your button but will break your UI and the modal will be useless.
There's an existing issue with this library repository.
https://github.com/jacklam718/react-native-modals/pull/210
but the solution provided is not 100% accurate for Android devices.
If you're using React Navigation, you already installed react-native-gesture-handler.
import TouchableOpacity from react-native-gesture-handler in place of `react-native. It should solve the issue for most devices.
I try to recreate this application in react-native :
But I don't have any clue for making the red component.
I search on internet and I found react-navigation V5, But it look complicated just for that.
I also think to create a horizontal ScrollView, and every time I click on new 'page', rerender all the current view. But this is a good way?
Thanks for answers.
I like #mainak's suggestion of using react-native-tab-view. To add to that answer I've made a simple implementation that does what you want.
First install the library: yarn add react-native-tab-view or npm i react-native-tab-view --save.
Then you can do something like this:
// ...
import {TabView, SceneMap, TabBar} from 'react-native-tab-view';
// ...
const initialLayout = {width: Dimensions.get('window').width};
const renderTabBar = (props) => (
<TabBar
{...props}
tabStyle={{width: 120}}
scrollEnabled={true}
indicatorStyle={{backgroundColor: 'white', height: 5, borderRadius: 10}}
/>
);
const App = () => {
// ...
const [index, setIndex] = React.useState(0);
const [routes] = React.useState([
{key: 'first', title: 'First'},
{key: 'second', title: 'Second'},
{key: 'third', title: 'Third'},
{key: 'fourth', title: 'Fourth'},
{key: 'fifth', title: 'Fifth'},
]);
const renderScene = SceneMap({
first: FirstRoute,
second: SecondRoute,
third: ThirdRoute,
fourth: FourthRoute,
fifth: FifthRoute,
});
return (
<TabView
navigationState={{index, routes}}
renderTabBar={renderTabBar}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={initialLayout}
/>
);
};
const styles = StyleSheet.create({
scene: {
flex: 1,
},
});
I've defined view components named FirstRoute, SecondRoute, etc...But change this to the screens you want to have in the tab bar.
The critical part here is renderTabBar, the custom TabBar component that is passed to the TabView component. On the TabBar component we have an option called scrollEnabled which if set to true, allows us to scroll the bar from left to right depending on the size of the tab view and tabs.
I've defined a width of 120 for each tab, but you might want play around with this value depending on the size of your tab labels.
The red highlight underneath the active tab bar you refer to is called an indicator and can be styled using the indicatorStyle prop:
indicatorStyle={{backgroundColor: 'white', height: 5, borderRadius: 10}}
For more info look at the documentation on the github page: https://github.com/satya164/react-native-tab-view.
You can try with react-native-tab-view. I've used this for my similar kind of work.
I am trying to build an auth flow that has a welcome page and then login or signup depending on the user's case.
It is built inside a stack navigator. The first screen has no header but then login and signup do via their screen options.
// AuthNavigator
const AuthStackNavigator = createStackNavigator();
export const AuthNavigator = () => {
return (
<AuthStackNavigator.Navigator
initialRouteName={WELCOME_PAGE.id}
screenOptions={{
headerShown: false,
headerTintColor: colors.primary,
}}
lazy>
<AuthStackNavigator.Screen
name={WELCOME_PAGE.id}
component={WELCOME_PAGE.component}
/>
<AuthStackNavigator.Screen
name={LOGIN_PAGE.id}
component={LOGIN_PAGE.component}
options={LOGIN_PAGE.options}
/>
<AuthStackNavigator.Screen
name={SIGN_UP_PAGE.id}
component={SIGN_UP_PAGE.component}
options={LOGIN_PAGE.options}
/>
</AuthStackNavigator.Navigator>
);
};
This flow, is nested inside a tabNavigator:
// AuthTabNavigator
const AuthTabNavigator =
Platform.OS === 'android'
? createMaterialBottomTabNavigator()
: createBottomTabNavigator();
const RootNavigator = () => {
return (
<AuthTabNavigator.Navigator
activeColor={colors.primary}
inactiveColor="grey"
barStyle={materialBottomNavBackgroundColor}
tabBarOptions={defaultTabNavOptions}>
<AuthTabNavigator.Screen
name={WELCOME_PAGE.id}
component={AuthNavigator}
options={WELCOME_PAGE.options}
/>
</AuthTabNavigator.Navigator>
);
};
export default RootNavigator;
On iOS, things work fine but on Android, It has weird behaviour. When pressing on the input field, the keyboard pushes the bottom bar causing a clunky effect on press but also on dismiss of the field. As if it were recalculating the height every time and repositioning the layout.
In order to make sure it's not coming from my code, I tested again with the snippet from the React Native documentation
I get the following (these are on the Android emulator but I get the same result on my OnePlus android phone)
base version: https://recordit.co/tcEwDbo1oT
no header version: https://recordit.co/O4lZ9G83vg
not nested in a tab navigator: https://recordit.co/uh7mOGlKdk
The only version that works is the one when not nested in the tab navigator so I guess the issue comes from there.
I checked a few solutions but none worked:
Android manifest
Issue coming from the header
Another
workaround because of header
I am using React Native CLI:
"react-native": "0.62.2",
"#react-navigation/bottom-tabs": "^5.2.8",
"#react-navigation/drawer": "^5.7.2",
"#react-navigation/material-bottom-tabs": "^5.1.10",
"#react-navigation/native": "^5.1.7",
"#react-navigation/stack": "^5.2.17",
Let me know if you encountered the same issue and found a way to fix it. Thanks in advance.
EDIT: SOLUTION I WENT WITH
On top of the "custom" solution I posted in the comments, after opening an issue on React Navigation repo and checking closed issues, I found that on Android, you might have 2 options:
Either your invert the nesting, see here
Or you can set the behaviour of the keyboardAvoidingView to position on Android and provide a custom Tabbar
https://github.com/react-navigation/react-navigation/issues/7359#issuecomment-545842090
https://stackoverflow.com/a/51169574/11287266
I ended up going with the latter:
const TabBarComponent = props => {
return (
<View collapsable={false}>
<BottomTabBar {...props} />
</View>
);
};
// AuthTabNavigator
const AuthTabNavigator = createBottomTabNavigator();
const RootNavigator = () => {
return (
<AuthTabNavigator.Navigator
activeColor={colors.primary}
inactiveColor="grey"
barStyle={materialBottomNavBackgroundColor}
tabBarOptions={defaultTabNavOptions}
tabBar={props => <TabBarComponent {...props} />}>
<AuthTabNavigator.Screen
name={WELCOME_PAGE.id}
component={AuthNavigator}
options={WELCOME_PAGE.options}
/>
</AuthTabNavigator.Navigator>
);
};
export default RootNavigator;
<KeyboardAvoidingView
contentContainerStyle={{ flexGrow: 1 }}
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'position'}
keyboardVerticalOffset={Platform.OS === 'android' ? -50 : 25}>
I have my code setup as such:
export default class HomeScreen extends Component {
constructor() {
super();
}
componentDidMount () {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide);
}
_keyboardDidShow = () => {
console.log('keyboard did show')
}
_keyboardDidHide = () => {
console.log('keyboard did hide')
}
render() {
return (
<Container styles={styles.container} >
<Content styles={styles.content} contentContainerStyle={marginLeft=this.state.marginLeft}>
<Image
style={styles.bgImg}
source={Images.bgImg}
>
</Image>
<Image
style={styles.logo}
source={Images.logo}
>
</Image>
<Text style={styles.slogan}>This is the title</Text>
<Form style={styles.search_form}>
<Item rounded floatingLabel style={styles.search}>
<Label style={styles.search_label}>Where are you headed?</Label>
<Input style={styles.search_input} />
<Button full rounded style={styles.search_btn}>
<Icon name="search"></Icon>
</Button>
</Item>
</Form>
</Content>
</Container>
);
}
}
I want basically the Content component of native-base to avoid the keyboard. I have my logo at the top, the slogan below it and the form at the bottom of the screen by giving some absolute positioning. At this point, the content component moves way up the screen which I don't want. What I want is the logo and the slogan staying right at the top of the screen but the form which is at the bottom of the page; to move up.
Here's what I've researched so far:
Found out that there is actually a component from react native called KeyboardAvoidingView and I played around with it but keeping the Logo, background image and the rest of the content inside the KeyboardAvoidingView made all the content not show in the screen.
Later I found out that the native base component 'Content' itself extends KeyboardAvoidingView, so there was no need to use it in the first place. But I don't think KeyboardAvoidingView is working with my versions of react native and native base.
So at last, I decided that this was a bug and I would use the Keyboard module of the react native instead to do some custom work, which is where I'm at right now, code-wise.
The Question
The console logging inside _keyboardDidShow and _keyboardDidHide are working, which means now I just need to know how to change the style of a component on keyboardDidShow and keyboardDidHide. Any help is appreciated, of course!
I'm really new to react native so any suggestions to better improve my workflow will be taken seriously.
I have already stumbled across the same problem! I'd recommend you save the keyboard width on state, with, for example:
keyboardDidShow = e => this.setState(p => ({ ...p, height: e.endCoordinates.height })
keyboardDidHide = () => this.setState(p => ({ ...p, height: 0 })
then, having this height, you can make your UI depend on that value. After you make sure that is working, in order for it not to jump between positions, use Animated to have a seamless transition between the positions. Hope this helps!
I'm trying to set the refresh indicator of flat list in react native but don't know how to do it. List View has this prop :
refreshControl={<RefreshControl
colors={["#9Bd35A", "#689F38"]}
refreshing={this.props.refreshing}
onRefresh={this._onRefresh.bind(this)}
/>
}
But Flat List has only these :
refreshing={this.props.loading}
onRefresh={this._onRefresh.bind(this)}
I found the solution! It might be the dummy but FlatList also has a prop called refreshControl like ListView but I just didn't test it! Just like this:
<FlatList
refreshControl={<RefreshControl
colors={["#9Bd35A", "#689F38"]}
refreshing={this.props.refreshing}
onRefresh={this._onRefresh.bind(this)} />}
/>
refreshControl={
<RefreshControl
isRefreshing={isRefreshing}
onRefresh={loadProducts}
colors={[Colors.GreenLight]} // for android
tintColor={Colors.GreenLight} // for ios
/>
}
this covers both ios and android
In official doc for RefreshControl component it is stated as - This component is used inside a ScrollView or ListView to add pull to refresh functionality. When the ScrollView is at scrollY: 0, swiping down triggers an onRefresh event
So for FlatList don't use it directly because they provides the two special props named as refreshing and onRefresh - on which the standard RefreshControl will be added for "Pull to Refresh" functionality. Make sure to also set the refreshing prop.
USAGE -
Step 1:
const wait = (timeout) => { // Defined the timeout function for testing purpose
return new Promise(resolve => setTimeout(resolve, timeout));
}
const [isRefreshing, setIsRefreshing] = useState(false);
const onRefresh = useCallback(() => {
setIsRefreshing(true);
wait(2000).then(() => setIsRefreshing(false));
}, []);
Step 2:
Now use component as
<FlatList
style={styles.flatListStyle}
data={inProgressProjects.current}
keyExtractor={item => item._id}
renderItem={renderItem}
showsVerticalScrollIndicator={false}
refreshing={isRefreshing} // Added pull to refesh state
onRefresh={onRefresh} // Added pull to refresh control
/>
For more information refer here -
https://reactnative.dev/docs/refreshcontrol
https://reactnative.dev/docs/flatlist#onrefresh
Hope this will help you or somebody else.
Thanks!
You can pass in the renderScrollComponent to your FlatList component with the same RefreshControl component you have showed above. I have created a expo snack for this: https://snack.expo.io/rJ7a6BCvW
The FlatList is using VirtualizedList within itself, and for VirtualizedList component, it takes a renderScrollComponent: https://facebook.github.io/react-native/docs/virtualizedlist.html#renderscrollcomponent
I was passing bounces={false} to my Flatlist which was causing problem. This will not allow you to refresh. Remove it if you want to use the above one solution mentioned. Thanks