My app is using the latest hook-based implementation of React Navigation (v5.4.0). It has a tab bar navigator that has a tab representing mail messages, with the obligatory number of messages shown in the corner of the icon. The tab code looks something like:
var messageCount = 0;
export function UpdateMessageCount(newnum:number)
{
messageCount = newnum;
}
function MessageIcon (color:string, size:number)
{
return (messageCount < 1)? (<FontAwesome style={{color:color, fontSize:size}} icon={LightIcons.envelope} pro={true} />):
(<View>
<FontAwesome style={{color:color, fontSize:size}} icon={LightIcons.envelope} pro={true} />
<View style={{position:'absolute',borderRadius:20, right:-10,top:0,backgroundColor:'red',minWidth:15, height:15, justifyContent:'center', alignItems:'center'}}>
<Text style={{fontSize:11, color:'white', fontFamily:'Roboto-Medium',marginLeft:2,marginRight:2}}>{messageCount}</Text>
</View>
</View>)
}
and
const MainTab = createBottomTabNavigator();
function MainTabNavigator() {
return (
<MainTab.Navigator
...
<MainTab.Screen name="Messages"
component={ MessageListNavigator }
options={{
tabBarIcon: ({ color, size }) => (
MessageIcon(color, size)
),
}}/>
I want the icon/tab navigator to refresh when the number of mail messages has changed (which happens asynchronously via a sync). Setting a fake option (as shown here) on the navigator will cause a tab navigator refresh, but is relying on an undocumented side effect:
UpdateMessageCount(count);
let op = this.props.navigation.dangerouslyGetParent();
op.setOptions({tickle:""})
1) Are there alternative ways of telling the tab navigator/icon to refresh? (I'm not using redux)
2) Is there a way to get the handle to a nested tab navigator from the main navigator object?
Thanks in advance for suggestions and comments.
I solved this by emitting an event when the message count changes, and catching the event in the tab navigator hook. Slightly revised code looks like:
function MessageIcon (messageCount:number, color:string, size:number)
{
return (messageCount < 1)? (<FontAwesome style={{color:color, fontSize:size}} icon={LightIcons.envelope} pro={true} />):
(<View>
<FontAwesome style={{color:color, fontSize:size}} icon={LightIcons.envelope} pro={true} />
<View style={{position:'absolute',borderRadius:20, right:-10,top:0,backgroundColor:'red',minWidth:15, height:15, justifyContent:'center', alignItems:'center'}}>
<Text style={{fontSize:11, color:'white', fontFamily:'Roboto-Medium',marginLeft:2,marginRight:2}}>{messageCount}</Text>
</View>
</View>)
}
const MainTab = createBottomTabNavigator();
function MainTabNavigator() {
const [messageCount, setMessageCount] = useState(0);
const messageCountListener = MyEvents.addListener(MyEventMessageCountUpdated,
(count)=>{
console.log("message count update: ", count)
setMessageCount(count);
});
useEffect(()=> {
return function cleanup() {
messageCountListener.remove();
}
});
return (
<MainTab.Navigator
...
<MainTab.Screen name="Messages"
component={ MessageListNavigator }
options={{
tabBarIcon: ({ color, size }) => (
MessageIcon(messageCount, color, size)
),
}}/>
Related
I wanted to connect the SearchMain, SearchHistoryList, and SearchResult screens using the custom header of the stack navigation.
At first, the SearchMain screen appeared,
and when the TextInput in the custom header is onFocus, the SearchHistory screen appeared,
and the SearchResult screen appeared when the TextInput is onSubmitEditing or when the SearchButton was pressed.
Also, I set the keyboard focus to be maintained when switching screens, and use Keyboard.discuss() when I want to close it.
However, if you enter something at the TextInput in custom header immediately after the screen changes, it will be entered at the TextInput on the previous screen.
I think it's because the three screens don't share a single custom header!! each header is created, and the keyboard focus on the previous screen!!
I want all screens use the same header, or the values of the headers are consistent.
What should I do to solve this problem?
I searched variously such as singleton header, static header, and header value consistent, but there is no good solution.
I recently started studying React Native, so I'm not sure about my method, and a completely different approach is also welcome~~
Below is my code, thanks.
const Stack = createStackNavigator();
function NavigationBar({navigation}){
const [query, setQuery] = React.useState('');
function changeSearchQuery(text : string) : void {
setQuery(text);
}
function deleteSearchQuery() : void {
setQuery("");
}
return(
<InputView>
<BackIcon onPress={() => {navigation.goBack()
Keyboard.dismiss()}}>
<Icon name="arrow-back-ios" size={widthPercentage(24)} color="#666666"/>
</BackIcon>
<InputText
keyboardType="default"
maxLength={100}
onChangeText={(str) => setQuery(str)}
value = {query}
placeholder="검색어 입력"
placeholderTextColor="#E9E9E9"
returnKeyType="search"
onFocus={() => navigation.navigate("SearchHistory", {changeSearchQuery:changeSearchQuery})}
onSubmitEditing={() => {navigation.navigate("SearchResult", {changeSearchQuery:changeSearchQuery})
Keyboard.dismiss()}}
allowFontScaling= {false}
/>
{ query.length > 0 &&
<DeleteIcon onPress={() => deleteSearchQuery()}>
<Icon name="clear" size={widthPercentage(20)} color="#666666"/>
</DeleteIcon>
}
<SearchIcon onPress={() => {navigation.navigate("SearchResult")
Keyboard.dismiss()}}>
<Icon name="search" size={widthPercentage(20)} color="#666666"/>
</SearchIcon>
<Line></Line>
</InputView>
);
}
export const Search = () =>{
return (
<Stack.Navigator initialRouteName="SearchMain" keyboardHandlingEnabled={false} screenOptions={{header:props => <NavigationBar navigation={props.navigation}/>}}>
<Stack.Screen name="SearchMain" component={SearchMain} options={{animationEnabled: false}}/>
<Stack.Screen name="SearchHistory" component={SearchHistory} options={{animationEnabled: false}}/>
<Stack.Screen name="SearchResult" component={SearchResult} options={{animationEnabled: false}}/>
<Stack.Screen name="SearchFilter" component={SearchFilter} options={{headerShown: false}}/>
</Stack.Navigator>
);
}
export default Search;
It's not a good pattern to set the state inside the header, instead you should centralized the state in your component and then use navigation.setOption to pass functions/state value.
In this way your header will look like this:
function NavigationBar({
query,
onFocus,
onChangeText,
onPressBackIcon,
onSubmitEditing,
onDeleteSearch,
onSearch
}){
return(
<InputView>
<BackIcon onPress={onPressBackIcon}>
<Icon name="arrow-back-ios" size={widthPercentage(24)} color="#666666"/>
</BackIcon>
<InputText
keyboardType="default"
maxLength={100}
onChangeText={onChangeText}
value = {query}
placeholder="검색어 입력"
placeholderTextColor="#E9E9E9"
returnKeyType="search"
onFocus={onFocus}
onSubmitEditing={onSubmitEditing}
allowFontScaling= {false}
/>
{ query.length > 0 &&
<DeleteIcon onPress={onDeleteSearch}>
<Icon name="clear" size={widthPercentage(20)} color="#666666"/>
</DeleteIcon>
}
<SearchIcon onPress={onSearch}>
<Icon name="search" size={widthPercentage(20)} color="#666666"/>
</SearchIcon>
<Line></Line>
</InputView>
);
}
Than you will have in SearchMainScreen your state, your functions and your custom header:
// remember headerMode='none' in screen option
const SearchMainScreen = () => {
const [query, setQuery] = React.useState('');
const onFocus = () => {}
...
return (
<View>
<NavigationBar
onFocus={onFocus}
query={query}
...
/>
{
(isFocus && !query) ?
<SearchHistoryComponent/> :
(isFocus && query) ?
<SearchResultComponent/> :
<List/>
}
</View>
)
}
Material Bottom Tab Navigator has the option TabBarBadge to show a little badge (string or number) on the tab icon (like a notification).
But in Top Tab Bar Navigator, this option does not exist.
In Top Tab Bar Navigator there is an option called TabBarPosition to set the position of the tab bar on top or bottom.
But in Bottom Tab Navigator, this options does not exist ...
So I'm looking for a way to have Badges on my Material Top Tab Bar, or to set the position of my Material Bottom Tab Bar to the top and be able to use TabBarBadge.
Thanks you !
I faced this problem too.This property is really not available until now (although the issue on github opened almost a year ago), so the only solution I have found is to try reinvent the wheel.
So my solution is to create a custom component.
import {MaterialTopTabBarProps} from '#react-navigation/material-top-tabs';
const CustomTabBar:React.FC<MaterialTopTabBarProps> = ({state, descriptors, navigation}) => {
return (
<View style={{ flexDirection: 'row', paddingTop: 20, backgroundColor: 'rgb(0,0,0)' }}>
{state.routes.map((route, index) => {
const
{options} = descriptors[route.key],
isFocused = state.index === index,
label =
options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: route.name,
badge = '1' // You can get this value using your api or something
return (
<Text
style={isFocused ? {opacity: 1, color: 'rgb(255,255,255)'} : {opacity: 0.5, color: 'rgb(255,255,255)'}}
onPress={() => {
navigation.navigate(route.name);
}}
>
{label}
{badge ? <Text>{badge}</Text> : null}
</Text>
);
})}
</View>
);
}
const Tab = createMaterialTopTabNavigator();
export const MainPage = ({navigation}: MainPageProps) =>{
return (
<Tab.Navigator
tabBar = {(props:MaterialTopTabBarProps) => {
return(
<CustomTabBar {...props} />
);
}}
>
<Tab.Screen name="Chats" component={ChatsScreen}/>
<Tab.Screen name="Albums" component={AlbumsScreen} />
</Tab.Navigator>
);
}
react-native version 0.62
I am making a quiz app and on every new question(a new quiz screen, QuestionScreen.js), I want to update a label in the navigation bar which is declared in App.js
In App.js, the problem here is I don't know how to pass the state questionIndex to HomeScreen or QuestionScreen, normally I thought this would be using props like <Question questionIndex={questionIndex}>. I tried to pass them in options but that doesn't seem to work...
const App = () => {
const [questionIndex, setQuestionIndex] = useState(0);
return (
<NavigationContainer>
<Stack.Navigator initialRouteName='Home'
>
<Stack.Screen
name='Home'
component={HomeScreen}
options={{
title:''
}}
/>
<Stack.Screen
name='QuestionScreen'
component={QuestionScreen}
options={({ navigation }) => ({
headerBackTitleVisible: false,
headerRight: ()=>(<HeaderRight pageIndex={questionIndex}/>),
headerRightContainerStyle: {
alignItems: "flex-end",
paddingRight: 25
},
})}
/>
function HeaderRight(props) {
return (
<Text>{props.pageIndex}/{questions.length-1}</Text>
);
}
const HomeScreen = ({ navigation }) => {
return (
<View style={styles.container} >
<ScrollView>
<TouchableOpacity onPress={()=>
//here I'm unable to retrieve the questionIndex props.
//tried navigation.getParams("questionIndex") or route.params
navigation.navigate('QuestionScreen', {
pageIndex: 0
})
}>
In QuestionScreen.js
function QuestionScreen({route, navigation, options}) {
const { pageIndex } = route.params;
[Full code here on github]https://github.com/liaoxsong/expofirst
I am a newbie to react-native and I come from native mobile development, this Stack.Navigator api looks weird, can't I just manage the navigation header from the screen component itself? That would make things easier.
it is difficult to customize the default header given by react navigation, what you can do is disabling the header and creating your own custom header.
disable header by giving prop headerMode={"none"} in createStackNavigator and have your own custom header on top,
to disable header docs : https://reactnavigation.org/docs/stack-navigator/#headermode
I have a React Native StackNavigator like so:
const AppStack = () => {
return (
<NavigationContainer theme={{ colors: { background: "white" }}}>
<Stack.Navigator headerMode="screen">
<Stack.Screen name="Master" component={ Master } />
<Stack.Screen
name="Details"
component={ Details }
options={{ headerTitle: props => <Header {...props} /> }} // <-- how can I pass props from Master to the Header here
/>
</Stack.Navigator>
</NavigationContainer>
)
}
The navigation works fine. When I'm on Master, if I press a TouchableOpacity, it brings up Details, with the header component Header.
However, what I want is to pass props from Master to the Header component in Details.
Something like this:
{/* What I want is that onPress, I want to pass someones_name and someones_photo_url to
the Details' Header component */ }
const Master = () => {
const someones_name = "Steve";
const someones_photo_url = "http://somephoto.com/001.jpg";
return (
<TouchableOpacity onPress={() => navigation.navigate("Details")}>
<Text>{ someones_name }</Text>
<Image source={{ uri: someones_photo_url }}>
</TouchableOpacity>
)
}
Is this possible?
You can pass values to other screens when navigating by doing the following:
Assume I am on screen "Feed" and click on someones name thus I want to go to screen "Profile" where it will show that user name that I clicked on.
In screen Feed
props.navigation.navigate('Profile, { userName: 'Shane' })`
In screen Profile I can grab that value passed in via navigate with:
const { userName } = props.route.params
To set this up inside a header assuming your screen options are outside the screen component and cannot access the props. Look into using setOptions to configure your header title dynamically.
props.navigation.setOptions({
headerTitle: ...
})
I'm using React Navigation. I'm directing you to the conversation after the record insert page. I provide automatic renewal when I redirect. I'm sending a parameter. But that doesn't work. I don't know where I'm making a mistake.
I get this error.
Uncaught typeerror. TypeError: s.handleRefresh is not a function. (In 's.handleRefresh()', 's.handleRefresh' is undefined
LeadDetail.js Page
static navigationOptions = ({ navigation }) => {
const { params = {} } = navigation.state;
return {
title: 'Müşteri Adayı Detay',
headerRight: (
<TouchableHighlight activeOpacity={0.4} underlayColor='transparent' onPress={() => navigation.navigate('CreateMeeting', { parentId: params.id, parentType: 'Lead' })} style={{marginRight: 12 }}>
<Icon
name="calendar-check-o"
size={25}
color="white"
/>
</TouchableHighlight>
)
};
};
CreateMeeting.js Page
dataSuccess(response) {
this.setState({ buttonLoading: false });
Alert.alert(
'Tebrikler!',
'Meeting başarılı bir şekilde kayıt edildi',
[
{ text: 'Tamam', onPress: () => this.props.navigation.navigate('Meeting', { refreshMeeting: true }) }
]
);
}
Meeting.js Page
static navigationOptions = ({ navigation }) => {
const { params } = navigation.state;
let refresh = navigation.getParam('refreshMeeting', false);
if(refresh) {
params.handleRefresh();
}
return {
title: 'Görüşmeler',
headerRight: (
<View style={{ flexDirection: 'row' }}>
<TouchableHighlight activeOpacity={0.4} underlayColor='transparent' onPress={navigation.getParam('setOverlay')}>
<Icon
name="filter"
size={25}
color="white"
/>
</TouchableHighlight>
<TouchableHighlight activeOpacity={0.4} underlayColor='transparent' onPress={navigation.getParam('setModalVisible')} style={{ marginRight: 12, marginLeft: 20 }}>
<Icon
name="plus"
size={25}
color="white"
/>
</TouchableHighlight>
</View>
)
};
};
Are you setting the param handleRefresh? Instead of params.handleRefresh(); shouldn't it be params.refresh();?
Also, s.handleRefresh where is this in your code? I think there is a typo or something like that and I'm considering that s.handleRefresh is actually params.handleRefresh
Please edit your question if none of this is correct.
I don't know where you are setting the param handleRefresh, but if you are setting it (please check that and add the code where you do that), maybe here is the reason why this happens.
If you look at the docs they have a not
React Navigation doesn't guarantee that your screen component will be mounted before the header. Because the increaseCount param is set in componentDidMount, we may not have it available to us in navigationOptions. This usually will not be a problem because onPress for Button and Touchable components will do nothing if the callback is null. If you have your own custom component here, you should make sure it behaves as expected with null for its press handler prop.
This means that the header can render before the component calls setParams and that doens't load you params in the first time. What you need to do some checking to avoid that time when the header renders first.
// added some checking
if(refresh && params && params.handleRefresh) {
params.handleRefresh();
}