How to avoid navigating to other screen multiple times - react-native

When press on any button on my React Native App to navigate to a different screen multiple times, then it will redirected to the next screen multiple times.
My sample code is:
// This is my button click event
myMethod()
{
this.props.navigation.navigate("ScreenName")
}
I am using react-navigation to navigate through my app.
How can I fix this behaviour?

I think there are a few ways this could be done. Perhaps recording when the navigation has occurred and preventing it from navigating multiple times.
You may also want to consider resetting hasNavigated after an amount of time etc as well.
// Somewhere outside of the myMethod scope
let hasNavigated = false
// This is my button click event
myMethod()
{
if (!hasNavigated) {
this.props.navigation.navigate("ScreenName")
hasNavigated = true
}
}

This react-navigation issue contains a discussion about this very topic, where two solutions were proposed.
The first, is to use a debouncing function such as Lodash's debounce that would prevent the navigation from happening more than once in a given time.
The second approach, which is the one I used, is to check on a navigation action, whether it is trying to navigate to the same route with the same params, and if so to drop it.
However, the second approach can only be done if you're handling the state of the navigation yourself, for example by using something like Redux.
Also see: Redux integration.

One of solution is custom custom components with adds debounce to onPress:
class DebounceTouchableOpacity extends Component {
constructor(props) {
super(props);
this.debounce = false;
}
_onPress = () => {
if (typeof this.props.onPress !== "function" || this.debounce)
return;
this.debounce = true;
this.props.onPress();
this.timeoutId = setTimeout(() => {
this.debounce = false;
}, 2000);
};
componentWillUnmount() {
this.timeoutId && clearTimeout(this.timeoutId)
}
render() {
const {children, onPress, ...rest} = this.props;
return (
<TouchableOpacity {...rest} onPress={this._onPress}>
{children}
</TouchableOpacity>
);
}
}
another: wrap onPress function into wrapper with similar behavior
const debounceOnPress = (onPress, time) => {
let skipCall = false;
return (...args) => {
if (skipCall) {
return
} else {
skipCall = true;
setTimeout(() => {
skipCall = false;
}, time)
onPress(...args)
}
}
}

Related

React native flatList scrollToItem not scrolling in componentDidMount

I have a really weird issue, I have two components (both navigation routes) which share the same higher-order component that keeps the posts as state.
One component can link to the other by passing a post slug to the route as a parameter, this component, in turn, scrolls a flaList to the correct index:
findIndexBySlug = memoizeOne(
(postsArray, selectedPostSlug) => postsArray.findIndex(
post => post.slug === selectedPostSlug,
),
);
constructor(props) {
super(props);
this.shouldScrollToPost = this.shouldScrollToPost.bind(this);
this.scrollToIndex = this.scrollToIndex.bind(this);
this.flatListRef = React.createRef();
}
componentDidMount() {
this.shouldScrollToPost();
}
componentDidUpdate() {
this.shouldScrollToPost();
}
shouldScrollToPost() {
const { navigation } = this.props;
const selectedPostSlug = navigation.getParam('selectedPostSlug');
console.log('shouldscrollto', selectedPostSlug);
if (selectedPostSlug) {
const { posts } = this.props;
const { postsArray } = posts;
const selectedPostIndex = this.findIndexBySlug(
postsArray, selectedPostSlug,
);
this.scrollToIndex(selectedPostIndex);
}
}
scrollToIndex(index) {
if (this.flatListRef.current) {
console.log('scrolling to ', index)
this.flatListRef.current.scrollToIndex({ index, animated: false });
}
}
In both the first mount (componentDidMount) and subsequent calls (componentDidUpdate) all the console.log fire (including the one that checks for the flatListref) but, when called the first time in componentDidMount no scrolling occurs, in componentDidUpdate it does actually scroll!
Why?
It's driving me insane, even read the Dan Abramov post about the availability of refs (componentDidMount called BEFORE ref callback) and the flatList is the only rendered component (in fact the ref is always available).
Any help greatly appreciated.
Thanks

Call a function after state changes

I'm building a React Native app and when one button is pressed I want to call two functions. The first one will make a get call and set the state loading: true, the second one will show a popup with the result of that get call.
I am calling the second function only if loading === false but it is executed immediately after the first one before the state can change, because loading is false by default. I can resolve this with setTimeout but I was wondering if there was a cleaner way to do this.
onPress() {
this.props.getUsers();
setTimeout(() => {
if (this.props.loading === false) {
this.props.popUpVisible();
}
}, 1000);
}
You can create callback function for that
getUsers = (callback) => {
//do whatever you want
//when it's done
callback();
}
In onPress function
onPress = () => {
this.props.getUsers(() => {
if (this.props.loading === false) {
this.props.popUpVisible();
}
});
}
setState Function can take two param:
setState(updater, callback)
setState({loading:true},() => {
//this fires once state.loading === true
})
Use getDerivedStateFromProps. It always fire when component's props change.
Below is the example.
class EmailInput extends Component {
state = {
email: this.props.defaultEmail,
prevPropsUserID: this.props.userID
};
static getDerivedStateFromProps(props, state) {
// Any time the current user changes,
// Reset any parts of state that are tied to that user.
// In this simple example, that's just the email.
if (props.userID !== state.prevPropsUserID) {
return {
prevPropsUserID: props.userID,
email: props.defaultEmail
};
}
return null;
}
// ...
}

DeviceEventEmitter it's been listening twice

I don't know why when I fire an event the DeviceEventEmitter.addListener it's been emit once but listening twice.
I have a component Base that it's been add in every screen, something like
<View>
{this.props.children}
<ModalComponent />
</View>
Where the ModalComponent should be able to open anytime that ModalVisible event it's fired.
constructor (props) {
super(props)
this.state = {
modalVisible: false
}
}
componentDidMount() {
DeviceEventEmitter.addListener('ModalVisible', this.onModalVisible)
}
componentWillUnmount () {
DeviceEventEmitter.removeListener('ModalVisible', this.onModalVisible)
}
onModalVisible = (args) => {
console.log(['ModalVisible', args]) // logging twice
this.setState({
modalVisible: args.visible
})
}
close () {
this.setState({
modalVisible: false
})
}
onRequestClose = () => {
this.close()
}
render() {
return (
<Modal animationType={'slide'} transparent={false} visible={this.state.modalVisible} onRequestClose={this.onRequestClose}>
...
</Modal>
)
}
And I have a Server the emits the event when needed
static show (data) {
console.log(['Service.show', data]) // only once
DeviceEventEmitter.emit('ModalVisible', { visible: true })
}
When Service.show it's called, the first log appears only once, but right away at the addListener it's been logged twice.
I've already tried
this.listener = DeviceEventEmitter.addListener(...)
this.listener.remove()
and
this.onModalVisible.bind(this)
But it gave me every the same problem.
Besides that, at the same moment the Modal it's been duplicated, where when I close, I have two modals to be close.
I also tried load all this in a new screen, without parents components, to see if that could be the problem, mas no. It still.
I had the same problems with events being fired/registered twice with socket.io, my problem was due to the fact that I was adding eventListeners on DidMount method. But since my component was mounted multiple it was also adding eventListeners multiple times.
My guess is that you are using the same component multiple times and so adding multiple times the same eventListener. Try to add your eventsListener into another place that will be called only once.
today, I met the problem too. and I look at the source js. I find the DeviceEventEmit.addListener will actually call EventSubscriptionVendor.addSubscription method.
_subscriber: EventSubscriptionVendor;
addListener(
eventType: string,
listener: Function,
context: ?Object,
): EmitterSubscription {
return (this._subscriber.addSubscription(
eventType,
new EmitterSubscription(this, this._subscriber, listener, context),
): any);
}
addSubscription(
eventType: string,
subscription: EventSubscription,
): EventSubscription {
invariant(
subscription.subscriber === this,
'The subscriber of the subscription is incorrectly set.',
);
if (!this._subscriptionsForType[eventType]) {
this._subscriptionsForType[eventType] = [];
}
const key = this._subscriptionsForType[eventType].length;
//here is the point
this._subscriptionsForType[eventType].push(subscription);
subscription.eventType = eventType;
subscription.key = key;
return subscription;
}
in the inner method, it will push the listener's argus function into an array; when we call it many times, it will push many listeners' function.
so in the project, we have to avoid call it many times, and after the component unmount, we have to remove it.
Wrap the "DeviceEventEmitter.addListener" with useEffect:
https://stackoverflow.com/a/73386913/7126848

While coming back from react navigation componentWillMount doesn't get called

I have used react-navigation and on clicking hardware back button in android, I come back to previous component but componentWillMount doesn't get called. How do I ensure that componentWillMount is called?
componentWillMount will not trigger when you entering new screen / back to the screen.
my solution is using event navigator handler
https://wix.github.io/react-native-navigation/#/screen-api?id=listen-visibility-events-in-onnavigatorevent-handler
you can implement your 'componentWillMount' codes while 'willAppear' event id triggered, see this implementation:
export default class ExampleScreen extends Component {
constructor(props) {
super(props);
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
}
onNavigatorEvent(event) {
switch(event.id) {
case '`willAppear`':
// { implement your code on componentWillMount }
break;
case 'didAppear':
break;
case 'willDisappear':
break;
case 'didDisappear':
break;
case 'willCommitPreview':
break;
}
}
}
Does this answer from #bumbur help you? It defines a global variable that tracks if nav state has changed. You could insert a piece of code to see if you're in the specific tab that you are interested in. With that you could trigger a call to componentWillMount() ?
If you don't want to use redux, this is how you can store globally
information about current route, so you can both detect a tab change
and also tell which tab is now active.
https://stackoverflow.com/a/44027538/7388644
export default () => <MyTabNav
ref={(ref) => { this.nav = ref; }}
onNavigationStateChange={(prevState, currentState) => {
const getCurrentRouteName = (navigationState) => {
if (!navigationState) return null;
const route = navigationState.routes[navigationState.index];
if (route.routes) return getCurrentRouteName(route);
return route.routeName;
};
global.currentRoute = getCurrentRouteName(currentState);
}}
/>;

In React Native, the method of BackHandler does not work until back root route

when I press physical return button on my phone the log have no output until back root route
componentWillMount(){
BackHandler.addEventListener('hardwareBackPress', this._onBackAndroid)
}
_onBackAndroid = () => {
console.log(this.props.navigation.state.routeName)
if (this.lastBackPressed && this.lastBackPressed + 2000 >= Date.now()) {
return false;
}
this.lastBackPressed = Date.now();
toastShort('Press Again Exit App');
return true;
};
componentWillUnmount(){
BackHandler.removeEventListener('hardwareBackPress', this._onBackAndroid)
}
By design, react-navigation takes full control of the back button unless you handle the state yourself (by using something like Redux).
You can take a look at this issue, which is referencing a similar situation.