Updating Vue state during navigation with vue-router - javascript

I'm building a Vue 3 app which uses Vue Router and Pinia for state management.
In my global state I'm defining a property which tells me if a user is logged in so the user can navigate through the app. However, the login process is being handled by Cognito, so once I enter the app, I click on the login button, it takes me to a Cognito screen which handles the login process and then redirects me back to my app's home page.
So after the redirect I extract some params from the resulting URL and I want to save them in the global state, for this I was using the beforeEach guard to parse the URL, check for the params, update the store and then reading from it to check the valid session.
But my issue was that the navigation continued even before the state was updated. I ended up using setTimeout just to see if waiting for a bit solved the issue and it did
router.beforeEach((to) => {
const mainStore = useMainStore();
if (to.name === 'Login') {
return;
}
if (to.hash !== '') {
const queryParams = window.location.href.split('&');
const paramValues = queryParams.map(param => {
})
const payload = {
id_token: paramValues.find(param => param.name === 'id_token').value,
access_token: paramValues.find(param => param.name === 'access_token').value,
token_type: paramValues.find(param => param.name === 'token_type').value,
isAuthenticated: true
}
//Here I'm trying to update my global state
mainStore.updateTokens(payload);
}
// Here I use timeout, but before I just had the check for the property
setTimeout(() => {
if (!mainStore.isAuthenticated) return '/login';
}, 3000)
});
How should I handled this case? I've read about the beforeResolve guard but I'm not sure on how to use it; basically I just need to know how should I go about performing some async operation (like fetching data from server) during the navigation, not inside components.

Related

React Hooks: setState, when does it actually take effect?

So I'm trying to understand React Hooks and how to use them. I can make a fetch inside a component as follows:
var [pages,setPages] = useState();
var [page,setPage] = useState();
async function fetchData() {
await fetch(`https://myBackend/pages`)
.then(response => response.json())
.then(response => {
setPages(response);
console.log(pages[0].title);
})
.then(()=>{
// AT THIS STAGE, pages IS UNDEFINED
setPage(pages.filter(singlepage=> {
return singlepage.url == props.match.params.url;
}))
}
.catch(err => setErrors(err));
}
useEffect(() => {
fetchData();
console.log(pages[0].title);
return () => {
console.log('unmounting...');
}
},[]);
I call to my backend to get all the pages, which works fine, as if I harcode pages[0].title it will render. But when I'm trying to access specific values in pages within the useEffect hook, for me to assign page, it gives me the error of pages being undefined. My console logging makes this apparent.
I want page as well as pages to be defined on 'component mounting', prior to the page loading. So my question is when does setPages actually set the page? and is it possible for me to assign both within useEffect, and based off each other?
Asynchronous actions take time. An indeterminate amount of time could theoretically pass. So, there's no way to guarantee that your data fetches before mounting your component.
Instead, you need to act as if the data could be undefined and have a state of the application that handles that.
As for getting access to the pages variable immediately after calling setPages, that will also fail because React actually runs the re-render asynchronously.
Even if it ran at the same time, there's a thing called closures in which javascript pulls in all variables around a function when it is created, so that the function always has access to those. React Hooks work by utilizing these closures to only have access to the variables as they were when the component was rendered/re-rendered. The reason this is the case, is because every re-render, all of the functions in your component are re-created which creates a new closure.
So, this means that you'll need to keep these concepts in mind as you work with React.
As for your code, the results solution that Dlucidione set up is one of your best bets, aside from setting up a separate useEffect to update page when pages changes.
const history = useHistory(); //from react-router-dom
useEffect(() => {
async function fetchData() {
return await fetch(`https://myBackend/pages`)
.then((response) => response.json())
.then((response) => {
setPages(response);
})
.catch((err) => setErrors(err));
}
fetchData();
return () => {
console.log('unmounting...');
};
}, []);
useEffect(() => {
const url = props.match.params.url;
if (!pages || !url) return;
// Using Array#find here because I assume based on the name, you want one page rather than an array
const page = pages.find((singlepage) => {
return singlepage.url === url;
});
if (!page) {
// This will change the route if a page isn't found.
history.push('/404');
}
setPage(page);
}, [pages, props.match.params.url, history]);
Redirect and history.push are different in how they work. Redirect is the declarative navigation method, while history.push is the programmatic.
Example usage of history.push:
if (!page) {
// This will change the route if a page isn't found.
history.push('/404');
}
Important note, this can be used anywhere in the code as long as you can pass the history object to there. I've used it within redux thunks before as well.
Example usages of Redirect without it redirecting on mount:
if(!pages && !page){
return <Redirect to="/404"/>
}
Or within some JSX:
<div>
{!pages && !page && <Redirect to="/404"/>}
</div>
Redirect has to be rendered which means its only usable within the return statement of a component.
The way I'm understanding it: the redirect is based on if the
individual page is found in the mounting process, therefore the
redirecting process has to also be in the mounting process to check
before rendering anything to redirect or not.
This is correct. When the Redirect itself mounts or updates, it will redirect if the conditions are correct. If there's a path or from prop on the Redirect (they are aliases of each other), then that limits the Redirect to only work when that path matches. If the path doesn't match, the redirect will do nothing until the path is changed to match (or it unmounts, of course).
Under the hood, Redirect just calls history.push or history.replace in componentDidMount and componentDidUpdate (via the Lifecycle component), so take that as you will.
useEffect(() => {
const fetchData = async () => {
try {
// Make a first request
const result = await axios.get(`firstUrl`);
setPages(result);
// here you can use result or pages to do other operation
setPage(result.filter(singlepage=> {
return singlepage.url == props.match.params.url;
or
setPage(pages.filter(singlepage=> {
return singlepage.url == props.match.params.url;
}))
} catch (e) {
// Handle error here
}
};
fetchData();
}, []);

AWS Amplify React UI Component - How to dispatch auth state change event?

My application uses AWS Amplify React UI Components (#aws-amplify/ui-react) to handle user flows and I needs to know when a user successfully signs in.
I have added the handleAuthStateChange prop below. This works and I can receive the new state, however it prevents the app from navigating to other AmplifyAuthenticator slots like sign-up and forgotpassword.
<AmplifySignIn
slot="sign-in"
handleAuthStateChange={(state, data) => {
// handle state === 'signedin' but pass along other states
}}
></AmplifySignIn>
Does anyone know how to get notified about changes in authentication state without breaking other AmplifyAuthenticator slots?
You can add it to the AmplifyAuthenticator component.
<AmplifyAuthenticator
handleAuthStateChange={(state, data) => {
console.log(state)
console.log(data)
//add your logic
}}
>
<AmplifySignIn
slot="sign-in"
>
</AmplifySignIn>
</AmplifyAuthenticator>
Or you can access Auth state changes in other components using
import { onAuthUIStateChange } from '#aws-amplify/ui-components'
useEffect(() => {
return onAuthUIStateChange((state, data) => {
console.log(state);
console.log(data);
//add your logic
});
}, []);
Hope this helps

How to stop code execution after routing to error page from vuex?

I have a cart page written with VueJs and Vuex. I have an api file that acts as a wrapper around axios.
In my vuex action, I call the API and if it was successful I commit data into my mutation.
async await mounted () {
const result = this.getStatus()
if (result === "locked") {
this.$router.push({name: "LockedPage"}
}
else if (result === "expired") {
this.$router.push({name: "SessionExpiredPage"}
}
doSomething(result)
},
methods: {
function doSomething(res) {
// does something with result
}
}
The getStatus function here is from my vuex action.
const {authid, authpwd} = router.history.current.params
if (!authid || !authpwd) {
router.push({name: "SomethingWrong"})
return
}
const res = await api.getStatus
commit("SET_STATUS", res.status)
if (res.otherLogic) {
//commit or trigger other actions
}
return status
}
What's the ideal way to handle these kind of API errors? If you look you'll see that I'm routing inside the Outer component's mounted hook as well as inside the vuex action itself. Should all the routing for this status function just happen inside the vuex action?
I think how it is currently set up, when theSomethingWrong page get's routed. It'll return execution back to the mounted function still. So technically the doSomething function can still be called but I guess with undefined values. This seems kind of bad. Is there a way to stop code execution after we route the user to the error page? Would it be better to throw an error after routing the page?
Should I use Vue.config.errorHandler = function (err, vm, info) to catch these custom route erorrs I throw from the vuex action?
For general errors like expired session I would recommend handle it low at axios level, using interceptor. https://github.com/axios/axios#interceptors
Interceptors can be defined eg. in plugins. Adding Nuxt plugin as example (Vue without Nuxt will use little bit different plugin definition, but still it should be useful as inspiration) (Also window is access because snippet is from SPA application - no server side rendering)
export default ({ $axios, redirect }) => {
$axios.onError((error) => {
const code = parseInt(error.response && error.response.status)
if (code === 401) {
// don't use route path, because error is happending before transition is completed
if (!window.localStorage.getItem('auth.redirect')) {
window.localStorage.setItem('auth.redirect', window.location.pathname)
}
redirect('/login-page')
}
})
}

Next/React-Apollo: React props not hooked up to apollo cache when query comes from getInitialProps

I'm using nextjs and react-apollo (with hooks). I am trying to update the user object in the apollo cache after a mutation (I don't want to refetch). What is happening is that the user seems to be getting updated in the cache just fine but the user object that the component uses is not getting updated. Here is the relevant code:
The page:
// pages/index.js
...
const Page = ({ user }) => {
return <MyPage user={user} />;
};
Page.getInitialProps = async (context) => {
const { apolloClient } = context;
const user = await apolloClient.query({ query: GetUser }).then(({ data: { user } }) => user);
return { user };
};
export default Page;
And the component:
// components/MyPage.jsx
...
export default ({ user }) => {
const [toggleActive] = useMutation(ToggleActive, {
variables: { id: user.id },
update: proxy => {
const currentData = proxy.readQuery({ query: GetUser });
if (!currentData || !currentData.user) {
return;
}
console.log('user active in update:', currentData.user.isActive);
proxy.writeQuery({
query: GetUser,
data: {
...currentData,
user: {
...currentData.user,
isActive: !currentData.user.isActive
}
}
});
}
});
console.log('user active status:', user.isActive);
return <button onClick={toggleActive}>Toggle active</button>;
};
When I continuously press the button, the console log in the update function shows the user active status as flipping back and forth, so it seems that the apollo cache is getting updated properly. However, the console log in the component always shows the same status value.
I don't see this problem happening with any other apollo cache updates that I'm doing where the data object that the component uses is acquired in the component using the useQuery hook (i.e. not from a query in getInitialProps).
Note that my ssr setup for apollo is very similar to the official nextjs example: https://github.com/zeit/next.js/tree/canary/examples/with-apollo
The issue is that you're calling the client's query method. This method simply makes a request to the server and returns a Promise that resolves to the response. So getInitialProps is called before the page is rendered, query is called, the Promise resolves and you pass the resulting user object down to your page component as a prop. An update to your cache will not trigger getInitialProps to be ran again (although I believe navigating away and navigating back should), so the user prop will never change after the initial render.
If you want to subscribe to changes in your cache, instead of using the query method and getInitialProps, you should use the useQuery hook. You could also use the Query component or the graphql HOC to the same effect, although both of these are now deprecated in favor of the new hooks API.
export default () => {
const { data: { user } = {} } = useQuery(GetUser)
const [toggleActive] = useMutation(ToggleActive, { ... })
...
})
The getDataFromTree method (combined with setting the initial cache state) used in the boilerplate code ensures that any queries fetched for your page with the useQuery hook are ran before the page render, added to your cache and used for the actual server-side rendering.
useQuery utilizes the client's watchQuery method to create an observable which updates on changes to the cache. As a result, after the component is initially rendered server-side, any changes to the cache on the client-side will trigger a rerender of the component.

Async/Await not working as it should in Vue.js [duplicate]

I am building a Vuejs app with authentication.
When the page is loaded and I initialise the app Vuejs instance, I am using beforeCreate hook to set up the user object. I load a JWT from localStorage and send it to the backend for verification.
The issue is that this is an async call, and the components of this app object (the navbar, the views etc.) are being initialised with the empty user data before the call returns the result of the verification.
What is the best practice to delay the initialisation of child components until a promise object resolves?
Here is what I have in my Vue app object:
beforeCreate: function(){
// If token or name is not set, unset user client
var userToken = localStorage.userToken;
var userName = localStorage.userName;
if (userToken == undefined || userName == undefined) {
StoreInstance.commit('unsetUserClient');
// I WANT TO RESOLVE HERE
return;
}
// If token and name is set, verify token
// This one makes an HTTP request
StoreInstance.dispatch({type: 'verifyToken', token: userToken}).then((response) => {
// I WANT TO RESOLVE HERE
}, (fail) => {
// I WANT TO RESOLVE HERE
})
}
The current lifecycle callbacks are functions without any promises/async behaviour. Unfortunately, there does not appear to be a way to cause the app to "pause" while you load data. Instead, you might want to start the load in the beforeCreate function and set a flag, display a loading screen/skeleton with empty data, flip the flag when the data has loaded, and then render the appropriate component.

Categories

Resources