showing custom component in header - react-native

On one screen, I would like to have a custom component to be shown on the header.
I tried:
const MyScreen = ({route, navigation}) => {
navigation.setOptions({
headerTitle: props => {
<MyComponent {...props} />;
},
});
...
export default MyScreen;
But it shows blank like below:
To further verify the approach is correct, I also tried to simply show a text:
navigation.setOptions({
headerTitle: props => {
<Text {...props}> Hello </Text>;
},
});
It also shows blank in the header title area. What am I missing? How to show a custom component in the header?
(MyScreen is hosted by a stack navigator)

The issue is that you are not returning the component, hence why it is not displayed.
You should either write:
const MyScreen = ({route, navigation}) => {
React.useLayoutEffect(() => {
navigation.setOptions({
headerTitle: props => {
return <MyComponent {...props} />;
},
});
}, [navigation]);
...
export default MyScreen;
or for a more concise syntax:
const MyScreen = ({route, navigation}) => {
React.useLayoutEffect(() => {
navigation.setOptions({
headerTitle: props => <MyComponent {...props} />,
});
}, [navigation]);
...
export default MyScreen;
You also need to use useLayoutEffect because you cannot update a component from inside the function body of a different component.
useLayoutEffect runs synchronously after a render but before the screen is visually updated, which will prevent your screen to flicker when your title is displayed.
By the way, since you seemingly don't need to use a function as props of your component, you could define this option directly in your navigator like so:
<Stack.Navigator>
<Stack.Screen
name="MyScreen"
component={MyScreen}
options={{ headerTitle: props => <MyComponent {...props} /> }}
/>
</Stack.Navigator>

Related

Nested Navigation Stack does not find navigator

I am want to add a secondary Navigation so that a subset of screens have access to providers, as opposed to the whole stack being wrapped by providers they do not need access to. However, I think there is something wrong with my nested setup because I get an error when clicking a button that would navigate to the substack:
ERROR The action 'NAVIGATE' with payload {"name":"VehicleDetailInspections","params":{"vehicleId":27541}} was not handled by any navigator.
Do you have a screen named 'VehicleDetailInspections'?
If you're trying to navigate to a screen in a nested navigator, see https://reactnavigation.org/docs/nesting-navigators#navigating-to-a-screen-in-a-nested-navigator.
The link to this only shows tabs & drawers, which we do not have.
My setup is:
HOMESTACK
const HomeStack = createNativeStackNavigator<HomeStackParams>();
export function HomeNavigator(): JSX.Element {
const styles = useInspectionStyles();
return (
<VehicleDataProvider>
<HomeStack.Navigator
screenOptions={{
headerStyle: styles.headerStyles,
headerBackVisible: false,
headerTitle: '',
headerLeft: () => <GoBack />,
}}
>
<HomeStack.Group
screenOptions={{
headerShadowVisible: false,
headerStyle: [styles.headerStyles],
}}
>
<HomeStack.Screen
name={VEHICLE_DETAIL}
component={VEHICLE_DETAIL.component}
/>
</HomeStack.Group>
/*
// I have tried both of these options:
<HomeStack.Group>
{() => <InspectionNavigator />}
</HomeStack.Group>
// or
<HomeStack.Screen
name={'InspectionSubStack'}
component={InspectionNavigator}
/>
*/
</HomeStack.Navigator>
</VehicleDataProvider>
);
}
In the above HomeStack, in the VEHICLE_DETAIL screen, there is a button which when clicked navigates to VEHICLE_DETAIL_INSPECTIONS which is part of the InspectionStack below.
const navigation = useNavigation<NavigationProp<InspectionStackParams>>();
...
onPress={() =>
navigation.navigate('VEHICLE_DETAIL_INSPECTIONS', {
vehicleId: vehicle.id,
})
}
INSPECTION substack:
const InspectionStack = createNativeStackNavigator<InspectionStackParams>();
export function InspectionNavigator(): JSX.Element {
const styles = useInspectionStyles();
return (
<InspectionsDataProvider>
<InspectionProgresssProvider>
<InspectionStack.Navigator
screenOptions={{
headerStyle: styles.headerStyles,
headerBackVisible: false,
headerTitle: '',
headerLeft: () => <GoBack />,
}}
>
<InspectionStack.Screen
name={VEHICLE_DETAIL_INSPECTIONS}
component={
VEHICLE_DETAIL_INSPECTIONS.component
}
/>
<InspectionStack.Screen
name={VEHICLE_INSPECTIONS_SECTION].name}
component={
VEHICLE_INSPECTIONS_SECTION.component
}
/>
<InspectionStack.Screen
name={VEHICLE_INSPECTIONS_STEP}
component={VEHICLE_INSPECTIONS_STEP.component}
/>
</InspectionStack.Navigator>
</InspectionProgresssProvider>
</InspectionsDataProvider>
);
}
What do I need to do to get the InspectionNavigator to be found by the navigate() from HomeStack?

React-Navigation v5: How to get screen's state from header

According to the React-Navigation docs (https://reactnavigation.org/docs/header-buttons/), in the header, we can interact with screen's state:
function StackScreen() {
return (
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
options={({ navigation, route }) => ({
headerTitle: props => <LogoTitle {...props} />,
})}
/>
</Stack.Navigator>
);
}
function HomeScreen({ navigation }) {
const [count, setCount] = React.useState(0);
React.useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<Button onPress={() => setCount(c => c + 1)} title="Update count" /> // <===== SET STATE
),
});
}, [navigation]);
return <Text>Count: {count}</Text>;
}
It works fine when I set screen's state from header. But when I try to get screen's state from header, it's always return the initial value of state.
<Button
onPress={() => {
setCount(c => c + 1);
alert(count)
}}
title="Update count" />
How can I get the current state of screen from header?
I've had the solution:
Problem is in the React.useLayoutEffect() function.
I need to pass [count] to the second argument of this function instead of [navigation]:
React.useLayoutEffect(()=>{
...
}, [count]);
Then, it works fine

How to navigate to a screen on back button press in React navigation 5 stack navigator

There are many similar questions that have been answered but none of them uses the newest react-navigation version.
I want to go to the 'Home' screen from the 'Document' screen on back button press. This is what I tried but it doesn't work.
<NavigationContainer>
<Stack.Navigator initialRouteName="Home" screenOptions={{ headerShown : false }}>
<Stack.Screen name="Home" component={Home} />
<Stack.Screen name="Camera" component={CameraScreen} />
<Stack.Screen name="Document" component={Document} options={{
headerLeft: (props) => (<HeaderBackButton {...props} onPress={() => props.navigation.navigate("Home")}/>)
}} />
</Stack.Navigator>
</NavigationContainer>
Edit for more clarification: My app starts with 'Home' then goes to 'Camera' then to 'Document'. Now, I don't want to go back to 'Camera' once I am at 'Document' rather straight to 'Home' when I press the phone's back button.
According to the documentation, this is how to override the back button.
<Screen
name="Home"
component={HomeScreen}
options={{
headerLeft: (props) => (
<HeaderBackButton
{...props}
onPress={() => {
// Do something
}}
/>
),
}}/>;
But I don't know how to go to 'Home' using the above code. So I searched similar questions and most of them had navigationOptions and other stuff. I tried following the below answer from a question.
import { HeaderBackButton } from 'react-navigation';
static navigationOptions = ({navigation}) => {
return{
headerLeft:(<HeaderBackButton onPress={()=>{navigation.navigate('A')}}/>)
}
}
So yeah, the back button doesn't respond even if I use console.log
You can customise back button behaviour using navigation 'beforeRemove' event, which fires before unloading a screen.
useEffect(() => {
const unsubscribe = navigation.addListener('beforeRemove', e => {
e.preventDefault(); // Prevent default action
unsubscribe() // Unsubscribe the event on first call to prevent infinite loop
navigation.navigate('Home') // Navigate to your desired screen
});
}, [])
The positive point with this approach is that it will fire on any type of back navigation, whether it's in-app back button or device back button/gesture.
Navigation Events Doc
I managed to achieve what I needed. I had to override the physical back button in 'Document'.
useFocusEffect(
useCallback(() => {
const onBackPress = () => {
navigation.pop(2); // remove two screens i.e. Document and Camera
return true // disable normal behaviour
};
BackHandler.addEventListener('hardwareBackPress', onBackPress); // detect back button press
return () =>
BackHandler.removeEventListener('hardwareBackPress');
}, [])
);
I am not sure how your code works.
<it doesn't work.> - you can give us more information.
But it's worth to try below.
import {CommonActions} from '#react-navigation/native';
props.navigation.dispatch(
CommonActions.navigate({name: 'Home'}),
);
https://reactnavigation.org/docs/navigation-prop/#dispatch
Did you try CommonActions.reset ?
something like this from the doc :
import { CommonActions } from '#react-navigation/native';
// when you want to navigate to the Documents page, instead of doing
navigation.navigate({routeName: 'Documents'});
// you can try
navigation.dispatch(
CommonActions.reset({
index: 1,
routes: [
{ name: 'Home' },
{
name: 'Documents',
params: { name: 'abc' },
},
],
})
);
So that when you're going back from Documents you're heading to the previous screen in the stack : Home.

React Navigation V5 does not receive focused parameter when using custom bottomTab component

I am currently trying to implement a custom tabBar design into my react native app which is using React Navigation 5 as the navigation library. Everything is working correctly, except that my tabBarIcons don't receive any props, so i cannot determine whether i have to show the active or inactive tabIcon. Whenever i use a default tabbar i do receive the props, so there must be something wrong in my custom tabbar. I did follow the docs though, and only find the instruction to emit the 'tabPress' event. I do however think that i should emit more events to get the correct focused prop. I have set up the navigator like this:
const Tabs = createBottomTabNavigator();
export default () => (
<Tabs.Navigator tabBar={TabBarComponent} initialRouteName="Home">
<Tabs.Screen
name="Home"
component={HomeScreen}
options={{
tabBarIcon: ({ focused }) => {
// The props here are {}, so focused is undefined.
const icon = focused
? require('images/iconOverviewRed.png')
: require('images/iconOverviewGrey.png');
return <Image source={icon} />;
},
}}
/>
<Tabs.Screen
name="Overview"
component={OverviewScreen}
options={{
tabBarIcon: props => {
console.log(props);
return <Image source={require('images/logoRed.png')} />;
},
}}
/>
<Tabs.Screen
name="Account"
component={AccountScreen}
options={{
tabBarIcon: ({ focused }) => {
const icon = focused
? require('images/iconAccountRed.png')
: require('images/iconAccountGrey.png');
return <Image source={icon} resizeMethod="resize" />;
},
}}
/>
</Tabs.Navigator>
);
And this is my custom tabBar compnent:
const TabBar = ({ navigation, state, descriptors }: any) => {
return (
<View style={styles.container}>
{state.routes.map((route: any) => {
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!event.defaultPrevented) {
navigation.dispatch({
...TabActions.jumpTo(route.name),
target: state.key,
});
}
};
return (
<TabIcon
key={route.key}
Icon={descriptors[route.key].options.tabBarIcon}
onPress={onPress}
isBig={route.name === 'Home'}
/>
);
})}
</View>
);
};
const TabIcon = ({ onPress, Icon, key, isBig }: any) => {
return (
<TouchableWithoutFeedback key={key} onPress={onPress}>
<View style={isBig ? styles.bigTab : styles.defaultTab} key={key}>
<Icon />
</View>
</TouchableWithoutFeedback>
);
};
Thanks in advance.
descriptors[route.key].options just gives you the options as you have specified them. If you log the value of descriptors[route.key].options.tabBarIcon, you'll see that it prints the function that you have specified.
In your custom tab bar, it's upto you to use the option as you need. Since it's a function here, you'll have to call it and pass desired arguments.
descriptors[route.key].options.tabBarIcon({ focused: state.index === index })
This also means that you fully control the option. You can put whatever type you'd like, function, a require statement directly etc. and then use that. You also don't have to call it tabBarIcon, you can call it whatever you want.

Sending props back to any screen using back button override

I have only been working with React-Native recently. For this reason, I would need help with the following problem:
I did override the back button action in the header of a stack navigator, to navigate to any screen. I have to "send back" props to the screen xy. I have tried it on several variants, but unfortunately get error messages (because it does not work as I want) or do not get the expected result. I would be grateful for a solution. Thank you!
static navigationOptions = ({navigation}) => ({
title: null,
headerStyle: {backgroundColor: 'transparent'},
headerLeft: <HeaderBackButton
onPress={() => {
this.props.navigation.state.params.returnData(CurrentServiceProvider,'ServiceProvider')
navigation.push('digitalCode'),{ Info1: SomeObject, Info2: SomeObject }
}}
/>
})
Edit:
Screen B:
export class ScreenB extends React.Component {
static navigationOptions = ({navigation}) => ({
title: null,
headerStyle: {backgroundColor: 'transparent'},
headerLeft: <HeaderBackButton onPress={() => {
navigation.state.params.goBackData({SomeObject, 'ObjectIdentifier'});
navigation.push('ScreenA')}
}
/>
})
}
Screen A:
export class ScreenA extends React.Component {
getBackData = (data) => console.log(data)
ForwardNextScreen(){
this.props.navigation.navigate('ScreenB', {goBackData: this.getBackData});
}
}
If you want to send data back to previous screen ,the correct way is to declare callback on navigationParams (Github issue)
ScreenA
getBackData = (data) => console.log(data)
changeScreen = () => this.props.navigation.navigate('ScreenB', { goBackData: this.getBackData })
ScreenB
this.props.navigation.state.params.goBackData({
//your data json or whatever
})
this.props.navigation.pop()
Edit
Please change your from
headerLeft: <HeaderBackButton onPress={() => {
navigation.state.params.goBackData({SomeObject, 'ObjectIdentifier'});
navigation.push('ScreenA')}
}
/>
to
headerLeft: <HeaderBackButton onPress={() => {
navigation.getParam('goBackData')({SomeObject, 'ObjectIdentifier'});
navigation.pop();
}
/>