Confusing behavior from router.push() from useRouter in NextJS - reactjs

I'm trying to implement a redirect using push() from useRouter and getting confusing behavior. My use case is simple: after a user signs in, I want to call router.push('/') to send them to the home page. However, calling router.push('/') after completing the signin flow does nothing. Here's the signin function. It's worth noting that this function gets called from a child component.
const authHandler = useCallback(async (emailCode) => {
setIsLoading(() => true);
try {
const result = await signIn.attemptFirstFactor({
strategy: 'email_code',
code: emailCode,
});
createAlerts('Success', false);
router.push('/') // this does nothing;
} catch (error) {
handleForeignAlert(error, 'Code is incorrect or expired');
} finally {
setIsLoading(() => false);
}
});
Why doesn't calling push() here work? In all the examples in the NextJS docs, it seems like calling it from within a function initiated by a click is exactly what I should do.
The only times router.push('/') does work is from within a useEffect() call after I manually reload the page, like this:
useEffect(() => {
if (userId) {
router.push('/');
}
}, [userId]);
Also worth noting is that other router methods, like reload(), work fine in the same place.
What rule or behavior am I missing here?

Related

Function inside useEffect fire twice even with empty dependency array

I have this example from https://github.com/vercel/next.js/blob/canary/examples/with-firebase-authentication/utils/auth/useUser.js
The effect works fine (fires once) but for some reason, the functions inside are called twice.
useEffect(() => {
const cancelAuthListener = firebase
.auth()
.onIdTokenChanged(async (user) => {
console.log('once or twice?')
if (user) {
// This fires twice
const userData = await mapUserData(user)
setUserCookie(userData)
setUser(userData)
} else {
removeUserCookie()
setUser()
}
})
const userFromCookie = getUserFromCookie()
if (!userFromCookie) {
router.push('/')
return
}
setUser(userFromCookie)
console.log(' i fire once')
return () => {
console.log('clean up')
cancelAuthListener()
}
}, [])
How can I make it to fire once?
I added some console logs:
On the first render I get: 'i fire once', 'once or twice', 'once or twice'
If I leave the page the cleanup console log fires (as it's supposed to do)
Many thanks
Later edit:
this is the code
export const mapUserData = async (user) => {
const { uid, email } = user
const token = await user.getIdToken()
return {
id: uid,
email,
token
}
}
If getIdToken() gets 'true' as an argument it will force a refresh regardless of token expiration.
https://firebase.google.com/docs/reference/js/firebase.User#getidtoken
Solved!!
the user was calling getIdToken(true) which forces a refresh.
https://firebase.google.com/docs/reference/js/firebase.User#getidtoken
Sorry guys, my bad!!!
You have a setState() inside useEffect thats the culprit, where useEffect having empty params [], one request on initial mount and another when do
setUser(userData) the component re-renders and useEffect() is invoked again.
Instead of using user as state, try using as ref and check. That might resolve this.

In web Firestore get() callback is executing twice when page is reloaded

I'm using Ionic with React and when the page is reloaded, Firestore call is returning duplicated data, since the callback executes twice.
Here's the code on the Page:
// View is already rendered
useIonViewDidEnter(() => {
fetchData()
});
function fetchData() {
getPendingCalls(userId).then((calls: any) => {
if (calls) {
// Show calls
setPendingCalls(calls)
}
})
}
And here's my firebaseService with the Firebase logic:
export function getPendingCalls(userId: string) {
return new Promise((resolve, reject) => {
db.collection(collection.calls)
.where(call.created_by, "==", userId)
.where(call.state, "==", callState.pending)
.orderBy(call.date)
.get()
.then(function(querySnapshot) {
const calls: Array<Call> = []
// ... some more stuff
resolve(calls)
})
.catch(function(error) {
console.log("Error getting pending calls: ", error);
resolve(null)
});
})
}
Any ideas why the code inside then() is executing twice?
This only happens when the page is reloaded, if I go to another page and come back it works fine, also when I land in that page.
Thanks!
[SOLUTION]
After a few days I reached to the solution thanks to one guy on Firebase community in Slack.
What happened was that even though the useIonViewDidEnter was calling once, it made some kind of conflict with the promise callback. So what I've been suggested to do was to use useEffect with no parameters instead. This is how it looks now:
// View is already rendered
useEffect(() => {
fetchData()
}, [])
Hope this helps to someone else :)

How do I force a page reload on Hooks state change?

I'm using react hooks and need to do a page refresh on state change.
I tried using window.location.reload() but this doesn't result in a proper refresh.
const responseGoogle = async googleData => {
await userLogin(googleData);
setLoggedIn(true);
window.location.reload()
};
Only a manual F5 refresh does the job!
Is there anything that's like a full refresh in React?
Any help is much appreciated.
If you mean just to re-render the page you can use hooks function,
const [dummy, reload] = useState(false);
and when you want to re-render just call the reload function
reload(!dummy);
just remember to import the useState from react;
Maybe you are running the window.location.reload() even the await promise fails. I would suggest to use trycatch in async functions to easily determine if it's successful or not.
You may try this code below
const responseGoogle = async googleData => {
try {
await userLogin(googleData);
setLoggedIn(true);
window.location.reload() //this will run if the await userLogin is succeeded
} catch (error) {
console.log(error); //will show an error if it fails
}
};
Actually all I had to do was to pass a boolean into the function: window.location.reload(true) to force the reload.

Display loading state and change route when API call is successfull

While working on a side project, I faced an issue with react-router-dom.
What I want to implement is: When I submit a Form, I need to save the data on my server. While the request is pending, I need to display a loading indicator. Once the server says everything is ok, I need to redirect the user on a new page
action.js
export const addNotification = value => async dispatch => {
dispatch(addNotificationPending())
try {
const response = await client.createNotification(values)
dispatch(addNotificationSuccess(response))
} catch(e) {
dispatch(addNotificationFailure())
}
}
component.js
class CreateNotificationForm extends Component {
onSubmit = (values) => {
this.props.addNotification(parameters, history)
}
render() {
const { isCreating } = this.props
const submitBtnText = isCreating ? 'Creating...' : 'Submit'
return (
<Form>
// content omitted
<Submit value={submitBtnText} />
</Form>
)
}
}
const mapStateToProps = (state) => ({
isCreating: getIsFetching(state)
})
const mapDispatchToProps = (dispatch) => ({ // omitted })
connect(mapStateToProps, mapDispatchToProps)(CreateNotificationForm)
So far so good: When I submit my form, the form's submit button shows a Creating... text.
However, how do I tell react-router to load a new path once the request is successful?
Right now, I've done that by using withRouter and using this.props.history as a second argument for this.props.addNotification.
It works great, but it seems really wrong
I've seen solutions using react-router-redux, but I don't really want to add a new middleware to my store.
Should I make the API call inside my component and use a Promise?
Any help?
Update:
After working a little on my own React project, and thinking about similar situations where I handle route changes there, I decided I want to change my original answer. I think the callback solution is OK, but the solution that you already mentioned of making the API call inside your component and using a promise is better. I realized that I've actually been doing this in my own app for a while now.
I use redux-form in my app, and it provides onSubmitSuccess/onSubmitFail functions that you can use to handle the submit result, and each of those rely on you returning a promise (usually from your action creator).
I think the fact that one of the most popular packages for form submission in React/Redux supports this pattern is an indication that it's probably a good pattern to use. Also, since react-router passes history into your component, it seems logical that they expect most people to do a lot of their programmatic route changes inside the component.
Here's an example of what the promise solution would look like with your code:
action.js
export const addNotification = value => dispatch => {
return new Promise(async (resolve, reject) => {
dispatch(addNotificationPending())
try {
const response = await client.createNotification(values)
dispatch(addNotificationSuccess(response))
resolve(response)
} catch(e) {
dispatch(addNotificationFailure())
reject(e)
}
})
}
component.js
onSubmit = async () => {
try {
await this.props.addNotification(parameters)
this.props.history.push('/new/route')
} catch(e) {
// could use a try/catch block here to display
// an error to the user here if addNotification fails,
// or go to a different route
}
}
Old Answer:
A simple solution would be to allow addNotification() to accept a callback function as an optional second argument.
export const addNotification = (value, callback=null) => async dispatch => {
dispatch(addNotificationPending())
try {
const response = await client.createNotification(values)
dispatch(addNotificationSuccess(response))
(typeof callback === 'function') && callback()
} catch(e) {
dispatch(addNotificationFailure())
}
}
Then inside your component use the router to go to the new route.
onSubmit = (values) => {
this.props.addNotification(parameters, () => {
this.props.history.push('/new/route')
})
}
You should not write your asynchronous calls in reducers or actions as the documentation clearly suggests them to be pure functions. You will have to introduce a redux-middleware like redux-thunk or redux-saga (I personally prefer sagas)
All your async calls will happen inside the middleware, and when it succeeds, you can use react-routers history .replace() or .push() methods to update your route. Let me know if it makes sense
You can use one popular package axios
See Here https://www.npmjs.com/package/axios
and you can implement your login like
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
You can write your loader login while calling api
and then you can hide your loader in .then

React Redux App - statement after dispatches do not always work

In my react app, I am trying to accept the user information and build some local objects and then navigate to some url.
const mapDispatchToProps = (dispatch) => {
return {
handleLogin: (userName, password) => {
dispatch(login(userName, password)).then(
(response) => {
if(!response.error){
dispatch(buildProfile(response.payload.data));
browserHistory.push('/'); // <-- This does not work in the first call to handleLogin and always works in the second call
}
else{
dispatch(loginFailed(response.payload));
}
});
}
};
};
The line browserHistory.push('/'); never gets called first time I click login button, though the buildProfile returns successfully. Only when I click login next time, redirection happens. What could be going wrong? And this happens consistently.
Thanks in advance.
There was a null variable access in my JSX (not posted here) which was crashing in response to the state update after first dispatch. After fixing that, this works as desired.

Resources