React Navigation custom navigator transitions - react-native

I'm looking to create a Stack Navigator that can handle animating specific elements between 2 screens. Fluid Transitions looked like a library I could use, but it doesn't support react-navigation 5.X. If there's a package that has this functionality for react-navigation v5, that would be great.
However, if there is no current package for v5, I'd like to extend the StackNavigator to handle this kind of functionality. I've been able to remove the default animations for the StackNavigator with something similar to the following (where transition is a bool taken in the options prop for the Stack.Screen:
const CustomTransitionStackNavigator = ({
initialRouteName,
children,
screenOptions,
...rest
}) => {
if (descriptors[state.routes[state.index].key].options.transition) {
return (
<View style={{ flex: 1 }}>
{descriptors[state.routes[state.index].key].render()}
</View>
);
}
return (
<StackView
{...rest}
descriptors={descriptors}
navigation={navigation}
state={state}
/>
);
};
I'd like to be able to use a Context (or some other method) of passing the transition progress to the scene's descendants in order to handle the animations. Is there some way to get the transition progress in v5? Or would this CustomTransitionStackNavigator need to manage that state? Thanks!

You can use CardAnimationContext or useCardAnimation (which is just a convenience wrapper for the first one) to get transition progress in a stack navigator.
For example:
import { useCardAnimation } from '#react-navigation/stack';
import React from 'react';
import { Animated } from 'react-native';
export const SomeScreen = () => {
const { current } = useCardAnimation();
return (
<Animated.View
style={{
width: 200,
height: 200,
backgroundColor: 'red',
transform: [{ scale: current.progress }],
}}
/>
);
};
This feature seems to be undocumented at the moment, but you can check TypeScript definitions to get some more information.

Related

Programatically hiding and showing individual tabs in React Native Router Flux Tabbar

I have a tabbar in my app using React Native Router Flux. There are a couple use cases where hiding or showing specific tabs based on the current user would be very helpful. The main ones I have run into are:
AB testing new tabs to specific users
Showing a special admin tab to certain users with certain privileges
The react-native-router-flux library does not support any options to do this from what I can see. How can I achieve this functionality?
The default tabbar component in react-native-router-flux is just the component from the react-navigation-tabs library. You can import this component directly into your code, customize as needed, and then pass it to react-native-router-flux through the tabBarComponent prop (documented here).
I created a new component, which you should be able to copy directly and just change the logic for actually hiding the tabs based on your state:
import React from 'react'
import { BottomTabBar } from 'react-navigation-tabs'
import { View, TouchableWithoutFeedback } from 'react-native'
import { connect } from 'react-redux'
const HiddenView = () => <View style={{ display: 'none' }} />
const TouchableWithoutFeedbackWrapper = ({
onPress,
onLongPress,
testID,
accessibilityLabel,
...props
}) => (
<TouchableWithoutFeedback
onPress={onPress}
onLongPress={onLongPress}
testID={testID}
hitSlop={{
left: 15,
right: 15,
top: 5,
bottom: 5,
}}
accessibilityLabel={accessibilityLabel}
>
<View {...props} />
</TouchableWithoutFeedback>
)
const TabBarComponent = props => (
<BottomTabBar
{...props}
getButtonComponent={({ route }) => {
if (
(route.key === 'newTab' && !props.showNewTab) ||
(route.key === 'oldTab' && props.hideOldTab)
) {
return HiddenView
}
return TouchableWithoutFeedbackWrapper
}}
/>
)
export default connect(
state => ({ /* state that you need */ }),
{},
)(TabBarComponent)
And then simply imported and used that in my Tabs component:
<Tabs
key="main"
tabBarComponent={TabBarComponent} // the component defined above
...
Detailed look at where these things are getting passed to
Looking at the line of the source of react-native-router-flux, it is using createBottomTabNavigator from the react-navigation library, and passing no component if you do not pass a custom tabBarComponent. The createBottomTabNavigator method in react-navigation comes from this line of the library, and is actually defined in react-navigation-tabs. Now, we can here see in react-navigation-tabs that if no tabBarComponent has been passed, it simply uses BottomTabBar, also defined in react-navigation-tabs. This BottomTabBar, in turn, takes a custom tab button renderer through props, called getButtonComponent.

Passing State to another Component in React Native

React Native Newbie here.
I have an (in my opinion) common use case here. I work with React Navigation and have 4 different Tabs. In the first Tab I have a FlatList from which I want to choose Favourites. These Favourites should be then listed in the other Tab. Nothing more so far.
The Problem I encounter is that I'm not figuring out how I can transmit the favourites variable declared in my state of the first tab, to the other Tab. Maybe the approach is completely wrong too..
First Tab/Screen:
import React, {Component} from 'react';
import { FlatList, Text, View, ScrollView, Image, TouchableHighlight} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons'
export default class HomeScreen extends Component {
state = {
data: [],
favourites: []
};
//Function called on the click of the Heart Button, adding the List Element to the State
addFav = item => {
this.setState((prevState) => ({'favourites': prevState.favourites+item.title+' '}))
alert(item.title)
}
componentWillMount() {
this.fetchData();
}
//Fetching the data from the API
fetchData = async () => {
const response = await fetch("http://192.168.1.19:8080/v1/api/event");
const json = await response.json();
this.setState({ data: json});
};
render() {
return <FlatList
ItemSeparatorComponent={() =>
<View
style={{ height: 1, width: '100%', backgroundColor: 'lightgray' }}
/>
}
data={this.state.data}
keyExtractor={(item => item.title)}
renderItem={({ item }) =>
<ScrollView>
<Image style={{alignSelf:'stretch', width:undefined, height:undefined, flex : 1, borderRadius:10}} source = {require('../img/landscape.jpeg')}/>
<TouchableHighlight onPress={()=>this.openEvent(item)}>
<View style={{flex: 1, flexDirection:'row', padding: 5}}>
<View style={{flex: 1}}>
<Text style={{fontSize:15, textAlign:'center', padding: 2}}>{this.timeConverterMonth(item.time)}</Text>
<Text style={{fontSize:15, textAlign:'center', padding: 2}}>{this.timeConverterDay(item.time)}</Text>
</View>
<View style={{flex: 4}}>
<Text style={{fontSize:15, padding: 2}}>{item.title}</Text>
<Text style={{fontSize:10, padding: 2}}>{item.locShort}</Text>
</View>
//That's where the function is called
<TouchableHighlight
style={{flex: 2}}
onPress={() => this.addFav(item)}
>
<Icon name="ios-heart-empty" size={24} style={{alignSelf: 'center', padding: 10}}/>
</TouchableHighlight>
</View>
</TouchableHighlight>
//just checking the state
<Text>{this.state.favourites}</Text>
</ScrollView>}
/>;
}
}
Second Tab/Screen:
import React, {Component} from 'react';
import {Text} from 'react-native';
export default class FavouritesScreen extends Component {
constructor(props){
super(props)
}
render(){
//Here I want to display my favourites Array from the HomeScreen State
return <Text>{this.props.favourites}</Text>;
}
}
I am actually not wondering why it's not functioning, I just tried the props method by reading all the other articles but the Screens are not in Parent/Child relation.
So what I want to do would be in the Second Tab something like
HomeScreen.state.favourites
Thanks.
Your case is a very common one. One I faced was passing a 'shared state' between the application.
The components have a local state, which you can pass to child components via props (which you have mentioned).
The problem arises when you want to access that state in another component. The solution here is having a global state.
You may want to consider Redux for your application.
https://redux.js.org/introduction/getting-started
From the redux website:
Redux is a predictable state container for JavaScript apps.
It helps you write applications that behave consistently, run in
different environments (client, server, and native), and are easy to
test. On top of that, it provides a great developer experience, such
as live code editing combined with a time traveling debugger.
Essentially, you'll be getting a global state which can be accessed by all your application's components. This allows you to update states within one component and access them in another.
I will warn you, it's not the easiest thing to learn. When you first look at it - it's a bit daunting. But, as your application grows in complexity and you add more state - you'll be glad you used it.
The good news is, Redux is very well documented with React and React Native - so you should find lots of tutorials on how to integrate it into your current application.
Your usecase of having "globally" accessed state is where state management libraries come in. One good example is the libary Redux - in this case you could store the favourites under a piece of state called "HomeScreen" and map it and use it in any screen in the rest of the app.
Here is a good article about getting started with redux: https://blog.cloudboost.io/getting-started-with-react-native-and-redux-6cd4addeb29

When there is a large list , flat list is laggy and sometimes crashing app in react native

Please help me. When there is a large list, flat list is laggy and sometimes crashing app in react native .
I have tried Optimized list: it is not showing images, and i have tried react-native large list. Nothing is working.
This is the code:
{loading == false && (
<FlatList
data={this.state.data}
numColumns="2"
renderItem={({ item }) => <ExampleComponent url=
{item.media} />}
keyExtractor={(item, index) => item.itemurl}
windowSize={15}
onEndReachedThreshold={1}
onEndReached={this.handleLoadMore}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
/>
)}
This is component class
import React, { PureComponent } from "react";
import { View, Text, StyleSheet, Image ,TouchableOpacity } from "react-native";
import FastImage from "react-native-fast-image";
export default class ExampleComponent extends PureComponent {
constructor(props) {
super(props);
//console.log(this.props);
}
render() {
return (
<TouchableOpacity style={styles.imageContainer}>
<View >
<FastImage
source={{
uri:this.props.url
}}
style={styles.imageStyle}
/>
</View>
</TouchableOpacity>
);
}
}
const styles = StyleSheet.create({
imageContainer: {
flex: 1,
borderWidth: 1,
borderColor: "#dedede",
borderRadius: 5,
margin: 5
},
imageStyle: {
flex: 1,
width: null,
height: 100,
borderTopLeftRadius:5,
borderTopRightRadius:5
},
});
And im getting this message in console when rows are more
This is handle load more function
handleLoadMore = () => {
console.log("Called");
if (this.state.next != 0) {
const u =
"https://url&q=" +
this.props.navigation.state.params.data +
"&pos=" +
this.state.next;
fetch(u, {
method: "GET"
})
.then(res => res.json())
.then(response => {
this.setState({
data: [...this.state.data, ...response.results],
next: response.next
});
});
}
}
VirtualizedList: You have a large list that is slow to update - make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc. {"dt":1098,"prevDt":684,"contentLength":6832}
Since the performance of the Flatlist depends on each implementation, you can follow these suggestions to help you in your case.
Try combinations of these approaches:
Enable or disable legacyImplementation
Enable or disable disableVirtualization
Increase or decrease the value of onEndReachedThreshold
Increase or decrease the value of windowSize
Increase or decrease the value of maxToRenderPerBatch
Implement shouldComponentUpdate
Remove the constructor from ExampleComponent
Enable removeClippedSubviews only for Android
Use a simpler keyExtractor, a number if possible.
Actually from the docs:
The default extractor checks item.key, then falls back to using the
index, like React does.
Also
Check in your render method how many items are being loaded and rendered in both ExampleComponent and your component that uses the FlatList.
Alternatively
Try using RecyclerListview component.
Further reading
Improve render performance for Flatlist
FlatList Scroll performance is laggy after 30+ rows
Wrapping the whole Flatlist in a ScrollView worked for me.

The best way to create a scrollable tab in the middle of the screen?

The mobile app of Twitter has a scrollable tab in the middle of the screen when you are on your profile. The top half of the screen displaying your profile info etc doesn't change when you click on the scrollable tabs mid screen : "Tweets & replies", "Media" etc. I am wondering how one would create this? Having half the screen stay the same and then having tabs which change mid screen... At the moment I have react navigation tabs as my main navigation - so on one of these tabs (the profile tab) I want to create the same concept as the picture..
Late answer but (for anyone else and future reference), react-navigation uses this package, react-native-tab-view: https://github.com/react-native-community/react-native-tab-view
for their tabs.
You can nest this within a screen, just like you desire (the previous answer only addresses the navigator inside navigator and that isn't what you want).
Here is an example (not exactly like you want, but you get the idea that you can. so instead of a background image, swap it out and use a view or scrollview accordingly to create that layout):
https://snack.expo.io/#satya164/collapsible-header-with-tabview
cheers :)
EDIT: i just found a way with just using react-navigation after all:
https://snack.expo.io/#mattx/collapsible-header-tabs
check it out
and another library: https://github.com/benevbright/react-navigation-collapsible
I don't know if you've figured it out yet, but you can nest the TabNavigator inside a StackNavigator. That way, you can have a scrollable Tab.
class ProfileMenu extends React.Component{
render() {
return(
//whatever you wanted at the top
)
}
}
const TabNaviga = createMaterialTopTabNavigator({
Tweets: {screen: TweetScreen,},
Replies: {screen: RepliesScreen,},
})
const YNavigator = createStackNavigator ({
Home:{screen: TabNaviga,
navigationOptions: ({navigation}) => ({
header: <ProfileMenu navigation= {navigation} />,
})
}
})
export default YNavigator
I found this tutorial and followed it,
EDIT: it seems there's a new library out that supports it https://github.com/PedroBern/react-native-collapsible-tab-view
https://medium.com/#linjunghsuan/implementing-a-collapsible-header-with-react-native-tab-view-24f15a685e07
I also wrote a bit of an explaination if you are interested.
create a ScrollY with useRef and .current at the end
create a handleScroll function which returns an event like so -
const handleScroll = Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
{ useNativeDriver: true }
);
Pass it down in props to the wanted component
<TabNavigator handleScroll={handleScroll} scrollY={scrollY} />
And also the scrollY so you can use the value in the Child component aswell
pass it farther down the line to actual events like and call handleScroll in the Child Child component onScroll prop. like so
<Animated.FlatList
...
onScroll={handleScroll}
/>
And now you can use the ScrollY value wherever you want.
what it does is checking if the current route is the one we check, it then caluclates the offset and scrollToOffset function of flatlist using the flatlist refs we got from here
return (
<Pictures
handleScroll={handleScroll}
onMomentumScrollBegin={onMomentumScrollBegin}
onScrollEndDrag={onScrollEndDrag}
onMomentumScrollEnd={onMomentumScrollEnd}
onGetRef={ref => {
if (ref) {
const found = listRefArr.current.find(e => e.key === route.key);
if (!found) {
listRefArr.current.push({ key: route.key, value: ref });
}
}
}}
/>
);
the onGetRef is connected to the FlatList ref
return (
<AnimatedFlatList
ref={onGetRef}
scrollToOverflowEnabled
onMomentumScrollBegin={onMomentumScrollBegin}
onScrollEndDrag={onScrollEndDrag}
onMomentumScrollEnd={onMomentumScrollEnd}
onScroll={handleScroll}
scrollEventThrottle={16}
contentContainerStyle={{
paddingTop: HeaderHeight + TabBarHeight,
paddingHorizontal: 10,
minHeight: windowHeight - TabBarHeight
}}
data={data}
renderItem={({ item }) => {
return <Comment data={item} />;
}}
keyExtractor={({ commentId }): any => {
return commentId.toString();
}}
/>
);
then we have these three functions which we send the flatlist as well
const onMomentumScrollBegin = () => {
isListGliding.current = true;
};
const onMomentumScrollEnd = () => {
isListGliding.current = false;
syncScrollOffset();
};
const onScrollEndDrag = () => {
syncScrollOffset();
};
and last but not least we still need to animate the TabBar so when the header is 500 height his is 0 when the header is 450 in the y the tabbar should be 50, we do that by getting the scrollY in the props and use it to interpolate.
const renderTabBar = (props: any) => {
return (
<Animated.View
style={{
top: 0,
zIndex: 1,
position: "absolute",
transform: [{ translateY: tabViewHeight }],
width: "100%"
}}
>
<TabBar ... />
</Animated.View>
);
};

Setting NavigationBar's title asynchronously

I am trying to set a NavigationBar for my Navigator. When I display a static title, it is OK. However, when I try to set title asynchronously (for example when I go to user's profile route, I get user's display name from API and set this.props.navigation.title when API call promise resolves) the title gets jumpy.
What would be the proper approach for this issue?
Here is my component (which is connected to redux store) that handles NavigationBar:
import React from 'react-native';
let {
Component,
Navigator,
View
} = React;
import {connect} from 'react-redux';
let NavigationBarRouteMapper = {
Title: (route, navigator, index, navState) => {
return (
<Text style={{marginTop: 15, fontSize: 18, color: colors.white}}>{this.props.navigation.title}</Text>
);
}
};
class App extends Component {
render() {
return (
<View style={{flex: 1}}>
<Navigator
ref={'navigator'}
configureScene={...}
navigationBar={
<Navigator.NavigationBar routeMapper={ NavigationBarRouteMapper } />
}
renderScene={(route, navigator) => {...}}
/>
</View>
);
}
}
export default connect(state => state)(App);
Since routeMapper is a stateless object and route seems to be the only way (best practice) to keep the state of NavigationBar.
route can be set with this.props.navigator.replace(newRoute); in child components. However, this will re-renderScene and will sometimes cause dead loop of reRendering child components. What we want is to change the NavigationBar itself without side effects.
Luckily, there's a way to keep the context of current route. Like this:
var route = this.props.navigator.navigationContext.currentRoute;
route.headerItems = {title: "Message"};
this.props.navigator.replace(route);
Refs:
How to change the title of the NavigatorIOS without changing the route in React Native
[Navigator] Binding the navigation bar with the underlying scene