主要问题是 react hook 不能在函数组件之外使用。但是,ApolloClient 的初始化发生在组件之外,它需要访问令牌来调用后端的 graphql API,这可以通过调用 getTokenSilently() 方法来实现。为了解决这个问题,我手动导出了getTokenSilently() 方法(在Auth0Provider 之外)。
例如:
import React, { useState, useEffect, useContext } from "react";
import createAuth0Client from "@auth0/auth0-spa-js";
const DEFAULT_REDIRECT_CALLBACK = () =>
window.history.replaceState({}, document.title, window.location.pathname);
export const Auth0Context = React.createContext();
export const useAuth0 = () => useContext(Auth0Context);
let _initOptions, _client
const getAuth0Client = () => {
return new Promise(async (resolve, reject) => {
let client
if (!client) {
try {
client = await createAuth0Client(_initOptions)
resolve(client)
} catch (e) {
console.log(e);
reject(new Error('getAuth0Client Error', e))
}
}
})
}
export const getTokenSilently = async (...p) => {
if(!_client) {
_client = await getAuth0Client()
}
return await _client.getTokenSilently(...p);
}
export const Auth0Provider = ({
children,
onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
...initOptions
}) => {
const [isAuthenticated, setIsAuthenticated] = useState();
const [user, setUser] = useState();
const [auth0Client, setAuth0] = useState();
const [loading, setLoading] = useState(true);
const [popupOpen, setPopupOpen] = useState(false);
useEffect(() => {
const initAuth0 = async () => {
_initOptions = initOptions;
const client = await getAuth0Client(initOptions)
setAuth0(client)
// const auth0FromHook = await createAuth0Client(initOptions);
// setAuth0(auth0FromHook);
if (window.location.search.includes("code=")) {
console.log("Found code")
const { appState } = await client.handleRedirectCallback();
onRedirectCallback(appState);
}
const isAuthenticated = await client.isAuthenticated();
setIsAuthenticated(isAuthenticated);
if (isAuthenticated) {
const user = await client.getUser();
setUser(user);
}
setLoading(false);
};
initAuth0();
// eslint-disable-next-line
}, []);
const loginWithPopup = async (params = {}) => {
setPopupOpen(true);
try {
await auth0Client.loginWithPopup(params);
} catch (error) {
console.error(error);
} finally {
setPopupOpen(false);
}
const user = await auth0Client.getUser();
setUser(user);
setIsAuthenticated(true);
};
const handleRedirectCallback = async () => {
setLoading(true);
await auth0Client.handleRedirectCallback();
const user = await auth0Client.getUser();
setLoading(false);
setIsAuthenticated(true);
setUser(user);
};
return (
<Auth0Context.Provider
value={{
isAuthenticated,
user,
loading,
popupOpen,
loginWithPopup,
handleRedirectCallback,
getIdTokenClaims: (...p) => auth0Client.getIdTokenClaims(...p),
loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p),
getTokenSilently: (...p) => auth0Client.getTokenSilently(...p),
getTokenWithPopup: (...p) => auth0Client.getTokenWithPopup(...p),
logout: (...p) => auth0Client.logout(...p)
}}
>
{children}
</Auth0Context.Provider>
);
};
现在,没有限制,我们可以在函数组件或类组件或任何其他地方调用getTokenSilently()方法。
我使用以下代码初始化ApolloClient,并在调用ApolloProvider 时传递客户端。
import React from "react";
import { Router, Route, Switch } from "react-router-dom";
import { Container } from "reactstrap";
import PrivateRoute from "./components/PrivateRoute";
import Loading from "./components/Loading";
import NavBar from "./components/NavBar";
import Footer from "./components/Footer";
import Home from "./views/Home";
import Profile from "./views/Profile";
import { useAuth0 } from "./react-auth0-spa";
import history from "./utils/history";
import "./App.css";
import { ApolloProvider } from '@apollo/react-hooks';
import initFontAwesome from "./utils/initFontAwesome";
import { InMemoryCache } from "apollo-boost";
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { ApolloLink, Observable } from 'apollo-link';
import { onError } from 'apollo-link-error';
import { withClientState } from 'apollo-link-state';
import {getTokenSilently} from "./react-auth0-spa";
initFontAwesome();
let API_URL="https://[BACKEND_GRAPHQL_API_URL]/graphql";
const cache = new InMemoryCache();
cache.originalReadQuery = cache.readQuery;
cache.readQuery = (...args) => {
try {
return cache.originalReadQuery(...args);
} catch (err) {
return undefined;
}
};
const request = async (operation) => {
const token = await getTokenSilently();
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : ''
}
});
};
const requestLink = new ApolloLink((operation, forward) =>
new Observable(observer => {
let handle;
Promise.resolve(operation)
.then(oper => request(oper))
.then(() => {
handle = forward(operation).subscribe({
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer),
});
})
.catch(observer.error.bind(observer));
return () => {
if (handle) handle.unsubscribe();
};
})
);
const client = new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
console.log("Graphqlerrors"+graphQLErrors)
// sendToLoggingService(graphQLErrors);
}
if (networkError) {
console.log("Network error"+networkError)
// logoutUser();
}
}),
requestLink,
withClientState({
defaults: {
isConnected: true
},
resolvers: {
Mutation: {
updateNetworkStatus: (_, { isConnected }, { cache }) => {
cache.writeData({ data: { isConnected }});
return null;
}
}
},
cache
}),
new HttpLink({
uri: API_URL,
// credentials: 'include'
})
]),
cache
});
const App = () => {
const { loading } = useAuth0();
if (loading) {
return <Loading />;
}
return (
<ApolloProvider client={client}>
<Router history={history}>
<div id="app" className="d-flex flex-column h-100">
<NavBar />
<Container className="flex-grow-1 mt-5">
<Switch>
<Route path="/" exact component={Home} />
<PrivateRoute path="/profile" component={Profile} />
</Switch>
</Container>
<Footer />
</div>
</Router>
</ApolloProvider>
);
};
export default App;