Question about custom header in React Navigation v5 (Not clickable back button) - reactjs

Hi I am using React navigation v5.
I am trying to implement custom header for specific screen. So my custom header looks like this
CustomHeader.js
export function CustomHeader({props}) {
const {scene, previous, navigation} = props;
const opacity = scene.route.params.opacity;
return (
<React.Fragment>
<Animated.View style={[styles.headerStyle, {opacity}]}>
<View style={styles.influencerNameContainer}>
<Text style={styles.influencerName}>
{scene.route.params.influencer.user.name}
</Text>
</View>
</Animated.View>
{previous ? (
/* This is a back button */
<Button
style={[styles.iconButton, {left: 0}]}
icon={BackIcon}
onPress={() => {navigation.goBack}
/>
) : (
undefined
)}
</React.Fragment>
);
}
Navigator looks like this
export function HomeStack() {
return (
<Stack.Navigator
initialRouteName="Home"
headerMode="screen"
style={{backgroundColor: 'yellow'}}>
<Stack.Screen
name="InfluencerScreen"
component={InfluencerScreen}
options={{
header: props => <CustomHeader props={props} />,
}}
/>
</Stack.Navigator>
);
}
It renders custom headers and custom back button but back button is not clickable.
So I tried.
<Stack.Screen
name="InfluencerScreen"
component={InfluencerScreen}
options={{
header: props => <CustomHeader props={props} />,
headerLeft: () => (
<Button title="Back Button" onPress={() => alert('Pressed')} />
)
}}
/>
With this code, it doesn't show back button at all with custom header. But it shows custom back button when I remove custom header(header: props => ).
What am I missing?

You're showing a custom header, how you render the button is up-to you. React Navigation cannot show a back button since React Navigation is no longer rendering the header.
Instead of using headerLeft option, you need to put the back button inside your custom header.
Also your destructuring is incorrect. function CustomHeader({props}) should be function CustomHeader(props) (without the curly braces).

You're destructuring props twice.. Try:
export function CustomHeader({scene, previous, navigation}) {
// const {scene, previous, navigation} = props; // remove this
...
}
Edit:
Also goBack is a method so call it with parentheses:
onPress={() => navigation.goBack()} // remove unnecessary `curly braces`

Related

React Native TextInput onFocus does not allow onPress of other child components

Nested TextInput component does not allow other components' onPress function to be called. Only when the TextInput is not focused, the onPress works fine.
React Native Version : 0.66.3
Here is my code
export const Component = (props): JSX.Element {
const { searchText, onChangeSearchText, onClearSearchText } = props
const [searchViewFocused, setSearchViewFocused] = useState<boolean>(false)
const searchIcon = searchViewFocused ? "searchActive" : "searchInactive"
const cancelIcon = searchViewFocused ? "cancelActive" : "cancelInactive"
return (
<View style={styles.searchBoxContainer}>
<View style={[styles.searchBox, searchViewFocused && styles.searchBarActive]}>
<Icon styles={styles.searchIcon} icon={searchIcon} />
<TextInput
style={styles.searchInput}
onChangeText={onChangeSearchText}
value={searchText}
onFocus={() => setSearchViewFocused(true)}
onBlur={() => setSearchViewFocused(false)}
autoCompleteType={"off"}
numberOfLines={1}
/>
{searchText !== "" && (
<Pressable style={styles.clearIcon} onPress={onClearSearchText}>
<Icon icon={cancelIcon} />
</Pressable>
)}
</View>
</View>
)
})
Attached are the images of
Expected.
VS
The issue
When the cross icon is pressed on focused state, the textInput is unfocused rather What I am trying to achieve is that the search text gets cleared & the input remains focused.
Note: The onPress works perfectly fine when input is not focused
Any help is appreciated. Thanks!
PS: Tried TouchableOpacity & also I have tried wrapping the components inside a ScrollView to use keyboardShouldPersistTaps='handled' as mentioned in one of the SO answer but to no success.
Found a workaround to this is to wrap the whole component into a ScrollView and adding the prop keyboardShouldPersistTaps='handled'
Previously I was making the View inside the Component as ScrollView and adding keyboardShouldPersistTaps='handled' which did not work
export const Component = (props): JSX.Element {
...
return (
<ScrollView contentContainerStyle={styles.searchBoxContainer}
keyboardShouldPersistTaps='handled'>
...
</ScrollView>
)
})
The key was to wrap the entire component inside the ScrollView,
Here's what worked:
<ScrollView keyboardShouldPersistTaps='handled'>
<Component {...props}/>
</ScrollView>
Guess this was a silly mistake, but worth pointing out!
You're setting the focus of the entire component based on whether the TextInput has focus. Since the clear button is outside the text input, pressing it causes the component to lose focus.
One solution is to store the TextInput instance in a ref. When the clear button is pressed, you can refocus the text input. I've copied your component below and added some new lines, which are marked in comments.
export const Component = (props): JSX.Element {
const { searchText, onChangeSearchText, onClearSearchText } = props
const textInputRef = useRef(null); // new
const [searchViewFocused, setSearchViewFocused] = useState<boolean>(false)
const searchIcon = searchViewFocused ? "searchActive" : "searchInactive"
const cancelIcon = searchViewFocused ? "cancelActive" : "cancelInactive"
const onClear = () => {
onClearSearchText();
textInputRef.current?.focus();
} // new
return (
<View style={styles.searchBoxContainer}>
<View style={[styles.searchBox, searchViewFocused && styles.searchBarActive]}>
<Icon styles={styles.searchIcon} icon={searchIcon} />
<TextInput
ref={textInputRef} // new
style={styles.searchInput}
onChangeText={onChangeSearchText}
value={searchText}
onFocus={() => setSearchViewFocused(true)}
onBlur={() => setSearchViewFocused(false)}
autoCompleteType={"off"}
numberOfLines={1}
/>
{searchText !== "" && (
<Pressable style={styles.clearIcon} onPress={onClear}> // calls new function
<Icon icon={cancelIcon} />
</Pressable>
)}
</View>
</View>
)
})

How can I open BottomSheetModal (#gorhom/bottom-sheet) in different component, using React-Native (Expo)

I am using https://gorhom.github.io/react-native-bottom-sheet/.
I was wondering how can I open "BottomSheetModal" in a different file e.g Navbar Component.
This is what my code looks like at the moment to open the Bottom Sheet inside of the same component.
const BottomSheetModal: FC = () => {
const bottomSheetModalRef = useRef<BottomSheet>(null);
const snapPoints = useMemo(() => ["25%", "50%"], []);
const handlePresentModalPress = useCallback(() => {
bottomSheetModalRef.current?.present();
}, []);
return (
<>
<Button title="Test" onPress={() => handlePresentModalPress()} />
<BottomSheet
index={1}
style={{ ...shadows.bottomSheet }}
ref={bottomSheetModalRef}
snapPoints={snapPoints}>
<View style={styles.container}>
<Text>Awesome 🎉</Text>
</View>
</BottomSheet>
</>
);
};
So how can I use the opening code of the Bottom Sheet inside of my Navbar Component?
Navbar Component:
// Open BottomSheet here
<TouchableWithoutFeedback onPress={() => openBottomSheet()}>
<View>
<Image
style={styles.avatar}
source={{
uri: "https://lumiere-a.akamaihd.net/v1/images/character_themuppets_kermit_b77a431b.jpeg?region=0%2C0%2C450%2C450",
}}
/>
</View>
</TouchableWithoutFeedback>
Thank you!
I found out to do this, incase anyone comes across this question, I'll post it here!
So what you have to do is pass the ref to the bottom sheet component. So in the Navbar component I created the ref for the bottom sheet, and then passed it into the bottom sheet.
Navbar:
// Create Ref
const userBottomSheetRef = useRef<BottomSheetModal>(null);
// Pass ref into the bottom sheet component
<BottomSheet ref={userBottomSheetRef} snapPoints={["30%"]}/>
Then inside the bottom sheet component you forward the ref using a react function, and then pass it in as normal:
<BottomSheetModal ref={ref} >
<BottomSheetScrollView>
<View style={styles.container}>{children}</View>
</BottomSheetScrollView>
</BottomSheetModal>

React Navigation v5: How to write a navigation route for a custom Navigation header button?

I am trying to add a "Gear" button on headerRight of Navigation header for all screens of my app. I added the button as ScreenOptions of Stack.Navigator inside a NavigationContainer in my App.js so that all Stack.Screens inside this Stack will have this button on header.
Now I need this button press to navigate to another screen (settingsScreen). I cannot add navigation.navigate('settingsScreen') onto the onPress event of the button like I do from the screens because there is no navigation prop available in the App.js file. Here is my code snippet.
const Stack = createStackNavigator();
const myStack = () => {
return (
<NavigationContainer>
<Stack.Navigator
screenOptions={{
headerRight: () => (
<Button
title='Gears'
onPress={() => {}} // no navigation prop available on this file
/>
),
}}
>
<Stack.Screen
name='Home'
component={HomeScreen}
options={{ title: 'Home' }}
/>
<Stack.Screen
name='Add new expense'
component={AddNewExpense}
/>
<Stack.Screen
name='Settings'
component={SettingsScreen}
/>
</Stack.Navigator>
</NavigationContainer>
);
}
How can this be achieved? Thanks in advance!
screenOptions will receive the navigation prop and the route prop for each screen see here
screenOptions={({ navigation}) => ({
//you can use navigation now
headerRight: () => (
<Button
title='Gears'
onPress={() => navigation.navigate('settingsScreen')}
/>
),
})}

React navigation drawer doesn't open consistently when dynamically setting drawer position using the Context API

I'm trying to implement a single drawer whose drawer position and content can be dynamically changed.
I have a drawer navigator with a stack navigator inside. The header of the stack navigator has two buttons. The left button sets the drawerPosition to "left" and calls navigations.openDrawer() and the right button sets the drawerPosition to "right" and calls navigation.openDrawer().
My current implementation looks like this:
const Stack = createStackNavigator();
const Drawer = createDrawerNavigator();
const DrawerPositionContext = React.createContext([{}, () => {}]);
const DrawerPositionProvider = (props) => {
const [drawerPosition, setDrawerPosition] = useState({});
return (
<DrawerPositionContext.Provider value={[drawerPosition, setDrawerPosition]}>
{props.children}
</DrawerPositionContext.Provider>
);
};
function CustomDrawerContent(props) {
return (
<DrawerContentScrollView {...props}>
{props.drawerPosition === 'left' ? (
<DrawerItem label="Left" />
) : (
<DrawerItem label="Right" />
)}
</DrawerContentScrollView>
);
}
const Screen1 = () => {
return <Text>Screen1</Text>;
};
const useDrawerPosition = () => {
const [drawerPosition, setDrawerPosition] = React.useContext(
DrawerPositionContext
);
return {
drawerPosition,
setDrawerPosition
};
};
const DrawerNavigator = () => {
const { drawerPosition, setDrawerPosition } = useDrawerPosition();
return (
<Drawer.Navigator
drawerPosition={drawerPosition}
drawerContent={(props) => (
<CustomDrawerContent {...props} drawerPosition={drawerPosition} />
)}
>
<Drawer.Screen name="stack navigator" component={StackNavigator} />
</Drawer.Navigator>
);
};
const StackNavigator = ({ navigation }) => {
const { setDrawerPosition } = useDrawerPosition();
return (
<Stack.Navigator
screenOptions={{
headerLeft: () => (
<Button
title="left"
onPress={() => {
setDrawerPosition("left");
navigation.openDrawer();
}}
/>
),
headerRight: () => (
<Button
title="right"
onPress={() => {
setDrawerPosition("right");
navigation.openDrawer();
}}
/>
)
}}
>
<Stack.Screen name="screen1" component={Screen1} />
</Stack.Navigator>
);
};
export default function App() {
return (
<DrawerPositionProvider>
<NavigationContainer>
<DrawerNavigator />
</NavigationContainer>
</DrawerPositionProvider>
);
}
So I use react context to share and update the current drawer position.
The behavior I'm experiencing is that opening the left drawer will always open the drawer correctly, but opening the right drawer will not open the drawer correctly most of the time. Instead of the drawer I only see the backdrop shadow.
snack
My first guess was that the context isn't updated before the drawer is opened, but converting the components to class-based components and using a setState callback gave the same result, so I'm not sure what is happening here.
I know the usual implementation for doing something like this is to create two drawers nested in a certain way, but it is it possible to do it with the approach I've tried?
Update
I think this is a bug. The problem seems to be inside Drawer.tsx (https://github.com/react-navigation/react-navigation/blob/main/packages/drawer/src/views/Drawer.tsx).
I'm not that familiar with Animated (https://reactnative.dev/docs/animated), but I think the problem is this code in componentDidUpdate:
if (prevProps.drawerPosition !== drawerPosition) {
this.drawerPosition.setValue(
drawerPosition === 'right' ? DIRECTION_RIGHT : DIRECTION_LEFT,
);
}
From messing around with the snack you linked, it seems like the context is actually updated. The problem only appears if you switch from "right" to "left". (This is somewhat supported by what I saw in the inspector, but I didn't dig deep).
See this updated snack that logs the position as it updates/bubbles and defaults to the "right" position. If you click the right button first, you can see it working, but if you click left it will break.
All in all, this is a reactnavigation bug IMHO. This is a problem that only seems to appear with drawerType="front", so as a workaround you could try adding a drawerType="back" prop to Drawer.Navigator.

Passing data between React Native screens (From Screen A to Screen B to Screen C)

Consider the following scenario.
Basically we have a MaterialTopTabNavigator nested within a StackNavigator.
How do I pass my data from the function all the way to the MaterialTopTabNavigator. Note that MaterialTopTabNavigator has to first go through StackNavigator
file1.js
const[test,setTest] = useState('testing');
function moveToResults(){
navigation.navigate('Results', test)
}
Here we have a simple function which makes the app navigate to a different screen and it passes the state of test. When moveToResults() is called it takes us to:
file2.js // Note that PostFun is the main page of the StackNavigator.
const Tab = createMaterialTopTabNavigator();
function PostFun({navigation}) {
return (
<Tab.Navigator
initialRouteName="Feed"
activeColor="#e91e63"
tabBarOptions={{
labelStyle: {fontSize:12, color:"#faa19b"},
tabStyle: {height:65, justifyContent: "flex-end"},
}}
style={styles.header}>
<Tab.Screen name="Activity" component={FindFunActivity} />
<Tab.Screen name="Event" component={FindFunEvent} />
</Tab.Navigator>
);
}
File2.js is the beginning of the `MaterialTopTabNavigator, as you can see this navigation has two top tabs (Activity and Event).
One of the tabs might look something like and this is where I need my variable to show up:
file3.js
const FindFunEvent = (navigation) =>{
return(
<View>
<Text>{navigation.getParam()}</Text>
</View>
)
}
The question
How do I get my test variable to display in the FindFunEvent component?
This is much harder to explain than anything, here a visual if you'd prefer.
Here is the link to a short video that goes over the issue
You can get this working, passing the value as component's prop:
file1.js
const[test, setTest] = useState('testing');
function moveToResults(){
navigation.navigate('Results', { test: test })
}
file2.js
const Tab = createMaterialTopTabNavigator();
function PostFun({ route, navigation }) {
const { test } = route.params;
return (
<Tab.Navigator
initialRouteName="Feed"
activeColor="#e91e63"
tabBarOptions={{
labelStyle: {fontSize:12, color:"#faa19b"},
tabStyle: {height:65, justifyContent: "flex-end"},
}}
style={styles.header}
>
<Tab.Screen name="Activity">
{ (props) => <FindFunActivity {...props} myProp='test' /> }
</Tab.Screen>
<Tab.Screen name="Event" component={() => <FindFunEvent test={test} />} />
</Tab.Navigator>
);
}
file3.js
const FindFunEvent = ({ test }) => {
return(
<View>
<Text>{ test }</Text>
</View>
)
}
Similar example can be viewed here:
https://snack.expo.io/UDHVnSUwK
Lets say you want to navigate and pass the value you got in Screen A to screen B and screen C.
You can do something like this
ScreenA
this.props.navigation.navigate('ScreenB',
{value: 'hi', });
}
Screen B
this.props.navigation.navigate('ScreenC',
this.props.route.params.value);
}
Screen C
this.props.route.params.value
One simple way to do this would be use to a global.data. When you use a global variable you can use it anywhere within the app. This is not ideal though, as it is not best practice in programming to use global. I am not entirely sure if what you are trying to is possible, if it is not, you could resort to using global.

Resources