Setting NavigationBar's title asynchronously - react-native

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

Related

React Native Scrollview: scroll to top on button click

So I have a component with ScrollView which contains many elements, so you have to scroll a long way down.
Now there should be a button at the bottom of the page that on click will scroll the page back to top.
I already created the button as a FAB (floating action button) in an extra component.
It is integrated in a parent component, where the ScrollView is located.
What I found was that you have to create a ref in the ScrollView component and implement a button right there that uses this ref to make scrolling work. Simplified, here is what I have so far:
imports ...
const ParentComponent: React.FC<Props> = () => {
const scroll = React.createRef();
return (
<View>
<ScrollView ref={scroll}>
<SearchResult></SearchResult> // creates a very long list
<FloatingButton
onPress={() => scroll.current.scrollTo(0)}></FloatingButton>
</ScrollView>
</View>
);
};
export default ParentComponent;
As you can see, there is the component FloatingButton with the onPress() method.
Here is the implementation:
import React, {useState} from 'react';
import {Container, Content, Button, Icon, Fab} from 'native-base';
const FloatingButton: React.FC<Props> = () => {
return (
<Fab
position="bottomRight"
onPress={(???}>
<Icon name="arrow-round-up" />
</Fab>
);
};
export default FloatingButton;
Now the problem: Where should I do the onPress() method? Because if I leave it in the parent component, it won't work because it is not directly located in the Fab (in FloatingButton). I would like to do the onPress() logic in Fab, but if I do so, the ScrollView that it needs is not available, because it's in the parent component. My idea was to maybe passing the ref as prop into FloatingButton, but for some reason this didn't work.
Can someone please help me?
You could either let the parent hook into the FloatingButton's onPress function or pass the ref down to the FloatingButton directly.
export const Parent : FC<ParentProps> = props => {
const scrollRef = useRef<ScrollView>();
const onFabPress = () => {
scrollRef.current?.scrollTo({
y : 0,
animated : true
});
}
return (
<View>
<ScrollView ref={scrollRef}>
{/* Your content here */}
</ScrollView>
<FloatingButton onPress={onFabPress} />
</View>
);
}
export const FloatingButton : FC<FloatingButtonProps> = props => {
const { onPress } = props;
const onFabPress = () => {
// Do whatever logic you need to
// ...
onPress();
}
return (
<Fab position="bottomRight" onPress={onFabPress}>
<Icon name="arrow-round-up" />
</Fab>
);
}
You should determine the horizontal or vertical value you want to scroll to, like this code snippet.
onPress={()=>
this.scroll.current.scrollTo({ x:0, y:0 });
}
Please have a look at my snack code. Hope it might be helpful for you.
https://snack.expo.io/#anurodhs2/restless-edamame

React Navigation custom navigator transitions

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.

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.

React Native - how to scroll a ScrollView to a given location after navigation from another screen

Is it possible to tell a ScrollView to scroll to a specific position when we just navigated to the current screen via StackNavigator?
I have two screens; Menu and Items. The Menu is a list of Buttons, one for each item. The Items screen contain a Carousel built using ScrollView with the picture and detailed description of each Item.
When I click on a button in the Menu screen, I want to navigate to the Items screen, and automatically scroll to the Item that the button represent.
I read that you can pass in parameters when using the StackNavigator like so: but I don't know how to read out that parameter in my Items screen.
navigate('Items', { id: '1' })
So is this something that is possible in React Native and how do I do it? Or perhaps I'm using the wrong navigator?
Here's a dumbed down version of my two screens:
App.js:
const SimpleApp = StackNavigator({
Menu: { screen: MenuScreen},
Items: { screen: ItemScreen }
}
);
export default class App extends React.Component {
render() {
return <SimpleApp />;
}
}
Menu.js
export default class Menu extends React.Component {
constructor(props){
super(props)
this.seeDetail = this.seeDetail.bind(this)
}
seeDetail(){
const { navigate } = this.props.navigation;
navigate('Items')
}
render(){
<Button onPress={this.seeDetail} title='1'/>
<Button onPress={this.seeDetail} title='2'/>
}
}
Items.js
export default class Items extends React.Component {
render(){
let scrollItems = [] //Somecode that generates and array of items
return (
<View>
<View style={styles.scrollViewContainer}>
<ScrollView
horizontal
pagingEnabled
ref={(ref) => this.myScroll = ref}>
{scrollItems}
</ScrollView>
</View>
</View>
)
}
}
P.S I am specifically targeting Android at the moment, but ideally there could be a cross-platform solution.
I read that you can pass in parameters when using the StackNavigator like so: but I don't know how to read out that parameter in my Items screen.
That is achieved by accessing this.props.navigation.state.params inside your child component.
I think the best time to call scrollTo on your scrollview reference is when it first gets assigned. You're already giving it a reference and running a callback function - I would just tweak it so that it also calls scrollTo at the same time:
export default class Items extends React.Component {
render(){
let scrollItems = [] //Somecode that generates and array of items
const {id} = this.props.navigation.state.params;
return (
<View>
<View style={styles.scrollViewContainer}>
<ScrollView
horizontal
pagingEnabled
ref={(ref) => {
this.myScroll = ref
this.myScroll.scrollTo() // !!
}>
{scrollItems}
</ScrollView>
</View>
</View>
)
}
}
And this is why I use FlatLists or SectionLists (which inherit from VirtualizedList) instead of ScrollViews. VirtualizedList has a scrollToIndex function which is much more intuitive. ScrollView's scrollTo expects x and y parameters meaning that you would have to calculate the exact spot to scroll to - multiplying width of each scroll item by the index of the item you're scrolling to. And if there is padding involved for each item it becomes more of a pain.
Here is an example of scroll to the props with id.
import React, { Component } from 'react';
import { StyleSheet, Text, View, ScrollView, TouchableOpacity, Dimensions, Alert, findNodeHandle, Image } from 'react-native';
class MyCustomScrollToElement extends Component {
constructor(props) {
super(props)
this.state = {
}
this._nodes = new Map();
}
componentDidMount() {
const data = ['First Element', 'Second Element', 'Third Element', 'Fourth Element', 'Fifth Element' ];
data.filter((el, idx) => {
if(el===this.props.id){
this.scrollToElement(idx);
}
})
}
scrollToElement =(indexOf)=>{
const node = this._nodes.get(indexOf);
const position = findNodeHandle(node);
this.myScroll.scrollTo({ x: 0, y: position, animated: true });
}
render(){
const data = ['First Element', 'Second Element', 'Third Element', 'Fourth Element', 'Fifth Element' ];
return (
<View style={styles.container}>
<ScrollView ref={(ref) => this.myScroll = ref} style={[styles.container, {flex:0.9}]} keyboardShouldPersistTaps={'handled'}>
<View style={styles.container}>
{data.map((elm, idx) => <View ref={ref => this._nodes.set(idx, ref)} style={{styles.element}}><Text>{elm}</Text></View>)}
</View>
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flexGrow: 1,
backgroundColor:"#d7eff9"
},
element:{
width: 200,
height: 200,
backgroundColor: 'red'
}
});
export default MyCustomScrollToElement;
Yes, this is possible by utilising the scrollTo method - see the docs. You can call this method in componentDidMount. You just need a ref to call it like: this.myScroll.scrollTo(...). Note that if you have an array of items which are all of the same type, you should use FlatList instead.
For iOS - the best way to use ScrollView's contentOffset property. In this way it will be initially rendered in a right position. Using scrollTo will add additional excess render after the first one.
For Android - there is no other option rather then scrollTo

Managing state in react-native when using react-navigation

I am trying to create a to-do app in react-native. It has some some extra functionality such as editing existing tasksand viewing task history. I have a two StackNavigators nested inside a TabNavigator - but I'm struggling to find out exactly how I should be passing state into my application, and then once the state/data is available with the app, how I should then update the application state.
Here is the relevant code for my App:
export default class App extends React.Component {
constructor(props){
super(props)
this.state = {
data: [
//Array of to-do objects here.
]
}
}
updateAppData(id, ){
//define function that wraps this.setState(). This will be passed into
application to update application state.
}
render(){
return(
<Root screenProps={this.state}/>
)
}
}
//make Todos Stack-Nav section to sit inside tab1
const TodosWrapper = StackNavigator({
CurrentTodos: { screen: CurrentTodos },
Details: { screen: Details},
EditTodo: { screen: EditTodo},
AddNewTodo: { screen: AddNewTodo}
})
//make History Stack-Nav section to sit inside tab2
const HistoryWrapper = StackNavigator({
CompletedTodos: { screen: CompletedTodos },
CompletedDetails: { screen: CompletedDetails }
})
//Make main tab navigation that wraps tabs 1 and 2
const Root = TabNavigator({
Todos : {
screen: TodosWrapper,
title: "Open Tasks"
},
History: { screen: HistoryWrapper }
})
I then want to pass the state of my App into my screens, such as the one below:
export default class CurrentTodos extends React.Component {
static navigationOptions = {
header: null
}
render(){
const { navigate } = this.props.navigation;
let { data } = this.props.screenProps;
data = data.map( (item, id) => (
<ListItem style={styles.listItem} key={"_"+id}>
<View style={styles.textWrapper}>
<Text style={styles.listItemTitle}>{item.title}</Text>
<Text note>{ item.isComplete ? "Complete" : "In Progress" }</Text>
<Text note>{item.description}</Text>
</View>
<View>
<Button
onPress={() => this.props.navigation.navigate('Details')}
title="Full Details"
style={styles.fullDetails}
/>
</View>
</ListItem>
)
);
return (
<ScrollView style={styles.scrollView}>
<View style={styles.viewHeader}>
<Text style={styles.title}>
Current Tasks
</Text>
<Text
style={styles.addNew}
onPress={() => navigate("AddNewTodo")}
>
+
</Text>
</View>
<List>
{data}
</List>
</ScrollView>
)
}
}
I am currently planning to pass the state into my screens using screenProps={this.state} and then also passing a function that wraps this.setState() so that screen components can update the app's state.
As I am a newbie to all this, I want to make sure I don't pick up poor practices.
Could anyone help me out as to whether or not this is the correct approach? The huge amount of documentation is overwhelming at times! Thanks in advance!
If you want a centralized state, you should definitely give redux a try. Redux is a global immutable state manager.
Summarizing, in redux all your state is stored into a "Store", which wraps your top navigator. This store is composed by many "Reducers" which are the ones holding the current state of your application. Your Components communicate with the store using actions, which are received by the reducers and those communicate the new state to your views and handle the required changes.
Tell me if you need me to add more details. And here you have some references for you to check:
Official Site: http://redux.js.org
Tutorials from the creator: https://egghead.io
Courses: https://www.udemy.com/the-complete-react-native-and-redux-course/
This should get you in the right direction!
Good luck!