graphql-hooks-with-get-support
Version:
550 lines (455 loc) • 15.4 kB
JavaScript
import React from 'react';
import { extractFiles } from 'extract-files';
import deepEqual from 'dequal';
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function ownKeys(object, enumerableOnly) {
var keys = Object.keys(object);
if (Object.getOwnPropertySymbols) {
var symbols = Object.getOwnPropertySymbols(object);
if (enumerableOnly) symbols = symbols.filter(function (sym) {
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
});
keys.push.apply(keys, symbols);
}
return keys;
}
function _objectSpread2(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? arguments[i] : {};
if (i % 2) {
ownKeys(source, true).forEach(function (key) {
_defineProperty(target, key, source[key]);
});
} else if (Object.getOwnPropertyDescriptors) {
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
} else {
ownKeys(source).forEach(function (key) {
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
});
}
}
return target;
}
var ClientContext = React.createContext();
ClientContext.displayName = 'ClientContext';
var GraphQLClient =
/*#__PURE__*/
function () {
function GraphQLClient(config) {
if (config === void 0) {
config = {};
}
// validate config
if (!config.url) {
throw new Error('GraphQLClient: config.url is required');
}
if (config.fetch && typeof config.fetch !== 'function') {
throw new Error('GraphQLClient: config.fetch must be a function');
}
if (!config.fetch && !fetch) {
throw new Error('GraphQLClient: fetch must be polyfilled or passed in new GraphQLClient({ fetch })');
}
if (config.ssrMode && !config.cache) {
throw new Error('GraphQLClient: config.cache is required when in ssrMode');
}
this.cache = config.cache;
this.headers = config.headers || {};
this.ssrMode = config.ssrMode;
this.ssrPromises = [];
this.url = config.url;
this.fetch = config.fetch || fetch.bind();
this.fetchOptions = config.fetchOptions || {};
this.logErrors = config.logErrors !== undefined ? config.logErrors : true;
this.onError = config.onError;
}
var _proto = GraphQLClient.prototype;
_proto.setHeader = function setHeader(key, value) {
this.headers[key] = value;
return this;
};
_proto.setHeaders = function setHeaders(headers) {
this.headers = headers;
return this;
};
_proto.removeHeader = function removeHeader(key) {
delete this.headers[key];
return this;
}
/* eslint-disable no-console */
;
_proto.logErrorResult = function logErrorResult(_ref) {
var result = _ref.result,
operation = _ref.operation;
if (this.onError) {
return this.onError({
result: result,
operation: operation
});
}
console.error('GraphQL Hooks Error');
console.groupCollapsed('---> Full Error Details');
console.groupCollapsed('Operation:');
console.log(operation);
console.groupEnd();
if (result.fetchError) {
console.groupCollapsed('FETCH ERROR:');
console.log(result.fetchError);
console.groupEnd();
}
if (result.httpError) {
console.groupCollapsed('HTTP ERROR:');
console.log(result.httpError);
console.groupEnd();
}
if (result.graphQLErrors && result.graphQLErrors.length > 0) {
console.groupCollapsed('GRAPHQL ERROR:');
result.graphQLErrors.forEach(function (err) {
return console.log(err);
});
console.groupEnd();
}
console.groupEnd();
}
/* eslint-enable no-console */
;
_proto.generateResult = function generateResult(_ref2) {
var fetchError = _ref2.fetchError,
httpError = _ref2.httpError,
graphQLErrors = _ref2.graphQLErrors,
data = _ref2.data;
var error = !!(graphQLErrors && graphQLErrors.length > 0 || fetchError || httpError);
return {
error: error,
fetchError: fetchError,
httpError: httpError,
graphQLErrors: graphQLErrors,
data: data
};
};
_proto.getCacheKey = function getCacheKey(operation, options) {
if (options === void 0) {
options = {};
}
var fetchOptions = _objectSpread2({}, this.fetchOptions, {}, options.fetchOptionsOverrides);
return {
operation: operation,
fetchOptions: fetchOptions
};
};
_proto.getCache = function getCache(cacheKey) {
var cacheHit = this.cache ? this.cache.get(cacheKey) : null;
if (cacheHit) {
return cacheHit;
}
};
_proto.saveCache = function saveCache(cacheKey, value) {
if (this.cache) {
this.cache.set(cacheKey, value);
}
} // Kudos to Jayden Seric (@jaydenseric) for this piece of code.
// See original source: https://github.com/jaydenseric/graphql-react/blob/82d576b5fe6664c4a01cd928d79f33ddc3f7bbfd/src/universal/graphqlFetchOptions.mjs.
;
_proto.getFetchOptions = function getFetchOptions(operation, fetchOptionsOverrides) {
if (fetchOptionsOverrides === void 0) {
fetchOptionsOverrides = {};
}
var fetchOptions = _objectSpread2({
method: 'POST',
headers: _objectSpread2({}, this.headers)
}, this.fetchOptions, {}, fetchOptionsOverrides);
var _extractFiles = extractFiles(operation),
clone = _extractFiles.clone,
files = _extractFiles.files;
var operationJSON = JSON.stringify(clone);
if (files.size) {
// See the GraphQL multipart request spec:
// https://github.com/jaydenseric/graphql-multipart-request-spec
var form = new FormData();
form.append('operations', operationJSON);
var map = {};
var i = 0;
files.forEach(function (paths) {
map[++i] = paths;
});
form.append('map', JSON.stringify(map));
i = 0;
files.forEach(function (paths, file) {
form.append("" + ++i, file, file.name);
});
fetchOptions.body = form;
} else {
fetchOptions.headers['Content-Type'] = 'application/json';
if (fetchOptions.method == 'POST') {
fetchOptions.body = operationJSON;
}
}
return fetchOptions;
};
_proto.request = function request(operation, options) {
var _this = this;
if (options === void 0) {
options = {};
}
var url = this.url;
if (typeof options.fetchOptionsOverrides !== 'undefined' && options.fetchOptionsOverrides.method == 'GET') {
var params = {
query: operation.query,
variables: JSON.stringify(operation.variables)
};
var paramsQueryString = Object.entries(params).map(function (_ref3) {
var k = _ref3[0],
v = _ref3[1];
return k + "=" + encodeURIComponent(v);
}).join('&');
url = url + '?' + paramsQueryString;
}
return this.fetch(url, this.getFetchOptions(operation, options.fetchOptionsOverrides)).then(function (response) {
if (!response.ok) {
return response.text().then(function (body) {
var status = response.status,
statusText = response.statusText;
return _this.generateResult({
httpError: {
status: status,
statusText: statusText,
body: body
}
});
});
} else {
return response.json().then(function (_ref4) {
var errors = _ref4.errors,
data = _ref4.data;
return _this.generateResult({
graphQLErrors: errors,
data: data
});
});
}
}).catch(function (error) {
return _this.generateResult({
fetchError: error
});
}).then(function (result) {
if (result.error && _this.logErrors) {
_this.logErrorResult({
result: result,
operation: operation
});
}
return result;
});
};
return GraphQLClient;
}();
var actionTypes = {
RESET_STATE: 'RESET_STATE',
LOADING: 'LOADING',
CACHE_HIT: 'CACHE_HIT',
REQUEST_RESULT: 'REQUEST_RESULT'
};
function reducer(state, action) {
switch (action.type) {
case actionTypes.RESET_STATE:
return action.initialState;
case actionTypes.LOADING:
if (state.loading) {
return state; // saves a render cycle as state is the same
}
return _objectSpread2({}, state, {
loading: true
});
case actionTypes.CACHE_HIT:
if (state.cacheHit) {
// we can be sure this is the same cacheKey hit
// because we dispatch RESET_STATE if it changes
return state;
}
return _objectSpread2({}, action.result, {
cacheHit: true,
loading: false
});
case actionTypes.REQUEST_RESULT:
return _objectSpread2({}, action.result, {
data: state.data && action.result.data && action.updateData ? action.updateData(state.data, action.result.data) : action.result.data,
cacheHit: false,
loading: false
});
default:
return state;
}
}
function useDeepCompareCallback(callback, deps) {
var ref = React.useRef();
if (!deepEqual(deps, ref.current)) {
ref.current = deps;
}
return React.useCallback(callback, ref.current);
}
/*
options include:
opts.variables: Object
opts.operationName: String
opts.fetchOptionsOverrides: Object
opts.skipCache: Boolean
*/
function useClientRequest(query, initialOpts) {
if (initialOpts === void 0) {
initialOpts = {};
}
if (typeof query !== 'string') {
throw new Error('Your query must be a string. If you are using the `gql` template literal from graphql-tag, remove it from your query.');
}
var client = React.useContext(ClientContext);
var isMounted = React.useRef(true);
var activeCacheKey = React.useRef(null);
var operation = {
query: query,
variables: initialOpts.variables,
operationName: initialOpts.operationName
};
var cacheKey = client.getCacheKey(operation, initialOpts);
var isDeferred = initialOpts.isMutation || initialOpts.isManual;
var initialCacheHit = initialOpts.skipCache || !client.cache ? null : client.cache.get(cacheKey);
var initialState = _objectSpread2({}, initialCacheHit, {
cacheHit: !!initialCacheHit,
loading: isDeferred ? false : !initialCacheHit
});
var _React$useReducer = React.useReducer(reducer, initialState),
state = _React$useReducer[0],
dispatch = _React$useReducer[1]; // NOTE: state from useReducer is only initialState on the first render
// in subsequent renders the operation could have changed
// if so the state would be invalid, this effect ensures we reset it back
var stringifiedCacheKey = JSON.stringify(cacheKey);
React.useEffect(function () {
if (!initialOpts.updateData) {
// if using updateData we can assume that the consumer cares about the previous data
dispatch({
type: actionTypes.RESET_STATE,
initialState: initialState
});
}
}, [stringifiedCacheKey]); // eslint-disable-line react-hooks/exhaustive-deps
React.useEffect(function () {
isMounted.current = true;
return function () {
isMounted.current = false;
};
}, []); // arguments to fetchData override the useClientRequest arguments
var fetchData = useDeepCompareCallback(function (newOpts) {
if (!isMounted.current) return Promise.resolve();
var revisedOpts = _objectSpread2({}, initialOpts, {}, newOpts);
var revisedOperation = _objectSpread2({}, operation, {
variables: revisedOpts.variables,
operationName: revisedOpts.operationName
});
var revisedCacheKey = client.getCacheKey(revisedOperation, revisedOpts); // NOTE: There is a possibility of a race condition whereby
// the second query could finish before the first one, dispatching an old result
// see https://github.com/nearform/graphql-hooks/issues/150
activeCacheKey.current = revisedCacheKey;
var cacheHit = revisedOpts.skipCache ? null : client.getCache(revisedCacheKey);
if (cacheHit) {
dispatch({
type: actionTypes.CACHE_HIT,
result: cacheHit
});
return Promise.resolve(cacheHit);
}
dispatch({
type: actionTypes.LOADING
});
return client.request(revisedOperation, revisedOpts).then(function (result) {
if (revisedOpts.updateData && typeof revisedOpts.updateData !== 'function') {
throw new Error('options.updateData must be a function');
}
var actionResult = _objectSpread2({}, result);
if (revisedOpts.useCache) {
actionResult.useCache = true;
actionResult.cacheKey = revisedCacheKey;
}
if (isMounted.current && revisedCacheKey === activeCacheKey.current) {
dispatch({
type: actionTypes.REQUEST_RESULT,
updateData: revisedOpts.updateData,
result: actionResult
});
}
return result;
});
}, [client, initialOpts, operation]); // We perform caching after reducer update
// To include the outcome of updateData
React.useEffect(function () {
if (state.useCache) {
client.saveCache(state.cacheKey, state);
}
}, [client, state]);
return [fetchData, state];
}
var defaultOpts = {
useCache: true
};
function useQuery(query, opts) {
if (opts === void 0) {
opts = {};
}
var allOpts = _objectSpread2({}, defaultOpts, {}, opts);
var client = React.useContext(ClientContext);
var _React$useState = React.useState(false),
calledDuringSSR = _React$useState[0],
setCalledDuringSSR = _React$useState[1];
var _useClientRequest = useClientRequest(query, allOpts),
queryReq = _useClientRequest[0],
state = _useClientRequest[1];
if (client.ssrMode && opts.ssr !== false && !calledDuringSSR) {
// result may already be in the cache from previous SSR iterations
if (!state.loading && !state.data && !state.error) {
var p = queryReq();
client.ssrPromises.push(p);
}
setCalledDuringSSR(true);
}
var stringifiedAllOpts = JSON.stringify(allOpts);
React.useEffect(function () {
queryReq();
}, [query, stringifiedAllOpts]); // eslint-disable-line react-hooks/exhaustive-deps
return _objectSpread2({}, state, {
refetch: React.useCallback(function (options) {
if (options === void 0) {
options = {};
}
return queryReq(_objectSpread2({
skipCache: true,
// don't call the updateData that has been passed into useQuery here
// reset to the default behaviour of returning the raw query result
// this can be overridden in refetch options
updateData: function updateData(_, data) {
return data;
}
}, options));
}, [queryReq])
});
}
var useManualQuery = function useManualQuery(query, options) {
return useClientRequest(query, _objectSpread2({
useCache: true,
isManual: true
}, options));
};
var useMutation = function useMutation(query, options) {
return useClientRequest(query, _objectSpread2({
isMutation: true
}, options));
};
export { ClientContext, GraphQLClient, useClientRequest, useManualQuery, useMutation, useQuery };