React Native: state is null sometimes - setState is working asynchronously - reactjs

I think I do have some problems with the state of my application . I already figured out that this.setState({ ... }) is a function which is working asynchronously.. So, I think this has something to do with my problem.
My problem is that I want to show a dialog popup to my user when I am sending a push notification via OneSignal. This push notification gets received by both iOS and Android. Even when the app is running in the
background, foreground or got killed and isn't even running in the background. For the popup dialog I am using this package: react-native-popup-dialog
This popup is only visible if I send certain key/value pairs with the push notification. These keys are:
showPopup:true - Displaying the popup when true. If it isn't set or not equals true, it isn't displayed!
openLink:mydomain.de- Adds a button with a link to the popup
buttonText:Open in Browser - Sets the button text to the link
Note, the extra URL button is only added to the popup if key openLink and buttonText is set. Of none of them or only one of the key is set, it isn't displaying this button.
However, the popup dialog only shows up sometimes in some cases. I will list them for you below:
Case 1: The application is opened. In this case the popup shows up on iOS and Android. This gets handled by the
onReceived function!
Case 2: The app is completely close (swiped off the screen/killed). In this case, the popup shows up on Android
devices but not on iOS devices! This gets handled by the onOpened function!
Case 3: The app has been opened and is now running in the background. In this case, the popup shows up on iOS
devices but not on Android devices. This gets handled by the onOpened function too!
So, cause I am not getting and error messages or something else, I guess I am right with my guess that this issue is
due the asynchronous this.setState({ ... }) function.
My question now is how can I make sure that the state of notification and visible is always set before rendering the getPopup(...) method.. I already was thinking about implementing it so that I call the getPopup(...) function with parameters. So, I can be sure the parameters are always set before calling the method. However, sadly this is not possible. Cause the class you see below, the SuperScreen class, is just a class which gets extended by some subclasses to bundle my code like the push notification code or some functions I need in every of these subclasses.
Also, I already tried out to add a variable to my SuperClass state e.g. called stateSet which gets set after the setState({ ... }) function of either onReceived or onOpened has finished and verify it with
if(this.state.stateSet) in the first line of the getPopup(...) function. However, this is also not possible. The reason for that is because then my popup is not closing anymore when I am pressing either Ok or the link button.
If you guys have any ideas on how to solve this problem I really would appreciate it!
Here is my code:
export default class SuperScreen extends Component {
constructor(props) {
super(props);
this.state = {
pushNotification: null,
visible: false
};
OneSignal.init("00000000", {
kOSSettingsKeyAutoPrompt: true
});
OneSignal.inFocusDisplaying(0);
OneSignal.enableVibrate(true);
OneSignal.enableSound(true);
OneSignal.addEventListener("received", this.onReceived);
OneSignal.addEventListener("opened", this.onOpened);
OneSignal.addEventListener("ids", this.onIds);
}
componentWillUnmount() {
OneSignal.removeEventListener("received", this.onReceived);
OneSignal.removeEventListener("opened", this.onOpened);
OneSignal.removeEventListener("ids", this.onIds);
}
onReceived = notification => {
//App is opened!
console.log("Notification received: ", notification);
this.setState({
pushNotification: notification,
visible: true
});
if (notification.payload.notificationID != null) {
firebase.analytics().logEvent("Popup_Link_Button", {
notificationID: notification.payload.notificationID,
clicked: true
});
}
};
onOpened = openResult => {
//App either is closed or running in background
//Android: Closed: Showing Background: Not Showing
//iOS: Closed: Not Showing Background: Showing)
console.log("openResult: ", openResult);
this.setState({
pushNotification: openResult.notification,
visible: true
});
if (openResult.notification.payload.notificationID != null) {
firebase.analytics().logEvent("Popup_Link_Button", {
notificationID: openResult.notification.payload.notificationID,
clicked: true
});
}
};
onIds = device => {
console.log("Device info: ", device);
};
getPopup() {
if (
this.state.pushNotification != null &&
this.state.pushNotification.payload.additionalData != null &&
this.state.pushNotification.payload.additionalData.showPopup != null &&
this.state.pushNotification.payload.additionalData.showPopup == "true"
) {
var actionButtons = null;
if (
this.state.pushNotification.payload.additionalData.openLink != null &&
this.state.pushNotification.payload.additionalData.buttonText != null
) {
actionButtons = [
<DialogButton
text="Ok"
key={0}
onPress={() => {
this.setState({ visible: false });
firebase.analytics().logEvent("Popup_Link_Button", {
notificationID: this.state.pushNotification.payload
.notificationID,
opened: false
});
}}
/>
];
actionButtons.push(
<DialogButton
text={this.state.pushNotification.payload.additionalData.buttonText}
key={1}
onPress={() => {
this.openLink(
this.state.pushNotification.payload.additionalData.openLink
);
this.setState({ visible: false });
firebase.analytics().logEvent("Popup_Link_Button", {
notificationID: this.state.pushNotification.payload
.notificationID,
link: this.state.pushNotification.payload.additionalData
.openLink,
opened: true
});
}}
/>
);
} else {
actionButtons = [
<DialogButton
text="Ok"
key={0}
onPress={() => {
this.setState({ visible: false });
firebase.analytics().logEvent("Popup_Link_Button", {
popupID: this.state.pushNotification.payload.notificationID,
opened: false
});
}}
/>
];
}
return (
<Dialog
visible={this.state.visible}
dialogTitle={
<DialogTitle
title={
this.state.pushNotification == null
? ""
: this.state.pushNotification.payload.title
}
/>
}
dialogAnimation={
new SlideAnimation({
slideFrom: "bottom"
})
}
dialogStyle={{ marginLeft: 20, marginRight: 20 }}
actions={actionButtons}
>
<DialogContent>
<Text />
<Text>
{this.state.pushNotification == null
? ""
: this.state.pushNotification.payload.body}
</Text>
</DialogContent>
</Dialog>
);
}
}

you can add a callback to setState to ensure the code runs after setting the state:
this.setState().then({ //Do something here. })

Use async, await for asynchronously .
onReceived = async (notification) => {
//App is opened!
console.log("Notification received: ", notification);
await this.setState({ // It will wait until finish setState.
pushNotification: notification,
visible: true
});
if (notification.payload.notificationID != null) {
firebase.analytics().logEvent("Popup_Link_Button", {
notificationID: notification.payload.notificationID,
clicked: true
});
}
};

You can use a callback in setState!, i learned from it a month ago and it's been useful since . Check this article , you can pass a function as a callback ;)
this.setState(
{ pushNotification: notification,
visible: true }, () => {this.getPopup()}) //this.getPopup it's the second parameter, a callback
Check the article, it's short and will help

Related

ReactJS: State variable not updating as expected

So I'm learning react and as a practise exercise, I made a basic add user app. The data should only be inserted in case in both input fields are valid. The problem is even if validations fails, the app still enters the data into the field. I'm not sure why because I have explicitly added the condition:
const onSubmit = (event) => {
event.preventDefault();
validateUser(newUser);
if (!showModal.display) {
console.log(showModal.display);
props.onAddingNewUser(newUser);
setNewUser({
username: "",
age: "",
});
}
};
Here is the link to sandbox for my app: https://codesandbox.io/s/beautiful-wilbur-j35mmi
Try clicking submit button without entering any data and you'll see it still populated the list.
I'm still learning React so it would be great if someone can elaborate what I'm doing wrong here.
Thanks!
the reason for that is every time you close the error box you convert display to false again, so no matter what in the end the if statmet:
validateUser(newUser);
if (!showModal.display){
...more code
}
will always be true because even if the user is not valid when you close the error box display will be false again and the if statement will run.
if you want a way around you can return false or true from the validateUser there are more ways to solve this, this is just one way.
Alternatively, you can check for validation before submitting a new user. Here is example below.
if(user.username !== "") and if(user.username) essentially same thing if statement will evaluate as true. Also you don't need to pass newUser from params you can access directly from your state.
const validateUser = () => {
let bool = true;
if (newUser.username && newUser.age && !isNaN(parseInt(newUser.age))) {
setShowModal({
display: false,
text: "",
});
} else {
bool = false;
console.log("Both are empty");
setShowModal({
display: true,
text: "Please enter a valid username and age (non-empty values).",
});
}
if (!newUser.username) {
bool = false;
// Username is empty
console.log("Username is empty");
setShowModal({
display: true,
text: "Please enter a valid age (> 0).",
});
}
if (!newUser.age) {
// Age is empty
bool = false;
console.log("Age is empty");
setShowModal({
display: true,
text: "Please enter a valid username (non-empty values).",
});
}
console.log("Validation End:", showModal);
return bool;
};
const onSubmit = (event) => {
event.preventDefault();
if (!showModal.display && validateUser()) {
console.log(showModal.display);
props.onAddingNewUser(newUser);
setNewUser({
username: "",
age: "",
});
}
};

React list, how does if else exactly work?

so in my React App, I basically have three buttons. When the specific button is clicked, I want to update the clicked value to be true. I also want the rest of the items that weren't clicked to be false. Is there another way to target the elements that weren't clicked on? I got this solution, but am confused on how it exactly works. I thought that if the first if statement returned true, the else if wouldn't run? So can someone explain how these are both running?
class App extends React.Component {
// state
state = {
list: this.props.tabs,
currentTabContent: '',
};
// event handlers
onButtonClick(tab) {
// ======THIS IS WHAT I DON'T UNDERSTAND========
const newList = this.state.list.map((item) => {
if (item === tab) {
item.clicked = true;
} else if (item !== tab) {
item.clicked = false;
}
return item;
});
// ==============================================
this.setState({
currentTabContent: tab.content,
list: newList,
});
}
// helpers
renderButtons() {
return this.props.tabs.map((tab, index) => (
<li key={index}>
<button
className={tab.clicked ? 'offset' : null}
onClick={() => this.onButtonClick(tab)}
>
{tab.name}
</button>
</li>
));
}
renderContent() {
return this.state.currentTabContent;
}
render() {
return (
<div className="App">
<ul>{this.renderButtons()}</ul>
<div className="display">{this.renderContent()}</div>
</div>
);
}
}
export default App;
I think your misunderstanding lies more in not quite understanding if...else if rather than anything to do with React. Let's take a look at your condition:
if (item === tab) {
item.clicked = true;
} else if (item !== tab) {
item.clicked = false;
}
return item;
This function runs when the following is called by the button's click handler:
() => this.onButtonClick(tab)
Where tab is a specific object corresponding to a specific button. You then map over list in state, which just appears to be the same list of tabs. For each object it checks if tab === listItem if that is true the stuff in the first block executes, that's why the correct button gets set to true. It then does not evaluate the second condition for that item, and just returns the item.
It then moves on to the other items, who will not be equal to tab, and they evaluate in the second condition, so they are marked as false for clicked.
There are some much more worrisome and larger issues in your code here that have more to do with you making comparisons between objects and the dataflow of your components, but those aren't the subject of your question here, I just wanted to warn you to look out for them in the future.

Adding and removing className dynamically in an inifinite loop

I built a carousel that displays 3 slides. While the center one is 100% width displayed, the left and right ones are only visible 10% of the width.
After accessing the website, the carousel starts moving automatically using this:
componentDidMount() {
this.carouselTimer = setInterval(() => {
this.handleButtonNext();
}, 5000);
}
And if I manually change the slide, I reset the interval that changes the slides automatically using clearInterval and setInterval again.
In order to be able to add a sliding animation, I want to change the state properties (leftSlide & rightSlide) from false to true and after the animation back to false.
I tried to change the properties from false to true inside handleButtonNext() method making changes here:
<Slide
className={` ${this.state.leftSlide ? ' left-slide' : ''} ${this.state.rightSlide ? 'right-slide' : ''}`}
...the rest of slides.../>
The dilemma I have and the problem I encountered so far is that I cannot remove the added class in such a manner that it won't break the autoplay feature.
I tried using a reset method and restarting the autoplay, but no solution seems to be working.
Without the removal of the added class, the autoplay (and reset in case of a manual change of the slides) works just fine, but that's not enough.
This is the method that handles next button:
handleButtonNext() {
this.setState({
rightSlide: true
});
// this.wait1ms = setInterval(() => {
// }, 1100); (useless attempt)
this.setState({
activeSlide: this.nextSlide()
})
}
nextSlide() {
let nextIndex = this.state.activeSlide+1;
return (nextIndex>this.state.slides.length-1) ? 0 : nextIndex ;
}
*The method is used here:*
<a className="button-container right">
<div className="carousel-button next" onClick={this.handleButtonNext}></div>
</a>
#same for the left button
I need to mention that I do not master React and I am fairly new to it. Thank you for the time you will take to help me! I wish you a great day.
L.E: I forgot to mention that I would like to do this using class component, not the hooks that function provides.
The problem is that I cannot remove the added class in such a manner that it won't break the autoplay feature.
setNextSlideStates(){
this.setState({
// remove rightSlide state
rightSlide: false,
// update the active slide state
activeSlide: this.nextSlide()
});
}
// for usert button click handler, set withAnimation to false.
handleButtonNext(withAnimation) {
if(this.carouselTimer) clearTimeout(this.carouselTimer)
// if there are any animations, terminate them and call setNextSlideStates manually
if(this.animationTimer) {clearTimeout(this.animationTimer); this.setNextSlideStates(); }
// start the animation
this.setState({
rightSlide: true
});
// wait 1.1 sec for animation to end
this.animationTimer = setTimeout(() => {
this.setNextSlideStates()
this.animationTimer = null;
// autoplay the next slide
this.carouselTimer = setTimeout(() => {
this.handleButtonNext(true);
}, 3900);
}, withAnimation ? 1100 : 0)
}
Also change your componentDidMount to:
componentDidMount() {
this.carouselTimer = setTimeout(() => {
this.handleButtonNext(true);
}, 3900);
}
Button Handler:
{/* no animation */}
<button onClick={() => {this.handleButtonNext(false)}}>next</button>

Papaparse doesn't work for more than 1 MB for me, react

Edit: Trying to use papaparse chunk & custom webworker multithreading now. Edit2: Sorry I could not figure this out, but I am going to render this list of cities through a web scraper on https://www.freemaptools.com/find-cities-and-towns-inside-radius.htm anyway I decided instead of from a csv with papaparse...
I am trying to render <WeatherCitySky /> for each city from a csv at a dropbox link parsed by papaparse. Inside componentDidMount the first cors-anywhere/dropbox link, commented out, is a 1.5 MB csv of eastern US cities... won't work. Wanted to do at least all of US cities at 5MB, but all I can get to work is the second corsanywhere/dropbox link at about 350bytes
Go to src>UIConainers>Map>CitiesMap.js
https://codesandbox.io/s/zen-dijkstra-1c31n?fontsize=14
the CitiesMap.js is found by bottom globe icon (after pressing the inbox icon if you're starting on the purple screen), then top left city animation
class CitiesMap extends React.Component {
_isMounted = false;
constructor(props) {
super(props);
this.updateData = this.updateData.bind(this);
this.state = { cities: [] };
}
componentDidMount() {
this._isMounted = true;
Papa.parse(
"https://dl.dropboxusercontent.com/s/k81s5enbamijuke/worldcitiespop_northamerica_nolonglat_few.csv",
// this one doesn't work"https://dl.dropboxusercontent.com/s/wy9vjxczbjm796y/worldcities_usa_few.csv",
{
download: true,
worker: true,
header: true,
skipEmptyLines: true,
step: this.updateData,
complete: function(results) {
}
}
);
}
updateData(results) {
if (this._isMounted) {
this.setState(prevState => ({
cities: [...prevState.cities, results.data.City]
}));console.log(this.state.cities)
}
}
componentWillUnmount() {
this._isMounted = false;
}
render(props) {
const filteredCities = this.state.cities.filter(cities => {
return (
cities.toUpperCase().indexOf(this.props.search.toUpperCase()) !==
-1
);
});
return (
<div>
<div className="Cities">
{filteredCities.map(City => {
return (
<div>
<WeatherCitySkyMap />
</div>
I wouldn't recommend reading this question I asked for this papaparse application that user_domino solved some problems, but this problem is different evidenced by it working, but only on a small file of only 350 bytes
Try switching step to chunk (it will process a chunk at a time vs one record at a time). You should only need to change that parameter and leave the function and everything else the same.
Another idea is setting chunkSize on Papa. So something like:
Papa.parse([your_url],
{
download: true,
worker: true,
header: true,
skipEmptyLines: true,
chunk: this.updateData,
chunkSize: 1024 * 1024 * 10, // 10MB
complete: function(results) {
}
}
);

React form validation still adds values

So I have a little bit of form validation going on and I am running into an issue. When I first load the web app up and try adding a value and submitting with my button it doesn't allow me and gives me the error I want to see. However, when I add a value setState occurs and then my value is pushed to UI and I try to add another blank value it works and my conditional logic of checking for an empty string before doesn't not go through what am I doing wrong?
addItem() {
let todo = this.state.input;
let todos = this.state.todos;
let id = this.state.id;
if (this.state.input == '') {
alert("enter a value");
document.getElementById('error').style.color = 'red';
document.getElementById('error').innerHTML = 'Please enter something first';
}
else {
this.setState({
todos: todos.concat(todo),
id: id + 1,
}, () => {
document.getElementById('test').value = '';
})
console.log(this.state.id);
}
}
You are checking this.state.input but no where in that code are you setting the input value on the state.
Try adding this where it makes sense in your application:
this.setState({ input: 'some value' });
Also, I recommend you use the state to define the application UI. So instead of using document.getElementById('error') or document.getElementById('test').value, have the UI reflect what you have in your state.
See here for more info: https://reactjs.org/docs/forms.html
Instead of manipulating the DOM directly:
document.getElementById('test').value = '';
you'll want to use React:
this.setState({ input: '' });
A good ground rule for React is to not manipulate the DOM directly through calls like element.value = value or element.style.color = 'red'. This is what React (& setState) is for. Read more about this on reactjs.org.
Before you look for the solution of your issue, I noticed that you are directly updating the DOM
Examples
document.getElementById('error').style.color = 'red';
document.getElementById('error').innerHTML = 'Please enter something first';
document.getElementById('test').value = '';
Unless you have special use case or dealing with external plugins this isn't recommended, when dealing with React you should update using the virtual DOM. https://www.codecademy.com/articles/react-virtual-dom
Pseudo code sample
constructor(props) {
this.state = {
// retain previous states in here removed for example simplicity
errorString: ''
}
}
addItem() {
let todo = this.state.input;
let todos = this.state.todos;
let id = this.state.id;
if (this.state.input == '') {
alert("enter a value");
this.setState({
errorString: 'Please enter something first'
});
}
else {
this.setState({
todos: todos.concat(todo),
id: id + 1,
input: '',
});
}
}
// notice the "error" and "test" id this could be omitted I just added this for your reference since you mentioned those in your example.
render() {
return (
<div>
{(this.state.errorString !== '') ? <div id="error" style={{color: 'red'}}>{this.state.errorString}</div> : null}
<input id="test" value={this.state.input} />
</div>
}
Every time you invoke setState React will call render with the updated state this is the summary of what is happening but there are lot of things going behind setState including the involvement of Virtual DOM.

Resources