I am making a call to Firebase, to check if user has a property.
If he has, I want him to be redirected to page A, else I want to redirect him to page B.
The problem is I think Firebase takes to long to come back, so React just renders the component with the default value.
My code:
export default class CheckIfHasCurrentTasksComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
questionId: this.props.match.params.id
};
}
async componentDidMount() {
var uid = firebase.auth().currentUser.uid;
var AssignedWordRef = await firebase.database().ref('Users').child(uid).child('assignedWork');
await AssignedWordRef.on('value', snapshot => {
var question = snapshot.val();
console.log('question', question);
if (question.length > 0) {
this.setState({
questionId: question,
});
console.log('redirect', this.state.questionId);
}
else {
this.setState({
questionId: this.props.match.params.id,
});
console.log('No redirect', this.state.questionId);
}
})
}
render() {
console.log('questionId', this.state.questionId);
return <QuestionComponent questionId={this.state.questionId}/>
}
}
The logs:
questionId -LEU6zUnLTO84KGh3pua CheckIfHasCurrentTasksComponent.js:35
question -LEU1fr2TmxHFKD3ObG_ CheckIfHasCurrentTasksComponent.js:18
questionId -LEU1fr2TmxHFKD3ObG_ CheckIfHasCurrentTasksComponent.js:35
redirect -LEU1fr2TmxHFKD3ObG_ CheckIfHasCurrentTasksComponent.js:23
The code seems to be running twice for some reason. The id is the one coming from a previous component. The last 3 are the ones from the database.
The confusing part is that they get logged async-like. How can I make it synchronous/wait for the response from Firebase to come back?
The this.props.match.params.id is an id coming from a previous component. This one is acting all right.
The confusing part is that they get logged async-like. How can I make it synchronous/wait for the response from Firebase to come back?
There's an approach where you can make it look synchronous.
You can attach a variable to this class, which by default, is false. Once, you have received the response, you can set that variable to true.
For instance -
this.state.isQuestionIDAvailable=false;
And in your firebase response, ( do take care of this's scope when using it )
this.setState({
isQuestionIDAvailable: true
});
You can then attach a condition to your component like -
{this.state.isQuestionIDAvailable && <QuestionComponent />}
Also, if <QuestionComponent/> is the only thing you are rendering you'll not need the curly braces as well. You can write it as
return this.state.isQuestionIDAvailable && <QuestionComponent />
Hope this answers :)
Related
I'm trying to make a Post request on component Mount. But if user reloads the page or states changes, then the function is called again as I'm useEffect and it sends the request again. But I want any better thing where the Post request should be made once and if even the page refreshes the shouldn't be called again if it has been called.
I'm using the Function base component. and make Post requests using redux.
const Main = () => {
// ....
// Here I'm forcing user to login if there's user is logged in then want to make a silent post request, But it sends request everytime on state change.
useEffect(() => {
getLocalStorage()
if (!userInfo) {
setModalShow(true)
}
if (userInfo) {
dispatch(postRequest())
setModalShow(false)
}
}, [userInfo])
return (
<div>Some JSX </div>
)
}
export default Main
So need your help to fix that issue. Can we use localStorage to store the information either the post request is already have been made or any other better idea?
Best way is to use localstorage, not sure if my placements of setting ang getting value from localstorage are on the right spot.
const Main = () => {
// ....
// Here I'm forcing user to login if there's user is logged in then want to make a silent post request, But it sends request everytime on state change.
useEffect(() => {
getLocalStorage()
// Check if the value of logged is true initiali will be false until the
// first request if made
if (!!localStorage.getItem('logged')) {
setModalShow(true)
}
if (userInfo) {
dispatch(postRequest())
setModalShow(false)
// set the value when the request is finished
localStorage.setItem('logged', true)
}
}, [userInfo])
return (
<div>Some JSX </div>
)
}
export default Main
There is a package named redux-persist that you can save the state, for example in localStorage. You can use this package, and send post request if there is not any data in state.
Using localStorage for that purpose is pretty useful, you can save the information on post request whether it was made or not.
For a basic setup;
this could be like that:
const postRequestStatus = localStorage.getItem('postRequestMade') ? JSON.parse(localStorage.getItem('postRequestMade')) : null
useEffect(() => {
getLocalStorage()
if (!userInfo) {
setModalShow(true)
}
if (userInfo) {
setModalShow(false)
if (!postRequestStatus) {
dispatch(postRequest())
console.log('Post Request Made')
localStorage.setItem('postRequestMade', true)
}
}
}, [userInfo, postRequestStatus])
Here's a catch. As far there is information in localStorage, of postRequestMade true . The request won't be made. So some point on the site you should set any logic to clear it out where it is necessary.
Secondly, What if the request was not successful if there was an error from the server. Then, you should also consider error handling as well. As you mentioned you are using redux and I'm sure there would be Axios as well try the functionality like that:
useEffect(() => {
getLocalStorage()
if (!userInfo) {
setModalShow(true)
}
if (userInfo) {
setModalShow(false)
if (!postRequestStatus) {
dispatch(postRequest())
// That block will take care if request was successful
// After a successful request postRequestMade should be set to true.
if (success) {
console.log('Successful Request')
localStorage.setItem('postRequestMade', true)
}
}
}
}, [userInfo, postRequestStatus, success])
So I have this nuxt page /pages/:id.
In there, I do load the page content with:
content: function(){
return this.$store.state.pages.find(p => p.id === this.$route.params.id)
},
subcontent: function() {
return this.content.subcontent;
}
But I also have an action in this page to delete it. When the user clicks this button, I need to:
call the server and update the state with the result
redirect to the index: /pages
// 1
const serverCall = async () => {
const remainingPages = await mutateApi({
name: 'deletePage',
params: {id}
});
this.$store.dispatch('applications/updateState', remainingPages)
}
// 2
const redirect = () => {
this.$router.push({
path: '/pages'
});
}
Those two actions happen concurrently and I can't orchestrate those correctly:
I get an error TypeError: Cannot read property 'subcontent' of undefined, which means that the page properties are recalculated before the redirect actually happens.
I tried:
await server call then redirect
set a beforeUpdate() in the component hooks to handle redirect if this.content is empty.
delay of 0ms the server call and redirecting first
subcontent: function() {
if (!this.content.subcontent) return redirect();
return this.content.subcontent;
}
None of those worked. In all cases the current page components are recalculated first.
What worked is:
redirect();
setTimeout(() => {
serverCall();
}, 1000);
But it is obviously ugly.
Can anyone help on this?
As you hinted, using a timeout is not a good practice since you don't know how long it will take for the page to be destroyed, and thus you don't know which event will be executed first by the javascript event loop.
A good practice would be to dynamically register a 'destroyed' hook to your page, like so:
methods: {
deletePage() {
this.$once('hook:destroyed', serverCall)
redirect()
},
},
Note: you can also use the 'beforeDestroy' hook and it should work equally fine.
This is the sequence of events occurring:
serverCall() dispatches an update, modifying $store.state.pages.
content (which depends on $store.state.pages) recomputes, but $route.params.id is equal to the ID of the page just deleted, so Array.prototype.find() returns undefined.
subcontent (which depends on content) recomputes, and dereferences the undefined.
One solution is to check for the undefined before dereferencing:
export default {
computed: {
content() {...},
subcontent() {
return this.content?.subcontent
👆
// OR
return this.content && this.content.subcontent
}
}
}
demo
I have a react app and I am using geolocated to get users location.
Following the instructions for the initialization I have wrapped the component:
export default geolocated({
positionOptions: {
enableHighAccuracy: true,
},
userDecisionTimeout: 15000,
})(ShowPois);
As soon as the user accepts (allows) the location finding on the browser I want two things to happen.
First I need to set a flag when then location is available to the app, so I have this:
static getDerivedStateFromProps(props, state) {
if (!state.geolocatedReady && props.coords) {
return {
geolocatedReady: true
}
}
return null;
}
Notice that props.coords comes from geolocated
The second thing is that I want to complete an input box with the address of the location found. In order to do this I have to do a post request to an api to get the address, but the problem is I cannot use the getDerivedStateFromProps() method because the method must return a value, not a promise (made by axios post request).
So how can I make a post request and then set the state when a prop changes in the component?
getDerivedStateFromProps is only for edge cases. The case you have here sounds like a fit for componentDidUpdate.
componentDidUpdate() {
if(!this.state.geolocatedReady && this.props.coords) {
this.setState({
geolocatedReady: true,
});
this.getAddress(this.props.coords);
}
}
getAddress = async (coords) => {
const address = await mapApi.getAddress(coords);
// or whatever you want with it.
this.setState({
address
})
}
I'm trying to increment the number I get from this.props.match.params.id to get the DOM to re-render with new API fetched content. I was trying to do that by calling a function on Click and incremeting value by one. However, that's not happening at all, instead of incrementing the value, it concatenates like this:
https://imgur.com/a/dnKScN0
This is my code:
export class Sofa extends React.Component {
constructor(props) {
super(props);
this.state = {
token: {},
isLoaded: false,
model: {}
};
}
addOne = () => {
console.log('addOne');
this.props.match.params.id += 1;
console.log('id: ', this.props.match.params.id);
}
lessOne = () => {
console.log('lessOne');
}
componentDidMount() {
/* fetch to get API token */
fetch(url + '/couch-model/' + this.props.match.params.id + '/', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'JWT ' + (JSON.parse(localStorage.getItem('token')).token)
}
}).then(res => {
if (res.ok) {
return res.json();
} else {
throw Error(res.statusText);
}
}).then(json => {
this.setState({
model: json,
isLoaded: true
}, () => { });
})
}
render() {
const { model, isLoaded } = this.state;
if (!isLoaded) {
return (
<div id="LoadText">
Loading...
</div>
)
} else {
return (
<div id="Sofa">
/* other HTML not important for this matter */
<img src="../../../ic/icon-arrow-left.svg" alt="Arrow pointing to left" id="ArrowLeft"
onClick={this.lessOne}/>
<img src="../../../ic/icon-arrow-right.svg" alt="Arrow pointing to right" id="ArrowRight"
onClick={this.addOne}/>
</div>
);
}
}
}
You can't manipulate props, they are immutable. You have a few options:
Use Redux or some other form of global state management and store the counter there.
Store the counter in the state of the parent, pass it down as a prop to the child along with a callback function allowing the child to increment the counter.
Store the state for the counter directly within the child component and update it there.
As your string is concatenating JS thinks its a string and not a number. Just increment like so Number(this.props...id) + 1
It seems that this.props.match.params.id is string. Adding a value to string variable works as concatination.
You need to parseInt before adding a value like:
this.props.match.params.id = parseInt(this.props.match.params.id) + 1;
Very likely this.props.match.params.id is a string. Doing this: '1'+1 returns '11'.
I guess this is a 'main' component - just 'below' router. You can't modify param from router - you should generate link to next item (pointing the same component) which will get it's own props ('updated' id) from router.
You're sure you will have contiuous range of ids? No deletions in db? Rare ;)
Normally you have a searched/category based list of items. Browsing list ... entering item (by id), back to list, enter another item. Browsing items w/o info about list is a little harder and inc/dec id isn't good idea.
Get list into main component, pass current id to sub-component (able to load data itself and render it). Prepare next/prev buttons in main (modify current in state, loop them?) - changed prop (id) will force rendering component to load next data.
Further ... extract next/prev into sub-components, passing handlers from main (you didn't passed them and even if it won't work for reasons above).
Accessing items only by id is a bit harder, f.e. you can 'ask' api for next/prev items for current one. It can be done async - render next/prev when data will be ready.
Don't make all in one step ;)
I would like to add a loading animation to my website since it's loading quite a bit when entering the website. It is built in ReactJS & NodeJS, so I need to know specifically with ReactJS how to add a loading animation when initially entering the site and also when there is any loading time when rendering a new component.
So is there a way to let people on my website already, although it's not fully loaded, so I can add a loading page with some CSS3 animation as a loading screen.
The question is not really how to make a loading animation. It's more about how to integrate it into ReactJS.
Thank you very much.
Since ReactJS virtual DOM is pretty fast, I assume the biggest load time is due to asynchronous calls. You might be running async code in one of the React lifecycle event (e.g. componentWillMount).
Your application looks empty in the time that it takes for the HTTP call. To create a loader you need to keep the state of your async code.
Example without using Redux
We will have three different states in our app:
REQUEST: while the data is requested but has not loaded yet.
SUCCESS: The data returned successfully. No error occurred.
FAILURE: The async code failed with an error.
While we are in the request state we need to render the spinner. Once the data is back from the server, we change the state of the app to SUCCESS which trigger the component re-render, in which we render the listings.
import React from 'react'
import axios from 'axios'
const REQUEST = 'REQUEST'
const SUCCESS = 'SUCCESS'
const FAILURE = 'FAILURE'
export default class Listings extends React.Component {
constructor(props) {
super(props)
this.state = {status: REQUEST, listings: []}
}
componentDidMount() {
axios.get('/api/listing/12345')
.then(function (response) {
this.setState({listing: response.payload, status: SUCCESS})
})
.catch(function (error) {
this.setState({listing: [], status: FAILURE})
})
}
renderSpinner() {
return ('Loading...')
}
renderListing(listing, idx) {
return (
<div key={idx}>
{listing.name}
</div>
)
}
renderListings() {
return this.state.listing.map(this.renderListing)
}
render() {
return this.state.status == REQUEST ? this.renderSpinner() : this.renderListings()
}
}
Example using Redux
You can pretty much do the similar thing using Redux and Thunk middleware.
Thunk middleware allows us to send actions that are functions. Therefore, it allows us to run an async code. Here we are doing the same thing that we did in the previous example: we keep track of the state of asynchronous code.
export default function promiseMiddleware() {
return (next) => (action) => {
const {promise, type, ...rest} = action
if (!promise) return next(action)
const REQUEST = type + '_REQUEST'
const SUCCESS = type + '_SUCCESS'
const FAILURE = type + '_FAILURE'
next({...rest, type: REQUEST})
return promise
.then(result => {
next({...rest, result, type: SUCCESS})
return true
})
.catch(error => {
if (DEBUG) {
console.error(error)
console.log(error.stack)
}
next({...rest, error, type: FAILURE})
return false
})
}
}