问题
I created ApolloCustomProvider
because I want to use addNotification
function from my AppContext
for graphQL error handling.
The problem is that this custom provider seems to work while querying or mutating API but errorLink
's functions like addNotification
and console.log()
doesn't work. Also console.log()
from request
function doesn't print anything.
But when I put everything from ApolloCustomProvider
to standard TypeScript file (.ts - not React Component) and then use it like:
import { ApolloProvider } from '@apollo/react-hooks'
import client from 'api/client'
...
<ApolloProvider client={client}>
<App />
</ApolloProvider>
everything works. All console.log
s prints etc. Of course without addNotification
which comes from AppContext
. Allow to use AppContext
is reason that's why I'm creating custom provider.
Files:
- index.ts
import React, { ReactElement } from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import AppContextProvider from 'contexts/AppContext'
import ApolloCustomProvider from 'components/ApolloCustomProvider'
const AppContainer = (): ReactElement => (
<AppContextProvider>
<ApolloCustomProvider>
<App />
</ApolloCustomProvider>
</AppContextProvider>
)
ReactDOM.render(<AppContainer />, document.getElementById('root'))
- ApolloCustomProvider.tsx
import React, { useContext, useEffect } from 'react'
import { ApolloProvider } from '@apollo/react-hooks'
import { ApolloClient, DefaultOptions } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { ApolloLink, Observable, Operation, NextLink } from 'apollo-link'
import { createUploadLink } from 'apollo-upload-client'
import { onError } from 'apollo-link-error'
import { AppContext } from 'contexts/AppContext'
import { Subscription } from 'apollo-client/util/Observable'
import { NotifierType } from 'components/Notifier'
import i18n from 'i18n'
const ApolloCustomProvider: React.FC = ({ children }) => {
const { addNotification } = useContext(AppContext)
useEffect(() => {
console.log('ApolloCustomProvider render')
})
const request = (operation: Operation): void => {
console.log('ApolloCustomProvider request')
const token = localStorage.getItem('at')
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : '',
},
})
}
const requestLink = new ApolloLink(
(operation: Operation, nextLink: NextLink) =>
new Observable(observer => {
console.log('gql requestLink observer')
let handle: Subscription
Promise.resolve(operation)
.then((o: Operation): void => void request(o))
.then(() => {
handle = nextLink(operation).subscribe({
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer),
})
})
.catch(observer.error.bind(observer))
return (): void => {
if (handle) handle.unsubscribe()
}
})
)
const errorLink: ApolloLink = onError(({ graphQLErrors, networkError }): void => {
console.log('ApolloCustomProvider errors', graphQLErrors, networkError)
if (graphQLErrors)
addNotification(
NotifierType.ERROR,
graphQLErrors.map(({ message }) => message)
)
if (networkError)
addNotification(NotifierType.ERROR, [
`${i18n.t('notification.networkError')}: ${networkError.message}`,
])
})
const uploadLink: ApolloLink = createUploadLink({ uri: process.env.REACT_APP_CORE_URI })
const defaultOptions: DefaultOptions = {
watchQuery: {
fetchPolicy: 'no-cache',
},
query: {
fetchPolicy: 'no-cache',
},
}
const client = new ApolloClient({
link: ApolloLink.from([requestLink, errorLink, uploadLink]),
cache: new InMemoryCache(),
defaultOptions,
})
return <ApolloProvider client={client}>{children}</ApolloProvider>
}
export default ApolloCustomProvider
回答1:
Here i'm trying to answer your question
The changes that i make is:
1. Change your function component to Class Component, the reason is when u add a notification, your whole component won't rerender. But it will rerender if u use functional component and causing spam notification on your onError.
2. Change the order of apolloLink. Here is the source https://github.com/apollographql/apollo-link/issues/133. The author said LogLink
is running first before httpLink
. He put the LogLink
after httpLink
.
import React, { useContext, useEffect } from 'react'
import { ApolloProvider } from '@apollo/react-hooks'
import { ApolloClient, DefaultOptions } from 'apollo-client'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { ApolloLink, Observable, Operation, NextLink } from 'apollo-link'
import { createUploadLink } from 'apollo-upload-client'
import { onError } from 'apollo-link-error'
import { AppContext } from 'contexts/AppContext'
import { Subscription } from 'apollo-client/util/Observable'
import { NotifierType } from 'components/Notifier'
import i18n from 'i18n'
class ApolloCustomProviderWithContext extends React.Component {
constructor(props) {
// The reason i change to class component is when u add a notification, your whole component won't rerender. But it will rerender if u use functional component and causing spam notification on your onError.
const request = (operation: Operation): void => {
console.log('ApolloCustomProvider request')
const token = localStorage.getItem('at')
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : '',
},
})
}
const requestLink = new ApolloLink(
(operation: Operation, nextLink: NextLink) =>
new Observable(observer => {
console.log('gql requestLink observer')
let handle: Subscription
Promise.resolve(operation)
.then((o: Operation): void => void request(o))
.then(() => {
handle = nextLink(operation).subscribe({
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer),
})
})
.catch(observer.error.bind(observer))
return (): void => {
if (handle) handle.unsubscribe()
}
})
)
const errorLink: ApolloLink = onError(({ graphQLErrors, networkError }): void => {
console.log('ApolloCustomProvider errors', graphQLErrors, networkError)
if (graphQLErrors)
addNotification(
NotifierType.ERROR,
graphQLErrors.map(({ message }) => message)
)
if (networkError)
addNotification(NotifierType.ERROR, [
`${i18n.t('notification.networkError')}: ${networkError.message}`,
])
})
const uploadLink: ApolloLink = createUploadLink({ uri: process.env.REACT_APP_CORE_URI })
const defaultOptions: DefaultOptions = {
watchQuery: {
fetchPolicy: 'no-cache',
},
query: {
fetchPolicy: 'no-cache',
},
}
const client = new ApolloClient({
// I change the order of this ApolloLink.from
link: ApolloLink.from([errorLink, uploadLink, requestLink]),
cache: new InMemoryCache(),
defaultOptions,
})
this._client = client
}
render () {
return (
<ApolloProvider client={this._client}>
{this.props.children}
</ApolloProvider>
)
}
}
const ApolloCustomProvider = props => {
const { addNotification } = useContext(appContext)
return <ApolloCustomProviderWithContext {...props} addNotification={addNotification} />
}
export default ApolloCustomProvider
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
来源:https://stackoverflow.com/questions/62086440/custom-apolloprovider-for-access-to-react-context-inside