How to make custom useTabDoublePressEffect() in React Navigation? - react-native

How can I call a callback function when I press on a tab in Tab.Navigator which is already selected?

The custom useTabDoublePressEffect hook:
import {EventArg, useNavigation, useRoute} from '#react-navigation/native';
import React from 'react';
export default function useTabDoublePressEffect(callback: Function) {
const navigation = useNavigation();
const route = useRoute();
React.useEffect(() => {
let current = navigation; // The screen might be inside another navigator such as stack nested in tabs
// We need to find the closest tab navigator and add the listener there
while (current && current.getState().type !== 'tab') {
current = current.getParent();
}
if (!current) {
return;
}
const unsubscribe = current.addListener(
// We don't wanna import tab types here to avoid extra deps
// in addition, there are multiple tab implementations
// #ts-expect-error
'tabPress',
(e: EventArg<'tabPress', true>) => {
// We should scroll to top only when the screen is focused
const isFocused = navigation.isFocused(); // In a nested stack navigator, tab press resets the stack to first screen
// So we should scroll to top only when we are on first screen
const isFirst =
navigation === current ||
navigation.getState().routes[0].key === route.key;
// Run the operation in the next frame so we're sure all listeners have been run
// This is necessary to know if preventDefault() has been called
requestAnimationFrame(() => {
if (isFocused && isFirst && !e.defaultPrevented) {
callback();
}
});
},
);
return unsubscribe;
}, [navigation, route.key]);
}
Basic example for how to use it:
import React, {useState} from 'react';
import {Text} from 'react-native';
import useTabDoublePressEffect from '../hooks/useTabDoublePressEffect';
export default function SomeComponent() {
const [text, setText] = useState('Hello');
useTabDoublePressEffect(() => setText('Bye'));
return <Text>{text}</Text>;
}
Code borrowed from:
https://github.com/react-navigation/react-navigation/blob/ac24e617af10c48b161d1aaa7dfc8c1c1218a3cd/packages/native/src/useScrollToTop.tsx

Related

Is there - a drawer-equivalent to the property tabPress react-navigation

I want to execute a certain code whenever a drawer element is clicked.
In other words, I am looking for a drawer equivalent to the property tabPress.
I tried adding my callback to the property focus. However, this callback is executed whenever "any screen" in the relevant stack is "in focus."
I want the code to be exuted only if the user "clicks" on a "drawer item". Is this at all possible?
import { createDrawerNavigator } from "#react-navigation/drawer";
const MyNav = createDrawerNavigator();
<MyNav.Navigator>
<MyNav.Screen name="abc" component={abcStack}
listeners={ ({ navigation, route }) => ({
// This is executed whenever any screen in the stack is accessed. This does NOT satisfy my requirements.
focus: (e) => {
console.log("We are in focus");
},
// Is there something like "tabPress" for Drawer stacks?
tabPress: (e) => {
console.log("We are on tabPress");
},
})}
/>
</MyNav.Navigator>
This question is related to React-Navigation Version 6.
I found a workaround, which should work in many cases. The idea is to send a parameter to the screen, which the drawer is calling, whenever the drawer is clicked. If the drawer calls a stack, you would have to add the parameter to the 1st screen of the stack
Drawer navigation with a single screen:
import { createDrawerNavigator } from "#react-navigation/drawer";
const MyNav = createDrawerNavigator();
<MyNav.Navigator>
<MyNav.Screen name="screen1" component={screen1} initialParams={{ opParam: "abc" }}/>
</MyNav.Navigator>
Drawer navigation with a stack:
import { createDrawerNavigator } from "#react-navigation/drawer";
import { createNativeStackNavigator } from "#react-navigation/native-stack";
const stack1 = () => {
const MyStack = createNativeStackNavigator();
return (
<MyStack.Navigator>
<MyStack.Screen name="screen1" component={screen1}/>
</MyStack.Navigator>
);
};
const MyNav = createDrawerNavigator();
<MyNav.Navigator>
<MyNav.Screen name="stack1" component={stack1}/>
</MyNav.Navigator>
Screen 1:
import React, { useEffect } from "react";
import { useIsFocused } from "#react-navigation/native";
const screen1 = (props) => {
const isFocused = useIsFocused();
useEffect(() => {
async function fetchData() {
// If this screen was loaded because of a click on the drawer, the following condition would be true.
if (props.route?.params?.opParam === "abc") {
// do whatever you want done when the user clicks on the drawer...
console.log("drawer was clicked");
}
}
// Only if this screen is in focus, we execute the function above.
if (isFocused) {
fetchData();
}
}, [ isFocused, props.route?.params?.opParam, ]);
return(
// ............
);
};

Expo notification doesnt fire method when app is killed/closed

im using react native expo and push notification works fine when app is running or in background but with the app is close. it doesnt call the method to handle the notification. I need to redirect to detail page. I tried to use function compoents and class components, tried to migrade to legacy notification and the new one.
import React, {useState, useEffect, useRef} from 'react';
import {
Image, ScrollView,
StyleSheet, Text, TouchableOpacity, Platform,
View, Linking,
} from 'react-native';
import * as Notifications from "expo-notifications";
const HomeScreen = (props) => {
useEffect(() => {
notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
const {request, date} = notification ||{}
const {content} = request ||{}
const {data} = content ||{}
const {annKey,type} = data ||{}
if(annKey) {
// navigation.navigate('Detail', {annKey, updateFeed: true, onSelect},)
}
});
responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
const {notification} = response ||{}
console.log(notification);
const {request, date} = notification ||{}
const {content} = request ||{}
const {data} = content ||{}
const {annKey, type} = data ||{}
if(annKey){
navigation.navigate('Detail', {annKey, updateFeed: true, onSelect},)
}
});
return () => {
Notifications.removeNotificationSubscription(notificationListener);
Notifications.removeNotificationSubscription(responseListener);
};
}, []);
}
export default HomeScreen;
The problem is that the useEffect() get called too late after the app has finished initializing. Therefore the listener is not added before the system has abandoned the notification, and the handler not called.
Fortunately, since you are using the new expo-notifications library, it introduced the useLastNotificationResponse() React hook. It can replace the addNotificationResponseReceivedListener() listener and returns the last notification the user interacted with (i.e. tap). It can be safely used in a useEffect() hook.
You can find the documentation here : https://docs.expo.io/versions/latest/sdk/notifications/#uselastnotificationresponse-undefined--notificationresponse--null
Here is how to use it (it's better to implement it on your root component):
import * as Notifications from 'expo-notifications';
export default function App() {
const lastNotificationResponse = Notifications.useLastNotificationResponse();
React.useEffect(() => {
if (
lastNotificationResponse &&
lastNotificationResponse.notification.request.content.data['someDataToCheck'] &&
lastNotificationResponse.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER
) {
// navigate to your desired screen
}
}, [lastNotificationResponse]);
return (
/*
* your app
*/
);
}
You have to add this to your app.json file:
"android": {
"useNextNotificationsApi": true,
},

How to prevent Back Button from hiding my keyboard in my react native app?

So I have a simple input, When I focus on it, the keybord pops up, but I want that my keybord hide only if If click on ok, or Enter, But what I see is that when I click the 'BACK' button of my phone, the keyboard disappear, and I don't want that, or at least I want to detect it in my component.
This is how I handle the back click in my app ! Knowing that I have three screens in my app, authentication screen ( LOGIN ), main screen ( MAIN ) , and route screen ( ROUTE ).
import { BackHandler } from 'react-native';
import { prevScreen } from '../../helpers/NavigationService';
import { SCREENKEYS } from '../../business/constants';
import { useEffect } from 'react';
const useBackAction = screen => {
useEffect(() => {
const backAction = () => {
if (screen === SCREENKEYS.LOGIN) BackHandler.exitApp();
else if (screen === SCREENKEYS.MAIN) return true;
else prevScreen();
return true;
};
const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction);
return () => backHandler.remove();
}, [screen]);
};
export default useBackAction;
Is there something I have to add to prevent the back click from hiding mu keybord view ?

React Native status bar event for dimensions in 0.62.x

StatusBarIOS has a method addListener which allows us to listen for changes to the status bar height, like so:
StatusBarIOS.addListener('statusBarFrameWillChange', (statusBarData) => {
this.setState({statusBarHeight: statusBarData.frame.height});
});
StatusBarIOS is deprecated, with a message that the code has been merged into StatusBar
How can we listen for the statusBarFrameWillChange event?
You can use the NativeEventEmitter module, here's an example of a react hook using the module to get the status bar height.
import React, { useState, useEffect } from 'react';
import { NativeEventEmitter, NativeModules } from 'react-native';
const { StatusBarManager } = NativeModules;
export default function useStatusBarHeight() {
const [value, setValue] = useState();
useEffect(() => {
const emitter = new NativeEventEmitter(StatusBarManager);
StatusBarManager.getHeight((statusBarFrameData) => setValue(statusBarFrameData.height));
const listener = emitter.addListener('statusBarFrameWillChange', (data) => setValue(data.frame.height));
return () => listener.remove();
}, []);
return value;
}
This snippet also uses the StatusBarManager to grab the initial height.

How to start with left menu collapsed

Is there an easy was to start with the left menu collapsed or do I need to update the layout ?
I'd like to have the menu collapsed by default with only the icons visible.
Thank you.
If you mean Sidebar when saying "left menu", you can hide it by turning on the user saga (the toggle action will continue to work):
// closeSidebarSaga.js
import {
put,
takeEvery,
} from 'redux-saga/effects'
import {
REGISTER_RESOURCE, // React-admin 3.5.0
setSidebarVisibility,
} from 'react-admin'
function* closeSidebar(action) {
try {
if (action.payload) {
yield put(setSidebarVisibility(false))
}
} catch (error) {
console.log('closeSidebar:', error)
}
}
function* closeSidebarSaga() {
yield takeEvery(REGISTER_RESOURCE, closeSidebar)
}
export default closeSidebarSaga
// App.js:
import closeSidebarSaga from './closeSidebarSaga'
<Admin customSagas={[ closeSidebarSaga ]} ... }>
...
</Admin>
In the react-admin library itself, apparently a bug, at some point in time after the login, action SET_SIDEBAR_VISIBILITY = true is called!
You can set the initial state when loading Admin, then there will be no moment when the Sidebar is visible at the beginning, and then it is hidden:
https://marmelab.com/react-admin/Admin.html#initialstate
const initialState = {
admin: { ui: { sidebarOpen: false, viewVersion: 0 } }
}
<Admin
initialState={initialState}
...
</Admin>
To hide the left sideBar divider we need to dispatch setSidebarVisibility action .
This is an example to hide the sideBar by using useDispatch react-redux hook &
setSidebarVisibility action inside the layout file (layout.tsx):
import React from 'react';
/**
* Step 1/2 :Import required hooks (useEffect & useDispatch)
*/
import { useEffect } from 'react';
import { useSelector,useDispatch } from 'react-redux';
import { Layout, Sidebar ,setSidebarVisibility} from 'react-admin';
import AppBar from './AppBar';
import Menu from './Menu';
import { darkTheme, lightTheme } from './themes';
import { AppState } from '../types';
const CustomSidebar = (props: any) => <Sidebar {...props} size={200} />;
export default (props: any) => {
const theme = useSelector((state: AppState) =>
state.theme === 'dark' ? darkTheme : lightTheme
);
/**
* Step 2/2 : dispatch setSidebarVisibility action
*/
const dispatch = useDispatch();
useEffect(() => {
dispatch(setSidebarVisibility(false));
});
return (
<Layout
{...props}
appBar={AppBar}
sidebar={CustomSidebar}
menu={Menu}
theme={theme}
/>
);
};
Since Redux is not used anymore by ReactAdmin, you need to adapt the local storage instead to hide the sidebar. Call this in your App.tsx class:
const store = localStorageStore();
store.setItem("sidebar.open", false);