Built API with NodeJS, Express & MongoDB, used JWT and Cookies for user authentication.
Fetched user data from API with axios service using store (vuex). Created auth.js in store folder, created fetchData action which GETs the data from backend (axios.get(apiRoute)) and sets the user to state.
Wanted to do this using nuxtServerInit, so i craeted index.js file in store folder. Added empty state & actions. Action containts nuxtServerInit which uses dispatch() to call fetchData method in auth.js.
Yet after all of this, it doesn't work at all. For example: User is logged in, but account page is not rendering with user data (name, email, image etc.).
I tried returning a promise from fetchData action in auth.js, and it didn't work.
Also i tried setting up fetchData action insite of the index.js file and calling dispatch directly on it.
store/auth.js
// Importing Files
import axios from 'axios';
// State
export const state = () => ({
user: null
});
// Mutations
export const mutations = {
SET_USER (store, data) {
store.user = data
},
RESET_USER (store) {
store.user = null
}
};
// Actions
export const actions = {
// Fetch User Account
async fetchData ({ commit }) {
try {
const response = await axios.get('http://localhost:3000/api/v1/users/account');
commit('SET_USER', response.data.doc);
return response;
} catch (err) {
commit('RESET_USER');
}
}
};
store/index.js
// State
export const state = () => ({
});
// Actions
export const actions = {
async nuxtServerInit({ dispatch }) {
console.log('Testing');
const res = dispatch('auth/fetchData');
return res;
}
};
components/Settings.vue
<template>
<section class="data-block-wrap" v-if="user">
<BlockHeader :blockHeaderName="`Welcome Back, ${user.name.split(' ')[0]}`" btnText="More Details" />
<img :src="getPhotoUrl(user.photo)" alt="User Photo" class="user-data__image">
<p class="user-data__short-bio">{{ user.shortBio }}</p>
</section>
</template>
<script>
export default {
// Computed
computed: {
user() {
return this.$store.state.auth.user;
}
}
...
};
</script>
I expect to render user data properly on Vue components but currently it doesn't work at all. The render is static, no data from database / api showing.
EDIT / UPDATE
App renders user data properly when calling fetchData on created() hook in default.vue file ('Parent' file for all of the components).
default.vue
<template>
<div class="container">
<TopNav />
<SideNav />
<nuxt />
</div>
</template>
// Importing Components
import TopNav from '#/components/navigation/TopNav';
import SideNav from '#/components/navigation/SideNav';
import axios from 'axios';
import { mapActions } from 'vuex';
export default {
components: {
TopNav,
SideNav
},
methods: {
// Map Actions
...mapActions('auth', ['fetchData']),
async checkUser() {
const user = await this.fetchData();
},
},
// Lifecycle Method - Created
created() {
this.checkUser();
}
}
</script>
It seems that something very interesting is happening here. The problem is calling axios.get('http://localhost:3000/api/v1/users/account') from within nuxtServerInit().
This is causing what is essentially an infinite recursion. nuxtServerInit makes a call to http://localhost:3000, which hits the same server, runs nuxtServerInit again, and calls http://localhost:3000, and so on until the javascript heap is out of memory.
Instead of using nuxtServerInit for this, use the fetch method:
The fetch method is used to fill the store before rendering the page,
it's like the asyncData method except it doesn't set the component
data.
Note: You do not have access to the Nuxt component in fetch, so you must use the context object instead of "this"
// inside your page component
export default {
fetch (context) {
return context.store.dispatch('auth/fetchData');
}
}
As a general rule:
Use fetch to fill store data on the server or client
Use asyncData to fill component data on the server or client
Use nuxtServerInit for things like setting up the store with values on the request object, like sessions, headers, cookies, etc, which is only required server side
The solution to this question is to use the NuxtServerInt Action this way inside your store.js
1. you will need to run npm install cookieparser and npm install js-cookie
const cookieparser = process.server ? require('cookieparser') : undefined
export const state = () => {
return {
auth: null,
}
}
export const mutations = {
SET_AUTH(state, auth) {
state.auth = auth
},
}
export const actions = {
nuxtServerInit({ commit }, { req }) {
let auth = null
if (req.headers.cookie) {
try {
const parsed = cookieparser.parse(req.headers.cookie)
auth = parsed.auth
} catch (err) {
console.log('error', err)
}
}
commit('SET_AUTH', auth)
},
}
Then in your login page component, you call your backend API, just like this
import AuthServices from '#/ApiServices/AuthServices.js'
import swal from 'sweetalert'
const Cookie = process.client ? require('js-cookie') : undefined
async onSubmit() {
try {
const body = {
email: this.email,
password: this.password,
}
const res = await AuthServices.loginUrl(body)
console.log('res', res)
console.log('res', res.data.message)
setTimeout(() => {
// we simulate the async request with timeout.
const auth = {
accessToken: res.data.payload.token, // from your api call, you get the user token
userData: res.data.payload.user,
}
swal('Logged in', `${res.data.message}`, 'success')
this.email = this.password = ''
this.$refs.loginForm.reset()
this.$store.commit('setAuth', auth) // mutating to store for client rendering
Cookie.set('auth', auth) // saving token in cookie for server rendering
this.$router.push('/')
}, 1000)
} catch (error) {
console.log('error', error)
swal('Error!', `${error.message}`, 'error')
}
},
your AuthServices.js looks like this
import axios from 'axios'
const apiClient = axios.create({
baseURL: `http://localhost:3000`,
})
export default {
loginUrl(body) {
return apiClient.post('/login', body, {
headers: {
'Content-Type': 'application/json',
},
})
}
}
then you get the user data using computed in the navbar or say dashboard e.g to say Hi,Xavier
inside where you want place the user data, just add this
<template>
<section>
<p class="firtname_data">Hi, {{ user.firstnam }}</p>
</section>
</template>
<script>
export default {
// Computed
computed: {
user() {
return this.$store.state.auth.userData
}
...
};
</script>
Hope this help... it worked for me
I think you forgot to write await before dispatch
export const actions = {
async nuxtServerInit({ dispatch }) {
console.log('Testing');
const res = await dispatch('auth/fetchData');
return res;
}
}
Related
With the graphql Vue code generator we created the following working query that returns the app preferences:
const { result, loading, error } = useViewerQuery()
const preference = useResult(result, null, (data) => data.viewer.preference)
We would like to fetch the preferences only once just after a user is authenticated in the MainLayout.vue component, so we can use a spinner based on the loading ref before we can render the full UI in the correct user language (which comes from the preferences).
The issue we have is to trigger the userViewerQuery only when the ref isAuthenticated is set to true in the MainLayout.vue setup function. We can't seem to expose the loading and error refs when they are within the watchEffect function.
// MainLayout.vue
<script>
import { defineComponent, watchEffect } from '#vue/composition-api'
import { isAuthenticated } from 'src/store/authStore'
import { useViewerQuery } from 'src/graphql/generated/operations'
export default defineComponent({
setup(_, { root }) {
watchEffect(() => {
if (isAuthenticated.value === true) {
const { result, loading, error } = useViewerQuery()
}
})
return {
isAuthenticated,
loading,
error,
}
},
})
</script>
Then later on we would simply like to use the preferences retrieved in the Settings.vue component. Because they are already fetched the data for preferences should be cached and readily available:
// Settings.vue
const { result } = useViewerQuery()
const preference = useResult(result, null, (data) => data.viewer.preference)
Maybe we're approaching this the wrong way. Any help or advise is welcome.
I figured it out thanks to this tip. When using useQuery with Vue Apollo there is an option called enabled that defines if a query is run or not.
From the docs:
const enabled = ref(false)
const { result } = useQuery(gql`
...
`, null, () => ({
enabled: enabled.value,
}))
function enableQuery () {
enabled.value = true
}
Applying this logic to my own situation, where I only want the query to run when the user is logged in and the ref isAuthenticated is set to true:
export default defineComponent({
setup(_, { root }) {
const { result, loading, error } = useViewerQuery(() => ({
enabled: isAuthenticated.value,
}))
watchEffect(() => {
console.log('MainLayout isAuthenticated: ', isAuthenticated.value)
})
watchEffect(() => {
console.log('MainLayout result: ', result.value)
})
return {
isAuthenticated,
loading,
error,
}
},
I hope this helps others with a similar issue.
I think you could scope up the refs and add a property that calls the gql endpoint. Something like...
export default defineComponent({
setup() {
const { result, loading, error, call } = useViewerQuery()
watchEffect(async () => {
if (isAuthenticated.value) {
await call()
}
})
return {
isAuthenticated,
loading,
error
}
}
})
I'm using redux-thunk for async actions, and all was working as expected until I added an Apollo Client into the mix, and I can't figure out why. The action is being dispatched, but the return function is not being called.
-- Client provider so that I can use the client in the Redux store outside of the components wrapped in <ApolloProvider>.
import { ApolloClient, InMemoryCache, createHttpLink } from "#apollo/client";
class ApolloClientProvider {
constructor() {
this.client = new ApolloClient({
link: createHttpLink({ uri: process.env.gqlEndpoint }),
cache: new InMemoryCache(),
});
}
}
export default new ApolloClientProvider();
-- My store setup
const client = ApolloClientProvider.client;
const persistConfig = {
key: "root",
storage: storage,
};
const pReducer = persistReducer(persistConfig, rootReducer);
const store = createStore(
pReducer,
applyMiddleware(thunk.withExtraArgument(client))
);
-- The action
export const fetchMakeCache = (token) => (dispatch, client) => {
console.log("fetching");
const query = gql`
query Source {
sources {
UID
Name
ActiveRevDestination
}
workspaces {
UID
Name
SourceUids
ActiveRevSource
}
}
`;
return () => {
console.log("reached return");
dispatch(requestMakeCache());
client
.query({
query: query,
context: {
headers: {
Authorization: `Bearer ${token}`,
},
},
})
.then((r) => r.json())
.then((data) => dispatch(receivedMakeCache(data)))
.catch((error) => dispatch(failedMakeCache(error)));
};
};
-- The component dispatching the thunk
import React from "react";
import { useAuth0 } from "#auth0/auth0-react";
import { useDispatch } from "react-redux";
import * as actions from "../store/actions";
const Fetch = () => {
const dispatch = useDispatch();
const { getAccessTokenSilently, isAuthenticated } = useAuth0();
if (isAuthenticated) {
// This will set loading = false immediately
// The async function below results in loading not being set to false
// until other components are performing actions that will error
dispatch(actions.requestMakeCache());
(async () => {
const token = await getAccessTokenSilently({
audience: process.env.audience,
});
dispatch(actions.fetchMakeCache(await token));
})();
}
return <></>;
};
export default Fetch;
When the Fetch component loads, the "fetching" console log prints so it's definitely being dispatched. But the "reached return" never gets hit. This exact same code worked as expected when not using the Apollo client. However, I've been able to use the same client successfully in a component. I'm not getting any errors, the return function just isn't being hit.
Most of the questions on here about thunks not running the return function have to do with not dispatching correctly, but I don't think that's the case since this worked pre-Apollo. (Yes, I know that using redux and Apollo together isn't ideal, but this is what I have to do right now)
I am using axios in my create-react-app. Which is the best way to use axios:
Method 1:
ajax.js
import axios from 'axios';
const axiosInstance = axios.create({});
export default axiosInstance;
app.js
import ajax from './ajax.js';
ajax.post('url');
Method 2:
ajax.js
import axios from 'axios';
class AjaxService{
constructor(apiConfig){
this.service = axios.create(apiConfig);
}
doGet(config){
return this.service.get(config.url);
}
...
}
export default AjaxService;
app.js:
import AjaxService from './ajax';
const service1 = new AjaxService();
service.doGet({url:'url'});
app2.js
import AjaxService from './ajax';
const service2 = new AjaxService();
service.doGet({url:'url'});
In method 2, we have to initialize the service wherever we make a call, which may or may not be a best practice. If we follow method 2, Is there a way to make it as a common service across the application?
i've seen a way in here and i came up with another solution like i explained below:
1 - i created my service with axios
import axios from 'axios';
const instance = axios.create({
baseURL: process.env.REACT_APP_BASE_URL
// headers: { 'X-Custom-Header': 'foobar' }
});
// Add a request interceptor
instance.interceptors.request.use(
(config) => {
// Do something before request is sent
return config;
},
(error) => {
// Do something with request error
return Promise.reject(error);
}
);
// Add a response interceptor
instance.interceptors.response.use(
(response) => {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
return response;
},
(error) => {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(error);
}
);
export default instance;
2- i use that service to create a function for api call.
in here i can add a new AbortController for later use in useEffect.
import axios from 'services/request';
export function getMarket(type, callBack) {
const controller = new AbortController();
axios
.get(`https://dev.zh1.app/api/market/map?type=${type}`, {
signal: controller.signal
})
.then((res) => {
callBack(true, res.data);
})
.catch((res) => {
callBack(false, res.response);
});
return controller;
}
export default {
getMarket
};
3- in the hooks folder i created a hook called useApi. the controller from step 2 used in here. if you check the link above you can see the author add request function because you may have some props to pass to api call. i think it is valid but ugly. so i decided to create a closure for useApi to pass any params i want to the Axios in step 2.
import { useEffect, useState } from 'react';
// in useStrict mode useEffect call twice but will not in production
export default function useApi(apiFunc) {
return function useApiCall(...params) {
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const apiCall = useCallback(() => {
setLoading(true);
// controller is new AbortController which set each api function
const controller = apiFunc(...params, (ok, data) => {
setLoading(false);
if (ok) {
setResult(data);
} else {
setError(data.message);
}
});
return () => {
controller.abort();
};
}, []);
useEffect(() => {
apiCall();
}, []);
return {result, loading, error, [apiFunc.name]: apiCall};
};
}
4- finally in my react component
import { IconButton } from '#mui/material';
import useApi from '#share/hooks/useApi';
import { Refresh } from '#share/icons';
import { getCaptcha as CaptchaApi } from 'api/oauth/captcha';
import CaptchaStyle from './style';
export default function Captcha() {
const { result: captcha, getCaptcha } = useApi(CaptchaApi)();
return (
<CaptchaStyle>
<img src={`data:image/png;base64,${captcha?.base64}`} alt="captcha" />
<IconButton onClick={getCaptcha}>
<Refresh />
</IconButton>
</CaptchaStyle>
);
}
i think this approach i quite good and if you dont need to pass any props just call useApi([yourfunction])() with empty function.
and you can have access to the function inside of useApi if you need to call it again.
It totally depends on your project. If your project relies more on the function component then go ahead and use the first approach.
If you use classes for the majority of your components go for the second approach.
I generally use the first approach, it's easy and avoids this altogether. Also, it's easy to target multiple instances.
// Ajax.js file
import axios from "axios";
export function updateData=(body,callback){
le url= 'your api to call'
axios
.put(url, body)
.then((response) => response.data)
.then((res) => {
callback(res);
})
.catch((error) => {
console.log(error);
callback('error occurred');
});
}
// app.js file
import {updateData} from './ajax.js'
//Place your code where you need
updateData(yourBodyToPass,res=>{
//Stuff after the response
)
Note:- pass your data as first argument and get response of api from second
I am using a custom NextJS server with Apollo Client. I want to fetch the GraphQL data server-side and then send it to the client. I was kind of able to do that, but the client-side fetches it again. I understand that the Apollo cache is available only on the server, then needs to be sent to the client and restored from there.
The Apollo docs mention SSR but I don't want to fully render my app using the Apollo client, I want to use NextJS, I want to get just the data from the Apollo client and manually inject it into the HTML to restore it on the client. I looked at some examples for NextJS using Apollo, but none of them showed how to do exactly that.
This is my custom handler for requests:
const app = next({ dev: process.env.NODE_ENV !== 'production' });
const customHandler = async (req, res) => {
const rendered = await app.renderToHTML(req, res, req.path, req.query);
// somehow get the data from the apollo cache and inject it in the rendered html
res.send(rendered);
}
When you create the ApolloClient in the server, you can pass the initialState to hydrate the cache.
const createApolloClient = ({ initialState, headers }) =>
new ApolloClient({
uri: GRAPHQL_URL,
cache: new InMemoryCache().restore(initialState || {}) // hydrate cache
});
export default withApollo(PageComponent, { ssr = true } = {}) => {
const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
const client = apolloClient || createApolloClient({ initialState: apolloState, headers: {} });
... rest of your code.
});
});
I have created a package just for this called nextjs-with-apollo. Take a look at https://github.com/adikari/nextjs-with-apollo. Once you have installed the package create a HOC as such.
// hocs/withApollo.js
import withApollo from 'nextjs-with-apollo';
import ApolloClient from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
const GRAPHQL_URL = 'https://your-graphql-url';
const createApolloClient = ({ initialState, headers }) =>
new ApolloClient({
uri: GRAPHQL_URL,
cache: new InMemoryCache().restore(initialState || {}) // hydrate cache
});
export default withApollo(createApolloClient);
Then you can use the hoc for your next page as such.
import React from 'react';
import { useQuery } from '#apollo/react-hooks';
import withApollo from 'hocs/withApollo';
const QUERY = gql`
query Profile {
profile {
name
displayname
}
}
`;
const ProfilePage = () => {
const { loading, error, data } = useQuery(PROFILE_QUERY);
if (loading) {
return <p>loading..</p>;
}
if (error) {
return JSON.stringify(error);
}
return (
<>
<p>user name: {data.profile.displayname}</p>
<p>name: {data.profile.name}</p>
</>
);
};
export default withApollo(ProfilePage);
I am using VueJS in conjunction with vuex and vue-router. I have a vuex module that is making a mutation to its store, and trying to use that to determine whether or not a user is authenticated.
Here is what my code looks like in relevant part.
main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
import router from './router'
router.beforeEach((to, from, next) => {
console.log(router.app) // prints a Vue$2 object
console.log(router.app.$store) // undefined
console.log(store.getters.isAuthenticated) // false
...
}
const app = new Vue({
store,
router,
...App
})
app.$mount('#app')
/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import core from './modules/core'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
core: core
}
})
export default store
/store/modules/core.js
import * as types from '../types'
import api from '../../api'
import router from '../../router'
const state = {
token: null,
user: null,
authenticated: false
}
const mutations = {
[types.LOGIN_SUCCESS] (state, payload) {
console.log('mutate')
state.token = payload.token
state.user = payload.user
state.authenticated = true
router.go('/')
}
}
const getters = {
isAuthenticated: state => {
return state.authenticated
}
}
const actions = {
[types.LOGIN] (context, payload) {
api.getToken(payload).then(response => {
context.commit(types.LOGIN_SUCCESS, response)
})
}
}
export default {
state,
mutations,
actions,
getters
}
When I go thru my logic to trigger the LOGIN action, I can see that the mutation executed properly, and when I use the Chrome extension to view the vuex state for my core module, the state for user and authenticated have been properly mutated.
QUESTION
It seems like this module just simply has not been loaded by the time the router is running in the .beforeEach loop. Is this true?
If yes, what are some other suggestions on how to handle this situation?
If no, what am I doing incorrect?
console.log(store.state.core.authenticated) return false because you not make a login yet.
In your code you not persist user info in anywhere. E.g. using localstorage
Same considerations:
Not use router.app.$store, use store that you import
In your LOGIN_SUCCESS mutation, store login info and token into localstorage
In your beforeEach hook, check localstorage, if was populated with token, get user information and apply the mutation. If not, just call login page
Something like this..
const mutations = {
[types.LOGIN_SUCCESS] (state, payload) {
state.token = payload.token
state.user = payload.user
state.authenticated = true
localstorage.setItem('token', payload.token)
localstorage.setItem('user', payload.user)
}
}
const actions = {
[types.LOGIN] (context, payload) {
return api.getToken(payload).then(response => {
context.commit(types.LOGIN_SUCCESS, response)
return response
})
}
}
router.beforeEach((to, from, next) => {
let user = localstorage.getItem('user')
let token = localstorage.getItem('token')
if (user && token) {
store.commit(types.LOGIN_SUCCESS, {token, user})
next()
}
else if (!store.getters.isAuthenticated) {
store.dispatch(types.LOGIN).then(() => next())
} else {
next()
}
}