OneSignal on notification open event fires after the home screen got launched then it navigates to the desired screen. I want to detect if the app was launched on pressing the notification prior the home screen get rendered so I can navigate to the Second screen directly and avoid unnecessarily calling of apis.
"react-native-onesignal": "^3.9.3"
"react-navigation": "^4.0.0"
code
const _opened = openResult => {
const { additionalData, body } = openResult.notification.payload;
// how to navigate or set the initial screen depending on the payload
}
useEffect(() => {
onesignal.init();
onesignal.addEventListener('received', _received);
onesignal.addEventListener('opened', _opened);
SplashScreen.hide();
return () => {
// unsubscriber
onesignal.removeEventListener('received', _received);
onesignal.removeEventListener('opened', _opened);
}
}, []);
Debug
your question is how to navigate or set the initial screen depending on the opened notification payload?
1) - set the initial screen depending on the opened notification payload.
according to class Lifecycle useEffect runs after the component output has been rendered, so listener in useEffect not listen until the component amounting, and this the reason of logs in home screen shown before logs in useEffect, see this explanation.
//this the problem (NavigationContainer called before useEffect).
function App() {
useEffect(() => {}); //called second.
return <NavigationContainer>; //called first.
}
//this the solution (useEffect called Before NavigationContainer).
function App() {
const [ready, setReady] = useState(false);
//called second.
useEffect(() => {
//listen here
setReady(true);
SplashScreen.hide();
});
//called first
//no function or apis run before useEffect here it just view.
if(!ready) return <></>;// or <LoadingView/>
//called third.
return <NavigationContainer>;
}
your code may be like this.
function App() {
const [ready, setReady] = useState(false);
const openedNotificationRef = useRef(null);
const _opened = openResult => {
openedNotificationRef.current = openResult.notification.payload;
}
const getInitialRouteName = () => {
if (openedNotificationRef.current) {
return "second"; //or what you want depending on the notification.
}
return "home";
}
useEffect(() => {
onesignal.addEventListener('opened', _opened);
//setTimeout(fn, 0) mean function cannot run until the stack on the main thread is empty.
//this ensure _opened is executed if app is opened from notification
setTimeout(() => {
setReady(true);
}, 0)
});
if(!ready) return <LoadingView/>
return (
<NavigationContainer initialRouteName={getInitialRouteName()}>
</NavigationContainer>
);
}
2) - navigate depending on the opened notification payload.
first you need to kown that
A navigator needs to be rendered to be able to handle actions If you
try to navigate without rendering a navigator or before the navigator
finishes mounting, it will throw and crash your app if not handled. So
you'll need to add an additional check to decide what to do until your
app mounts.
read docs
function App() {
const navigationRef = React.useRef(null);
const openedNotificationRef = useRef(null);
const _opened = openResult => {
openedNotificationRef.current = openResult.notification.payload;
//remove loading screen and start with what you want.
const routes = [
{name : 'home'}, //recommended add this to handle navigation go back
{name : 'orders'}, //recommended add this to handle navigation go back
{name : 'order', params : {id : payload.id}},
]
navigationRef.current.dispatch(
CommonActions.reset({
routes : routes,
index: routes.length - 1,
})
)
}
useEffect(() => {
//don't subscribe to `opened` here
//unsubscribe
return () => {
onesignal.removeEventListener('opened', _opened);
}
}, []);
//subscribe to `opened` after navigation is ready to can use navigate
const onReady = () => {
onesignal.addEventListener('opened', _opened);
//setTimeout(fn, 0) mean function cannot run until the stack on the main thread is empty.
//this ensure _opened is executed if app is opened from notification
setTimeout(() => {
if (!openedNotificationRef.current) {
//remove loading screen and start with home
navigationRef.current.dispatch(
CommonActions.reset({
routes : [{name : 'home'}],
index: 0,
})
)
}
}, 0)
};
return (
<NavigationContainer
ref={navigationRef}
onReady={onReady}
initialRouteName={"justLoadingScreen"}>
</NavigationContainer>
);
}
refrences for setTimeout, CommonActions.
Related
I wanted to convert a hide element when keyboard active HOC I found to the newer react-native version using hooks (useEffect), the original solution using the older react lifecycle hooks looks like this - https://stackoverflow.com/a/60500043/1829251
So I created a useHideWhenKeyboardOpen function that wraps the child element and should hide that child if the device keyboard is active using useEffect. But on render the child element useHideWhenKeyboardOpen isn't displayed regardless of keyboard displayed.
When I've debugged the app I see the following error which I didn't fully understand,because the useHideWhenKeyboardOpen function does return a <BaseComponent>:
ExceptionsManager.js:179 Warning: Functions are not valid as a React
child. This may happen if you return a Component instead of from render. Or maybe you meant to call this function rather than
return it.
in RCTView (at View.js:34)
Question:
How can you attach keyboard displayed listener to a component in the render?
Example useHideWhenKeyboardOpen function:
import React, { useEffect, useState } from 'react';
import { Keyboard } from 'react-native';
// Wrapper component which hides child node when the device keyboard is open.
const useHideWhenKeyboardOpen = (BaseComponent: any) => (props: any) => {
// todo: finish refactoring.....
const [isKeyboadVisible, setIsKeyboadVisible] = useState(false);
const _keyboardDidShow = () => {
setIsKeyboadVisible(true);
};
const _keyboardDidHide = () => {
setIsKeyboadVisible(false);
};
/**
* Add callbacks to keyboard display events, cleanup in useeffect return.
*/
useEffect(() => {
console.log('isKeyboadVisible: ' + isKeyboadVisible);
Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
return () => {
Keyboard.removeCurrentListener();
};
}, [_keyboardDidHide, _keyboardDidShow]);
return isKeyboadVisible ? null : <BaseComponent {...props}></BaseComponent>;
};
export default useHideWhenKeyboardOpen;
Example Usage:
return(
.
.
.
{useHideWhenKeyboardOpen(
<View style={[styles.buttonContainer]}>
<Button
icon={<Icon name="save" size={16} color="white" />}
title={strings.STOCKS_FEED.submit}
iconRight={true}
onPress={() => {
toggleSettings();
}}
style={styles.submitButton}
raised={true}
/>
</View>,
)}
)
Mindset shift will help: think of hooks as data source rather than JSX factory:
const isKeyboardShown = useKeyboardStatus();
...
{!isKeyboardShown && (...
Accordingly your hook will just return current status(your current version look rather as a HOC):
const useHideWhenKeyboardOpen = () => {
const [isKeyboadVisible, setIsKeyboadVisible] = useState(false);
const _keyboardDidShow = useCallback(() => {
setIsKeyboadVisible(true);
}, []);
const _keyboardDidHide = useCallback(() => {
setIsKeyboadVisible(false);
}, []);
useEffect(() => {
Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
return () => {
Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
Keyboard.addListener('keyboardDidHide', _keyboardDidHide);
};
}, [_keyboardDidHide, _keyboardDidShow]);
return isKeyboadVisible;
};
Note usage of useCallback. Without it your hook will unsubscribe from Keyboard and subscribe again on every render(since _keyboardDidHide would be referentially different each time and would trigger useEffect). And that's definitely redundant.
so I've been trying to reload the content from asyncStorage in a screen when navigating back from a second screen, but it only refreshes when i navigate forth and back again
here is my code
componentDidMount() {
const {navigation} = this.props
navigation.addListener('focus', () => {
AsyncStorage.getItem('Servers').then((servers) => {
servers = JSON.parse(servers);
if (servers) {
return this.setState({servers:servers, loaded: true})
}
this.setState({servers: [], loaded: true});
});
});
};
Also, i think it should be re-rendering everytime a setState is done, but its not doing it for some reason
this is my code after the changes:
focusHandler(){
AsyncStorage.getItem('Servers').then((servers) => {
servers = JSON.parse(servers);
if (servers.length) {
return this.setState({servers, carregado: true})
}
this.setState({carregado: true});
});
}
componentDidMount() {
const {navigation} = this.props
this.focusHandler();
navigation.addListener('focus', this.focusHandler());
};
it gives the following error:
That's the expected behavior ... cause you've only registered a listener for focus event .... Execute the callback of addListener directly in componentDidMount...
componentDidMount() {
const {navigation} = this.props;
yourFocusHandler();
this.unsubscribe = navigation.addListener('focus', yourFocusHandler);
};
componentWillUnmount() {
this.unsubscribe();
}
Screen C:
I have an edit function screen which user allow to delete.
Screen A:
The home page, I have a ready function which is onRefresh function that allows to the page refresh.
From screen C the user delete the function then go to screen A
is that possible to refresh the specific function ?
example.
this.props.navigation.navigate('Home', {
specific function here
})
My home page code looks like this
constructor(props) {
super(props);
}
componentWillUnmount = () => {
}
componentWillMount() {
this.getOrderListData(1)
}
onRefresh = () => {
this.setState({
onRefreshLoading: true
}, () => {
this.getOrderListData(this.state.page)
})
}
If I understand this case, you want to trigger onRefresh in Screen A, when coming back from Screen C.
Not sure regarding the function, but you can just pass a param to the Home screen, that Home screen would check on load and refresh if it's specified.
navigation.navigate('Home', {
refreshOnLoad: true
})
...
function HomeScreen({ route, navigation }) {
const { refreshOnLoad } = route.params;
if (refreshOnLoad) {
...
}
I would have a common state for the App (Redux or MobX tree), that would be updated in such a case
Another way to refresh ScreenA after you navigate it from ScreenC is to add a listener to focus event on navigation, for example in the constructor of ScreenA have a listener like this
construction(props){
...
this.focusListener = props.navigation.addListener('focus', this.onRefresh);
...
}
Dont forget to remove it on componentWillUnmount() like this
componentWillUnmount = () => {
this.focusListener();
}
Hope this helps. You can read more about this here.
React Navigation 2x example
In react-navigation 2x you have to listen for didFocus event. For example have it like this in your constructor
construction(props){
...
this.focusListener = props.navigation.addListener('didFocus', this.onRefresh);
...
}
And then unsubscribe to the event like this
componentWillUnmount = () => {
this.focusListener.remove();
}
I solved the problem, Another way to do it.
Screen A:
constructor(props) {
super(props);
this.onRefresh = this.onRefresh.bind(this);
}
onRefresh = () => {
this.setState({
onRefreshLoading: true,
orderList:[]
}, () => {
this.getOrderListData(1)
})
}
this.props.navigation.navigate('Screen B', {
onRefresh: this.onRefresh,
})
Screen B passing params to Screen C
const { state, setParams, navigate } = this.props.navigation;
const params = state.params || {};
this.props.navigation.navigate('Screen C', {
onRefresh: params.onRefresh
})
Screen C :
this.props.navigation.navigate('ScreenA')
const { state, setParams, navigate } = this.props.navigation;
const params = state.params || {};
this.props.navigation.state.params.onRefresh()
on Android Mi Note 3, hardware back button is not fire the handleBackPress , when I will click on back the app exit.
I have do the following code but the handleBackPress is not called.
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
}
handleBackPress = () => {
this.goBack(); // works best when the goBack is async
return true;
}
Navigation Code :
const ModalSignUp = createStackNavigator(
{
Signup: { screen: Signup, key: 'Signup' },
PartyList: { screen: PartyList, key: 'PartyList' },
StatesList: { screen: StatesList, key: 'StatesList' },
},
{
initialRouteName: 'Signup',
headerMode: 'none',
mode: 'card',
}
);
Navigate :
this.props.navigation.push("StatesList")
Expected :
back click on hardware button, go to previous screen.
Your error can be in the way you get the next view of react-navigation.
You need to use .push to create a new view on the stack and when you click the back button, the .goBack() will be triggered.
By default, back button will always make the navigation to go back on the stack, but if you have only one view in the stack (this happens when you only use .navigate) the app will exit.
Not sure how you are navigating through the views, but this can be a solution.
Edit: To solve this problem, when navigating through views, use navigation.push('viewname') instead of navigation.navigate('viewname'). You don't need any other method (like the one you put in the question).
Also check the docs to understand the how navigating works or this question
Try using return false instead of return true.
1. Import
import { BackHandler, DeviceEventEmitter } from 'react-native'
2. constructor
constructor(props) {
super(props)
this.backPressSubscriptions = new Set()
}
3. Add and Remove Listeners
componentDidMount() {
DeviceEventEmitter.removeAllListeners('hardwareBackPress')
DeviceEventEmitter.addListener('hardwareBackPress', () => {
let invokeDefault = true
const subscriptions = []
this.backPressSubscriptions.forEach(sub => subscriptions.push(sub))
for (let i = 0; i < subscriptions.reverse().length; i += 1) {
if (subscriptions[i]()) {
invokeDefault = false
break
}
}
if (invokeDefault) {
BackHandler.exitApp()
}
})
this.backPressSubscriptions.add(this.handleHardwareBack)
}
componentWillUnmount() {
DeviceEventEmitter.removeAllListeners('hardwareBackPress')
this.backPressSubscriptions.clear()
}
4. Handle back
handleHardwareBack = () => {
this.props.navigation.goBack(null)
console.log(" ********** This is called ************ ");
return true;
}
Try this:
import {BackHandler} from 'react-native';
export default class Component extends Component {
_didFocusSubscription;
_willBlurSubscription;
constructor(props) {
super(props);
this._didFocusSubscription = props.navigation.addListener('didFocus',payload =>
BackHandler.addEventListener('hardwareBackPress', this.onBackButtonPressAndroid)
);
}
}
componentDidMount() {
this._willBlurSubscription = this.props.navigation.addListener('willBlur', payload =>
BackHandler.removeEventListener('hardwareBackPress', this.onBackButtonPressAndroid)
);
}
componentWillUnmount() {
this._didFocusSubscription && this._didFocusSubscription.remove();
this._willBlurSubscription && this._willBlurSubscription.remove();
}
onBackButtonPressAndroid = () => {
//code when you press the back button
};
Give it a try... this one works for me: in componentWillUnmount
BackHandler.removeEventListener('hardwareBackPress', () => {});
Also, make sure in each case you check in your this.goBack(); it return something
goback = () => {
if (condition2)
// handling
return something;
if (condition2)
// handling
return something;
// default:
return true;
};
I'd like to refresh the data on the screen in a react native app whenever the screen appears - like in a ViewWillAppear method. I tried using the componentWillMount method but it looks like it fires once before it appears, and doesn't fire again when the view is navigated to again.
Looking at this example https://reactnavigation.org/docs/guides/screen-tracking, it looks like I can add a listener on the onNavigationStateChange method on the root navigation, but I'd like to keep the logic in the screen as it gets confusing if I move the data fetch logic for that one scree outside to the root navigator.
I've tried to follow the example and set that method to my stacknavigation but it doesn't seem to trigger.
<RootNavigation ref={nav => { this.navigator = nav; }}
onNavigationStateChange={(prevState, currentState, action) => {
// maybe here I can fire redux event or something to let screens
// know that they are getting focus - however it doesn't fire so
// I might be implementing this incorrectly
const currentScreen = getCurrentRouteName(currentState);
const prevScreen = getCurrentRouteName(prevState);
if (prevScreen !== currentScreen) {
console.log('navigating to this screen', currentScreen);
}
}}
/>
So here's how I did it using the onNavigateStateChange.
<RootNavigation
ref={nav => { this.navigator = nav; }}
uriPrefix={prefix}
onNavigationStateChange={(prevState, currentState) => {
const currentScreen = this.getCurrentRouteName(currentState);
const prevScreen = this.getCurrentRouteName(prevState);
if (prevScreen !== currentScreen) {
this.props.emitActiveScreen(currentScreen);
{/*console.log('onNavigationStateChange', currentScreen);*/}
}
}}
/>
And in your screen you can check to see if your view will appear, note MyPage is the route name from your navigation object.
componentWillReceiveProps(nextProps) {
if ((nextProps.activeScreen === 'MyPage' && nextProps.activeScreen !== this.props.activeScreen)) {
// do your on view will appear logic here
}
}
Here is my navigator reducer.
function getCurrentRouteName(navState) {
if (!navState) {
return null;
}
const navigationState = (navState && navState.toJS && navState.toJS()) || navState;
const route = navigationState.routes[navigationState.index];
// dive into nested navigators
if (route.routes) {
return getCurrentRouteName(route);
}
return route.routeName;
}
export default function NavigatorReducer(state, action) {
// Initial state
if (!state) {
return fromJS(AppNavigator.router.getStateForAction(action, state));
}
// Is this a navigation action that we should act upon?
if (includes(NavigationActions, action.type)) {
// lets find currentScreen before this action based on state
const currentScreen = getCurrentRouteName(state);
const nextState = AppNavigator.router.getStateForAction(action, state.toJS());
// determine what the new screen will be after this action was performed
const nextScreen = getCurrentRouteName(nextState);
if (nextScreen !== currentScreen) {
nextState.currentRoute = nextScreen;
console.log(`screen changed, punk: ${currentScreen} -> ${nextScreen}`);
}
return fromJS(nextState);
}
return state;
}
And then we have to connect the module/route to the redux store (sceneIsActive is the important bit):
export default connect(
state => ({
counter: state.getIn(['counter', 'value']),
loading: state.getIn(['counter', 'loading']),
sceneIsActive: state.getIn(['navigatorState', 'currentRoute']) === 'Counter',
}),
dispatch => {
return {
navigate: bindActionCreators(NavigationActions.navigate, dispatch),
counterStateActions: bindActionCreators(CounterStateActions, dispatch),
};
},
)(CounterView);
And then inside your component, you can watch for/trigger code when the scene becomes active:
componentWillReceiveProps(nextProps) {
if (nextProps.sceneIsActive && (this.props.sceneIsActive !== nextProps.sceneIsActive)) {
console.log('counter view is now active, do something about it', this.props.sceneIsActive, nextProps.sceneIsActive);
doSomethingWhenScreenBecomesActive();
}
}
Know that componentWillReceiveProps does not run when initially mounted. So don't forget to call your doSomethingWhenScreenBecomesActive there as well.
You can use focus/blur event listeners(demonstrated here and discussed here).