Last active
September 10, 2018 07:34
-
-
Save RabidFire/30ce6047a628f2b6a1e20c7db1136315 to your computer and use it in GitHub Desktop.
React + Apollo Tricks & Boilerplate
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ApolloLink } from 'apollo-link' | |
import cache from 'client/cache' | |
import GET_TOKEN from 'queries/session' | |
const authLink = new ApolloLink((operation, forward) => { | |
const { session: { token } = {} } = cache.readQuery({ query: GET_TOKEN }) | |
const headers = {} | |
if (token) { | |
headers['X-Token'] = token | |
} | |
operation.setContext({ headers }) | |
return forward(operation) | |
}) | |
export default authLink |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { InMemoryCache } from 'apollo-cache-inmemory' | |
const cache = new InMemoryCache() | |
export default cache |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { onError } from 'apollo-link-error' | |
import { logout } from 'client/methods' | |
const errorLink = onError(({ networkError }) => { | |
if (networkError && networkError.statusCode === 401) { | |
logout() | |
} | |
}) | |
export default errorLink |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { HttpLink } from 'apollo-link-http' | |
const httpLink = new HttpLink({ | |
uri: `${process.env.API_BASE_URL}/graphql` | |
}) | |
export default httpLink |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ApolloClient from 'apollo-client' | |
import { from } from 'apollo-link' | |
import authLink from './authLink' | |
import cache from './cache' | |
import errorLink from './errorLink' | |
import httpLink from './httpLink' | |
import stateLink from './stateLink' | |
const client = new ApolloClient({ | |
link: from([ | |
authLink, | |
errorLink, | |
stateLink, | |
httpLink | |
]), | |
cache | |
}) | |
/* | |
https://www.apollographql.com/docs/link/links/state.html#defaults | |
https://www.apollographql.com/docs/react/advanced/caching.html#reset-store | |
The cache is not reset back to defaults set in `stateLink` on client.resetStore(). | |
Therefore, we need to rehydrate cache with defaults using `onResetStore()`. | |
*/ | |
client.onResetStore(stateLink.writeDefaults) | |
export default client |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import client from 'client' | |
import { ALERT_FAILURE, ALERT_SUCCESS } from 'mutations/alert' | |
const logout = () => { client.resetStore() } | |
const showAlertFailure = alert => client.mutate({ mutation: ALERT_FAILURE, variables: { alert } }) | |
const showAlertSuccess = alert => client.mutate({ mutation: ALERT_SUCCESS, variables: { alert } }) | |
export { logout, showAlertFailure, showAlertSuccess } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { withClientState } from 'apollo-link-state' | |
import cache from './cache' | |
import resolvers from '../resolvers' | |
const stateLink = withClientState({ | |
cache, | |
...resolvers | |
}) | |
export default stateLink |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import gql from 'graphql-tag' | |
import React from 'react' | |
import LoginForm from 'components/forms/LoginForm' | |
import { withClientMutation, withMutation } from 'lib/data' | |
import SET_TOKEN from 'mutations/session' | |
function LoginPage({ createSession, setToken }) { | |
const handleSubmit = (values, formApi, callback) => ( | |
createSession(values, formApi, callback) | |
.then(response => setToken({ token: response.data.session.token })) | |
) | |
return ( | |
<LoginForm onSubmit={handleSubmit} /> | |
) | |
} | |
LoginPage = withMutation(gql` | |
mutation CreateSessionMutation($input: CreateSessionInput!) { | |
session: createSession(input: $input) { | |
id | |
token | |
} | |
} | |
`)(LoginPage) | |
LoginPage = withClientMutation(SET_TOKEN)(LoginPage) | |
export default LoginPage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { compose, graphql } from 'react-apollo' | |
import { filter } from 'graphql-anywhere' | |
import parseError from 'lib/errorParser' | |
import { showAlertFailure, showAlertSuccess } from 'client/methods' | |
const OptimisticResponseModes = Object.freeze({ | |
CREATE: 'CREATE', | |
UPDATE: 'UPDATE', | |
DESTROY: 'DESTROY' | |
}) | |
const MutationResponseModes = Object.freeze({ | |
IGNORE: 'IGNORE', | |
APPEND: 'APPEND', | |
PREPEND: 'PREPEND', | |
DELETE: 'DELETE' | |
}) | |
// Helpers | |
const findMutationMethodName = mutation => ( | |
mutation.definitions[0].selectionSet.selections[0].name.value | |
) | |
const findMutationMethodNameOrAlias = (mutation) => { | |
const selection = mutation.definitions[0].selectionSet.selections[0] | |
return (selection.alias && selection.alias.value) || selection.name.value | |
} | |
const findQueryFieldNameOrAlias = (query) => { | |
const selection = query.definitions[0].selectionSet.selections[0] | |
return (selection.alias && selection.alias.value) || selection.name.value | |
} | |
const addRecords = ( | |
currentRecords = [], | |
responseRecords = [], | |
mode = MutationResponseModes.APPEND | |
) => { | |
switch (mode) { | |
case MutationResponseModes.APPEND: | |
return currentRecords.push(...responseRecords) | |
case MutationResponseModes.PREPEND: | |
return currentRecords.unshift(...responseRecords) | |
default: | |
throw new Error('Incorrect `mode` specified when using addRecords.') | |
} | |
} | |
const deleteRecords = (currentRecords = [], responseRecords = [], key = 'id') => { | |
const deleteIds = responseRecords.map(record => record[key]) | |
deleteIds.forEach((deleteId) => { | |
const index = currentRecords.findIndex(record => record[key] === deleteId) | |
if (index !== -1) { | |
currentRecords.splice(index, 1) | |
} | |
}) | |
} | |
const optimisticCreateResponse = (other = {}) => ({ input }) => ({ | |
id: Math.round(Math.random() * -1000000), | |
createdAt: +new Date(), | |
updatedAt: +new Date(), | |
...input, | |
...other | |
}) | |
const optimisticUpdateResponse = (other = {}) => ({ id, input }) => ({ | |
id, | |
updatedAt: +new Date(), | |
...input, | |
...other | |
}) | |
const optimisticDestroyResponse = (other = {}) => ({ id }) => ({ | |
id, | |
...other | |
}) | |
// React apollo HOCs | |
const withClientMutation = (mutation) => { | |
const methodName = findMutationMethodName(mutation) | |
return graphql(mutation, { | |
props: ({ mutate }) => ({ | |
[methodName]: variables => mutate({ variables }) | |
}) | |
}) | |
} | |
const withClientQuery = (query, config = {}) => { | |
const configOptions = config.options || {} | |
delete config.options | |
const configWithDefaults = Object.assign({ | |
props: ({ data }) => ({ ...data }), | |
options: (props) => { | |
const defaultOptions = { | |
fetchPolicy: 'cache-only' | |
} | |
if (typeof configOptions === 'function') { | |
return { | |
...defaultOptions, | |
...configOptions(props) | |
} | |
} | |
return { | |
...defaultOptions, | |
...configOptions | |
} | |
} | |
}, config) | |
if (configWithDefaults.name) { // Automatically set by the field. | |
throw new Error('You cannot override the `name` when using `withClientQuery`.') | |
} | |
return graphql(query, configWithDefaults) | |
} | |
const withMutation = (mutation, { | |
query: mutationQuery, | |
mode = MutationResponseModes.IGNORE, | |
inputFilter, | |
refetch = false, | |
optimistic, | |
successAlert | |
} = {}) => { | |
const methodName = findMutationMethodName(mutation) | |
return graphql(mutation, { | |
props: ({ mutate, ownProps: { query: componentQuery, variables } }) => ({ | |
[methodName]: (values, formApi, callback) => { | |
/* | |
https://github.com/final-form/final-form/blob/master/src/FinalForm.js#L864 | |
https://www.ecma-international.org/ecma-262/6.0/#sec-function-definitions-static-semantics-expectedargumentcount | |
[methodName]: ({ id, ...input } = {}, formApi, callback) | |
Destructuring like the above statement will not work with react-final-form | |
because final-form uses `function.length` on onSubmit | |
to determine if `callback` will be called. | |
And as per the ecma specs, internally, parameters without default that appear after | |
one with default are ‘considered to be optional with undefined as their default value’. | |
const fn1 = (a, b) => { } | |
const fn2 = (a, ...b) => { } | |
const fn3 = (a, b = 2) => { } | |
const fn4 = (a = 1, b) => { } | |
fn1.length // -> 2 | |
fn2.length // -> 1 | |
fn3.length // -> 1 | |
fn4.length // -> 0 | |
*/ | |
const { id, ...rawInput } = values || {} | |
const input = inputFilter ? filter(inputFilter, rawInput) : rawInput | |
const mutationConfig = { | |
variables: { id, input }, | |
errorPolicy: 'none' // Ensure all errors are in catch block | |
} | |
const responseName = findMutationMethodNameOrAlias(mutation) | |
const query = mutationQuery || componentQuery | |
if (optimistic) { | |
const { mode: optimisticMode, response: optimisticResponse } = optimistic | |
if (optimisticResponse) { | |
const { __typename } = optimisticResponse | |
let response = null | |
if (!optimisticMode || !__typename) { | |
throw new Error('You must specify both `mode` and `__typename` for `optimistic` shorthand.') | |
} | |
if (!Object.prototype.hasOwnProperty.call(OptimisticResponseModes, optimisticMode)) { | |
throw new Error('Incorrect mode specified when using `optimistic` shorthand.') | |
} | |
if (optimisticMode === OptimisticResponseModes.CREATE) { | |
response = optimisticCreateResponse({ __typename }) | |
} | |
if (optimisticMode === OptimisticResponseModes.UPDATE) { | |
response = optimisticUpdateResponse({ __typename }) | |
} | |
if (optimisticMode === OptimisticResponseModes.DESTROY) { | |
response = optimisticDestroyResponse({ __typename }) | |
} | |
mutationConfig.optimisticResponse = { | |
__typename: 'Mutation', | |
[responseName]: response({ id, input: rawInput }) | |
} | |
} | |
} | |
if (mode !== MutationResponseModes.IGNORE) { | |
mutationConfig.update = (cache, { data }) => { | |
const currentRecords = cache.readQuery({ query, variables }) | |
const fieldName = findQueryFieldNameOrAlias(query) | |
/* | |
Convert response to array for mutations like BatchCreate* | |
*/ | |
const responseRecords = (data[responseName].constructor === Array) | |
? data[responseName] : [ data[responseName] ] | |
if (mode === MutationResponseModes.APPEND || mode === MutationResponseModes.PREPEND) { | |
addRecords(currentRecords[fieldName], responseRecords, mode) | |
} else if (mode === MutationResponseModes.DELETE) { | |
deleteRecords(currentRecords[fieldName], responseRecords) | |
} | |
cache.writeQuery({ data: currentRecords, query, variables }) | |
} | |
} | |
if (refetch) { | |
mutationConfig.refetchQueries = [ { query, variables } ] | |
} | |
return mutate({ ...mutationConfig }) | |
.then((response) => { | |
if (successAlert) { | |
showAlertSuccess(typeof successAlert === 'function' ? successAlert({ response, input }) : successAlert) | |
} | |
/* | |
This check is for cases like `destroySession` | |
where no arguments are passed to the mutation method. | |
Therefore, `formApi` and `callback` will be null. | |
*/ | |
if (typeof callback === 'function') { | |
callback() // Call to notify final-form of success | |
} | |
return Promise.resolve(response) | |
}).catch((error) => { | |
const { alert, submissionError } = parseError(error) | |
if (submissionError) { | |
if (typeof callback === 'function') { | |
callback(submissionError) | |
} | |
return Promise.reject(submissionError) | |
} | |
if (alert) { | |
showAlertFailure(alert) | |
} | |
if (typeof callback === 'function') { | |
callback(error) | |
} | |
return Promise.reject(error) | |
}) | |
} | |
}) | |
}) | |
} | |
const withQuery = (query, config = {}) => { | |
const configOptions = config.options || {} | |
delete config.options | |
const configWithDefaults = Object.assign({ | |
props: ({ data }) => ({ ...data, query }), | |
options: (props) => { | |
const defaultOptions = { | |
fetchPolicy: 'cache-and-network', | |
errorPolicy: 'none' | |
} | |
if (typeof configOptions === 'function') { | |
return { | |
...defaultOptions, | |
...configOptions(props) | |
} | |
} | |
return { | |
...defaultOptions, | |
...configOptions | |
} | |
} | |
}, config) | |
if (configWithDefaults.name) { // Automatically set by the field. | |
throw new Error('You cannot override the `name` when using `withQuery`.') | |
} | |
return graphql(query, configWithDefaults) | |
} | |
/* | |
This function was created to handle components like `Header.js` wherein we need | |
to break a query into multiple queries. | |
Sample use cases - | |
1. You can skip one of the queries without affecting others within the component. | |
2. Pass an individual query as argument to withMutation HOC to handle update | |
*/ | |
const withQueries = queries => compose(queries.map(({ query, config }) => withQuery(query, config))) | |
export { | |
MutationResponseModes, | |
OptimisticResponseModes, | |
withClientMutation, | |
withClientQuery, | |
withMutation, | |
withQueries, | |
withQuery | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { FORM_ERROR, setIn } from 'final-form' | |
const errorTypes = Object.freeze({ | |
UNPROCESSABLE_ENTITY: 'UNPROCESSABLE_ENTITY', | |
UNAUTHORIZED: 'UNAUTHORIZED', | |
FORBIDDEN: 'FORBIDDEN', | |
NOT_FOUND: 'NOT_FOUND', | |
INTERNAL_SERVER_ERROR: 'INTERNAL_SERVER_ERROR' | |
}) | |
/* | |
https://www.apollographql.com/docs/link/links/http.html#Errors | |
https://github.com/apollographql/apollo-link/tree/master/packages/apollo-link-error | |
*/ | |
const parseSubmissionError = graphQLErrors => ( | |
graphQLErrors.reduce((errors, { type, path, message }) => { | |
if (type && type === errorTypes.UNPROCESSABLE_ENTITY && path) { | |
const field = path.join('.') | |
if (field === 'base') { | |
return setIn(errors, FORM_ERROR, message) | |
} | |
return setIn(errors, field, message) | |
} | |
return errors | |
}, {}) | |
) | |
const parseError = ({ networkError, graphQLErrors = [] }) => { | |
let message = null | |
let submissionError = null | |
if (networkError) { | |
const { response, result: { exception, errors } = {} } = networkError | |
if (!response) { | |
// Server is down / No internet. | |
message = 'Please try again after some time.' | |
} else if (exception) { | |
/* | |
Internal Server Errors with status 500. | |
For eg: `#<ActiveRecord::PendingMigrationError: ...>` | |
In development, they are not formatted and therefore are not part of the `errors` key. | |
*/ | |
message = exception | |
} else if (errors) { | |
errors.forEach(({ type, path, message: errorMessage }) => { | |
if (type === errorTypes.UNAUTHORIZED) { | |
message = errorMessage || 'You seem to have logged out.' | |
} else if (type === errorTypes.FORBIDDEN) { | |
message = errorMessage || 'You are not allowed to do that.' | |
} else if (type === errorTypes.NOT_FOUND) { | |
message = errorMessage || 'That resource does not exist.' | |
} else if (type === errorTypes.INTERNAL_SERVER_ERROR) { | |
message = errorMessage || 'Something went wrong. Our engineers are looking into it.' | |
} else if (type === errorTypes.UNPROCESSABLE_ENTITY && (!path || path === '')) { | |
message = errorMessage | |
} | |
}) | |
if (!message) { | |
submissionError = parseSubmissionError(errors) | |
} | |
} | |
} else { | |
/* | |
Errors with status 200 | |
For eg: Variable input of type `*Input!` was provided invalid value | |
*/ | |
graphQLErrors.forEach(({ message: errorMessage }) => { | |
if (process.env.NODE_ENV === 'development') { | |
message = errorMessage | |
} else { | |
message = "We've been notified. Please try after some time." | |
} | |
}) | |
} | |
const alert = { message } | |
return { alert, submissionError } | |
} | |
export default parseError |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import gql from 'graphql-tag' | |
const ALERT_FAILURE = gql` | |
mutation OpenFailureAlertMutation($alert: AlertInput!) { | |
openFailureAlert(alert: $alert) @client | |
} | |
` | |
const ALERT_SUCCESS = gql` | |
mutation OpenSuccessAlertMutation($alert: AlertInput!) { | |
openSuccessAlert(alert: $alert) @client | |
} | |
` | |
const CLOSE_ALERT = gql` | |
mutation CloseAlertMutation { | |
closeAlert @client | |
} | |
` | |
export { ALERT_FAILURE, ALERT_SUCCESS, CLOSE_ALERT } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import gql from 'graphql-tag' | |
const SET_TOKEN = gql` | |
mutation SetTokenMutation($token: String!) { | |
setToken(token: $token) @client | |
} | |
` | |
export default SET_TOKEN |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import gql from 'graphql-tag' | |
const GET_ALERT = gql` | |
query AlertQuery { | |
alert @client { | |
isOpen | |
icon | |
message | |
title | |
variant | |
} | |
} | |
` | |
export default GET_ALERT |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import gql from 'graphql-tag' | |
const GET_TOKEN = gql` | |
query SessionQuery { | |
session @client { | |
token | |
} | |
} | |
` | |
export default GET_TOKEN |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment