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);
Related
So, I'm fetching data from an API that is built in Liferay (json web service) and in which I'm to integrate into a React frontend. Right now I'm at the stage where I need to authenticate a user, and I'm halfway there. I'm currently sending a post-request with the Fetch API, to a specific endpoint that is defined in the backend. From there I get back some user data in JSON format, but also a JSESSIONID cookie. I'm mostly used to using json web tokens for authentication and basically what I need to know how to use this session cookie in a similar way.
I'm thinking in my mind I want to store the cookie in a state/context, and let the application determine if there is a session, and only then be able to access protected routes, etc, but I'm not entirely sure this is the way you do it.
I would really be glad if someone could point me in the right direction.
The context to verify that the user is authenticated.
import * as React from 'react'
export const AuthContext = React.createContext({})
const AuthProvider = ({
children
}) => {
const [dataUser, setDataUser] = React.useState({})
const Data = async () => {
const user = await fetch('url')
const resp = await user.json()
setDataUser(resp)
//cookies or localStorage
localStorage.setItem('user', resp)
}
React.useEffect(() => {
Data()
}, [])
const existUser = () => {
return JSON.parse(localStorage.user).id === dataUser.id
}
const Auth = existUser()
return(
<AuthContext.Provider value={{dataUser, setDataUser, Auth}}>
{children}
</AuthContext.Provider>
)
}
export default AuthProvider
put the provider in the app
const App = () => {
return(
<AuthProvider>
<Pages />
</AuthProvider>
)
}
export default App
build the pages that need authentication, using useContext, or a custom react hook.
const DashBoard = ({}) => {
const { Auth } = React.useContext(AuthContext)
const isAuth = () => {
Auth ?? window.location('url') // or react router history
}
React.useEffect(() => {
isAuth()
}, [])
return(
<>
{Auth && //optional
<div>
<h1>DashBoard</h1>
</div>
}
</>
)
}
I'm new to React and I have this function.
import Axios from "axios";
const UserService = {
getUserRole: (access_token: string = "") => {
return Axios({
method: "get",
url: "https://<url>/user/role",
headers: {
"Authorization": `Bearer ${access_token}`
}
}).then((response) => {
return response.data;
}).catch((error) => {
console.log(error);
});
}
}
export default UserService
The getUserRole is used constantly by another component, for example
import UserService from "../../../services/authentication/userService";
import { useAuth } from "react-oidc-context";
...
const auth = useAuth();
UserService.getUserRole(auth.user?.access_token);
As you can see, I have to constantly pass the access_token from useAuth. Is there any way I can call useAuth inside my UserService so I don't have to constantly pass the access_token from my component?
The premise of the question is backward, as we shouldn't try to use hooks outside of React, but instead use outside code inside of React.
Quick solution: Custom hook
If the roles are used all over the place, a quick custom hook will get you started. This is the easiest way to wrap custom logic as hooks are meant to wrap stateful logic for reuse in components.
import { useState, useEffect } from "react";
import { useAuth } from "react-oidc-context";
import UserService from "../../../services/authentication/userService";
/**
* Custom hooks that fetches the roles for the logged in user.
*/
const useRoles = () => {
const auth = useAuth();
const [roles, setRoles] = useState();
useEffect(() => {
if (!user) return; // pre-condition
UserService
.getUserRole(auth.user.access_token)
.then(setRoles);
}, [auth.user]);
return roles;
}
Then in any component:
import useRoles from "../useRoles";
const MyExampleComponent = () => {
const roles = useRoles();
if (!roles) return <span>Please login (or something) to see the roles!</span>
return <div>{/* use roles here */}</div>
}
Better solution: Service provider
If there's a lot of different methods on the user service that needs to be used all over the app, then wrapping the whole service and providing a ready-to-use version through React's context would be best in my opinion.
But first, let's rework the UserService a little so that it uses a local axios instance instead of the global axios instance.
// I also made it a class, but it would also work with an object.
class UserService {
constructor(axios) {
this.axios = axios;
}
getUserRole(){
// use the local axios instance
return this.axios({
method: "get",
// Use the default URL from local axios instance
url: "user/role",
})
.then(({ data }) => data)
.catch(console.log),
}
getSomethingElse() {
// ...
}
}
Then, we can setup the React's context for the user service.
// UserServiceContext.js
import React from 'react';
import { useAuth } from "react-oidc-context";
import UserService from "../../../services/authentication/userService";
// Local axios instance
const axiosInstance = axios.create({
baseURL: 'https://<url>', // set the base URL once here
});
const userServiceInstance = new UserService(axiosInstance);
const UserServiceContext = React.createContext(userServiceInstance);
// Convenience hook
export const useUserService = () => useContext(UserServiceContext);
export const UserServiceProvider = (props) => {
const auth = useAuth();
useEffect(() => {
// If the user changes, update the token used by our local axios instance.
axiosInstance.defaults.headers
.common['Authorization'] = `Bearer ${auth.user?.access_token}`;
}, [auth.user]);
return (
<UserServiceContext.Provider value={userServiceInstance} {...props} />
);
}
Then anywhere, but commonly at the App's root:
import { AuthProvider } from "react-oidc-context";
import { UserServiceProvider } from "./UserServiceContext";
const App = () => (
<AuthProvider>
<UserServiceProvider>
<Content />
</UserServiceProvider>
</AuthProvider>
);
Now everything is ready to be used in any component!
import { useUserService } from '../UserServiceContext';
const MyExampleComponent = () => {
const userService = useUserService();
const [roles, setRoles] = useState();
// e.g. load roles once on mount.
useEffect(() => {
userService // use the service from the context
.getUserRole() // no auth token needed anymore!
.then(setRoles);
}, []);
if (!roles) return <span>Please login (or something) to see the roles!</span>
return <div>{/* use roles here */}</div>
}
Note that a custom hook could still be used to wrap the roles fetching logic. Both the context and hooks can be used together to wrap logic to each's own preferences.
// Here's what the hook could look like if it used the new provider above.
const useRoles = () => {
const userService = useUserService();
const [roles, setRoles] = useState();
// e.g. load roles once on mount.
useEffect(() => {
userService // use the service from the context
.getUserRole() // no auth token needed anymore!
.then(setRoles);
}, []);
return roles;
}
I consider the provider solution to be better since it provides more flexibility while keeping control over the exposed API.
In my solution, I suggest using the UserService instance as the provided value, but the provider could be changed to expose only parts of the API, or it could provide the roles and other data automatically. It's up to you!
Disclaimer: I've used minimal code to demonstrate a working solution and my answer may not address all constraints of your situation. For example, the axios instance could be created inside the provider as a lazy initialized useRef, same thing goes for the UserService instance, etc.
Situation
Due to the compatible issue, I am using ApolloClient to form my AWS AppSync API client, so that I can use ApolloProvider to pass the client to different sub-component.
In the sub-component, I call useQuery to fetch data, but the problem is the output always give me default "undefined" value.
Code in Top Level
import awsconfig from '../aws-exports';
import { AUTH_TYPE } from 'aws-appsync';
import {
ApolloProvider,
ApolloClient,
ApolloLink,
HttpLink,
InMemoryCache,
} from "#apollo/client";
import { createAuthLink } from "aws-appsync-auth-link";
import { getUserDb, listUserDbs } from '../graphql/queries';
const link = ApolloLink.from([
createAuthLink({
url: awsconfig.aws_appsync_graphqlEndpoint,
region: awsconfig.aws_appsync_region,
auth: {
type: AUTH_TYPE.API_KEY,
apiKey: awsconfig.aws_appsync_apiKey,
},
}),
new HttpLink({ uri: awsconfig.aws_appsync_graphqlEndpoint }),
]);
const client = new ApolloClient({
link,
cache: new InMemoryCache(),
credentials: 'include'
});
function WithProvider (){
return (
<ApolloProvider client={client}>
<App />
</ApolloProvider>
)
}
export default WithProvider;
Code in sub-Component (usage of useQuery)
import { listUserDbs } from '../graphql/queries';
const GET_USER_PROFILE = gql(listUserDbs)
function App() {
const { loading, error, out_data, client } = useQuery(GET_USER_PROFILE);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
What test have I done?
when I call query straight from client, it worked
client
.query({
query: gql`
query ListUserDbs(
$filter: Table
) {
listUserDBS(filter: $filter) {
items {
sub
}
nextToken
}
}
`
})
.then(result => console.log(result));
when I ask useQuery to return client, it returns the same client I set up
const { loading, error, out_data, client } = useQuery(LIST_USER_PROFILE);
My guess of the issue
The issue is probably coming from useQuery but I don't know how to solve it.
Maybe you need data instead of out_data?
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)
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;
}
}