DeviceEventEmitter it's been listening twice - react-native

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

Related

Why use Hub.listen inside useEffect - Amplify Auth

I'm looking through the documentation at https://docs.amplify.aws/lib/auth/social/q/platform/js/#full-samples
and can't understand why Hub.listen is being used within use Effect.
Hub.listen('auth', ({ payload: { event, data } }) => {
switch (event) {
case 'signIn':
case 'cognitoHostedUI':
getUser().then(userData => setUser(userData));
break;
}
});
If I'm creating an event listener why should I create in useEffect instead of in the main body of the function.
What am I misunderstanding?
Figured it out, I was getting confused between functions and classes.
The useEffect with an empty array is being used to only create the event listener on initial render and not on subsequent renders.
Leaving the question up in case anyone else gets similarly confused
This has little to do with functions and classes. It is inside the useEffect(() => {/*...*/}, []) (note the empty dependency list) as it acts as the constructor of the component and is therefore only called once.
If Hub.listen is called inside the render function, this will result in growing list of event listeners, because everytime the component rerenders, a new listener is attached.
To keep you free from any memory issues, you would also detach a listener inside the useEffect hook. So for common listeners, would look like the following:
const App = () => {
useEffect(() => {
const listener = () => { console.log('foo'); }
window.addEventListener('resize', listener);
// destruct
return () => {
window.removeEventListener('resize', listener)
}
}, []);
}

useState does not work in SQLlite SELECT function react-native [duplicate]

So I have this:
let total = newDealersDeckTotal.reduce(function(a, b) {
return a + b;
},
0);
console.log(total, 'tittal'); //outputs correct total
setTimeout(() => {
this.setState({ dealersOverallTotal: total });
}, 10);
console.log(this.state.dealersOverallTotal, 'dealersOverallTotal1'); //outputs incorrect total
newDealersDeckTotal is just an array of numbers [1, 5, 9] e.g.
however this.state.dealersOverallTotal does not give the correct total but total does? I even put in a timeout delay to see if this solved the problem.
any obvious or should I post more code?
setState() is usually asynchronous, which means that at the time you console.log the state, it's not updated yet. Try putting the log in the callback of the setState() method. It is executed after the state change is complete:
this.setState({ dealersOverallTotal: total }, () => {
console.log(this.state.dealersOverallTotal, 'dealersOverallTotal1');
});
In case of hooks, you should use useEffect hook.
const [fruit, setFruit] = useState('');
setFruit('Apple');
useEffect(() => {
console.log('Fruit', fruit);
}, [fruit])
setState is asynchronous. You can use callback method to get updated state.
changeHandler(event) {
this.setState({ yourName: event.target.value }, () =>
console.log(this.state.yourName));
}
Using async/await
async changeHandler(event) {
await this.setState({ yourName: event.target.value });
console.log(this.state.yourName);
}
The setState is asynchronous in react, so to see the updated state in console use the callback as shown below (Callback function will execute after the setState update)
this.setState({ email: 'test#example.com' }, () => {
console.log(this.state.email)
)}
I had an issue when setting react state multiple times (it always used default state). Following this react/github issue worked for me
const [state, setState] = useState({
foo: "abc",
bar: 123
});
// Do this!
setState(prevState => {
return {
...prevState,
foo: "def"
};
});
setState(prevState => {
return {
...prevState,
bar: 456
};
});
The setState() operation is asynchronous and hence your console.log() will be executed before the setState() mutates the values and hence you see the result.
To solve it, log the value in the callback function of setState(), like:
setTimeout(() => {
this.setState({dealersOverallTotal: total},
function(){
console.log(this.state.dealersOverallTotal, 'dealersOverallTotal1');
});
}, 10)
If you work with funcions you need to use UseEffect to deal with setState's asynchrony (you can't use the callback as you did when working with classes). An example:
import { useState, useEffect } from "react";
export default function App() {
const [animal, setAnimal] = useState(null);
function changeAnimal(newAnimal) {
setAnimal(newAnimal);
// here 'animal' is not what you would expect
console.log("1", animal);
}
useEffect(() => {
if (animal) {
console.log("2", animal);
}
}, [animal]);
return (
<div className="App">
<button onClick={() => changeAnimal("dog")} />
</div>
);
}
First console.log returns null, and the second one returns 'dog'
just add componentDidUpdate(){} method in your code, and it will work.
you can check the life cycle of react native here:
https://images.app.goo.gl/BVRAi4ea2P4LchqJ8
As well as noting the asynchronous nature of setState, be aware that you may have competing event handlers, one doing the state change you want and the other immediately undoing it again. For example onClick on a component whose parent also handles the onClick. Check by adding trace. Prevent this by using e.stopPropagation.
I had the same situation with some convoluted code, and nothing from the existing suggestions worked for me.
My problem was that setState was happening from callback func, issued by one of the components. And my suspicious is that the call was occurring synchronously, which prevented setState from setting state at all.
Simply put I have something like this:
render() {
<Control
ref={_ => this.control = _}
onChange={this.handleChange}
onUpdated={this.handleUpdate} />
}
handleChange() {
this.control.doUpdate();
}
handleUpdate() {
this.setState({...});
}
The way I had to "fix" it was to put doUpdate() into setTimeout like this:
handleChange() {
setTimeout(() => { this.control.doUpdate(); }, 10);
}
Not ideal, but otherwise it would be a significant refactoring.

React native UI is not getting rendered after callback from native event emitter. Even callback having state change

I want to navigate the user to another screen in react native project after native app widget click in android. I was able to catch event using native event emitter in my MainView.js and there i changed state of one of my component and it got changed but UI is not getting rendered after this state change. It is showing blank screen and there is not error on the console. Thanks in advance for any help!!
export default class MainView extends React.Component {
constructor(props) {
super(props);
this.state = {text: 'Hi, This is main screen for app widget!!!'};
}
componentDidMount() {
const eventEmitter = new NativeEventEmitter();
this.listener = eventEmitter.addListener('MyCustomEvent', (event) => {
console.log('MyCustomEvent -->', event);
console.log('MyCustomEvent ArticleId -->', event.ArticleId);
if (event.ArticleId === data.articleId) {
console.log('data ArticleId true', data.articleId);
//navigation.push('Article Details', data);
this.setState({
text: data.articleDes,
});
// setText(data.articleDes);
console.log('text -->', this.state.text);
} else {
// setText('No such article found.');
console.log('text -->', this.state.text);
}
});
}
componentWillUnmount() {
this.eventListener.remove(); //Removes the listener
}
render() {
return (
<View style={{flex: 1}}>
<Text>{this.state.text}</Text>
<Button
title="click"
onPress={() => this.props.navigation.push('Article Details', data)}
/>
</View>
);
}
}
CustomActivity source code which is launched from appwidget click. From this activity's oncreate, I'm emitting events to react-native main view.
int articleId = 0;
if (getIntent() != null) {
articleId = getIntent().getIntExtra("articleId", 0);
Log.e("articleid", "" + articleId);
}
// Put data to map
WritableMap payload = Arguments.createMap();
payload.putInt("ArticleId", articleId);
// Emitting event from java code
ReactContext context = getReactNativeHost().getReactInstanceManager().getCurrentReactContext();
if ( context != null && context.hasActiveCatalystInstance()) {
Log.e("react context", "not null");
(getReactNativeHost().getReactInstanceManager().getCurrentReactContext())
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("MyCustomEvent", payload);
}
That is not how to use NativeEventEmitter. You need to initialise the NativeEventEmitter with the native module you are emitting events from:
import { NativeEventEmitter, NativeModules } from 'react-native';
const { myNativeModule } = NativeModules;
componentDidMount() {
...
const eventEmitter = new NativeEventEmitter(myNativeModule);
this.eventListener = eventEmitter.addListener('myEvent', (event) => {
console.log(event.eventProperty) // "someValue"
});
...
}
componentWillUnmount() {
this.eventListener.remove(); //Removes the listener
}
Read more about NativeModules here: https://reactnative.dev/docs/native-modules-android
This sound familiar with an issue I am experiencing on IOS. The code is similar, but I cannot guarantee that the underlying structure in Android works in the same way. Anyways, I am sending an event message from IOS-Native (written in swift in xCode) to React-native file using the NativeEventEmitter. After the initial render, the value just wont update, and as I understand this issue is not limited to this type of Event. After some googling I found out that everything you read from state inside that event-callback has a reference to only the first render, and will not update on future renders.
Solution; use useRef so you keep a reference to the the updated value. useRef keeps the value across renders and event-callbacks. This is not something I have found out myself, please look at https://medium.com/geographit/accessing-react-state-in-event-listeners-with-usestate-and-useref-hooks-8cceee73c559 and React useState hook event handler using initial state for, they are the one that deserves the credit.

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;
}
// ...
}

How to avoid navigating to other screen multiple times

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)
}
}
}