diff --git a/.gitignore b/.gitignore index 088fa47..c6d4cb5 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,5 @@ __diff_output__ *.bak *.cya .vscode +.idea *.tgz diff --git a/README.md b/README.md index 3d94067..77cc41d 100644 --- a/README.md +++ b/README.md @@ -359,7 +359,7 @@ const AbortComponent = () => { ); }; ``` -Instead of setting up a `useEffect` within the component it's possible to pass a hook to signal using packages such as +Instead of setting up a `useEffect` within the component it's possible to pass a hook to signal using packages such as [use-unmount-signal](https://www.npmjs.com/package/use-unmount-signal/v/1.0.0). ### Sequential API Execution @@ -587,6 +587,51 @@ const BookList = ({ ssl }) => { }; ``` +### Passing dynamic headers + +When you call the `run` function returned from useFetchye, it will use the values last rendered into the hook. + +This means any correlationId, timestamp, or any other unique dynamic header you might want sent to the server will use its previous value. + +To overcome this, you can specify a function instead of a `headers` object in the options. + +This function will be called, to re-make the headers just before an API call is made, even when you call `run`. + +Note: If you don't want the dynamic headers to result in a cache miss, you must remove the keys of the dynamic headers from the options using `mapOptionsToKey` (see example below). + +```jsx +import React from 'react'; +import { useFetchye } from 'fetchye'; +import uuid from 'uuid'; + +const BookList = () => { + const { isLoading, data } = useFetchye('http://example.com/api/books/', { + // remove the 'correlationId' header from the headers, as its the only dynamic header + mapOptionsToKey: ({ headers: { correlationId, ...headers }, ...options }) => ({ + ...options, + headers, + }), + headers: () => ({ + // static headers are still fine, and can be specified here like normal + staticHeader: 'staticValue', + // This header will be generated fresh for every call out of the system + correlationId: uuid(), + }), + }); + + if (isLoading) { + return (

Loading...

); + } + + return ( + {/* Render data */} + ); +}; + +export default BookList; +``` + + ### SSR #### One App SSR @@ -792,6 +837,7 @@ const { isLoading, data, error, run } = useFetchye(key, { defer: Boolean, mapOpt | `mapKeyToCacheKey` | `(key: String, options: Options) => cacheKey: String` | `false` | A function that maps the key for use as the cacheKey allowing direct control of the cacheKey | | `defer` | `Boolean` | `false` | Prevents execution of `useFetchye` on each render in favor of using the returned `run` function. *Defaults to `false`* | | `initialData` | `Object` | `false` | Seeds the initial data on first render of `useFetchye` to accomodate server side rendering *Defaults to `undefined`* | +| `headers` | `Object` or `() => Object` | `false` | `Object`: as per the ES6 Compatible `fetch` option. `() => Object`: A function to construct a ES6 Compatible `headers` object prior to any api call | | `...restOptions` | `ES6FetchOptions` | `true` | Contains any ES6 Compatible `fetch` option. (See [Fetch Options](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Supplying_request_options)) | **Returns** diff --git a/packages/fetchye/__tests__/handleDynamicHeaders.spec.js b/packages/fetchye/__tests__/handleDynamicHeaders.spec.js new file mode 100644 index 0000000..c34c0f4 --- /dev/null +++ b/packages/fetchye/__tests__/handleDynamicHeaders.spec.js @@ -0,0 +1,70 @@ +/* + * Copyright 2023 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { handleDynamicHeaders } from '../src/handleDynamicHeaders'; + +describe('handleDynamicHeaders', () => { + it('should pass back the exact object passed if the headers field is not a function', () => { + const testValues = [ + {}, + { body: 'mockBody' }, + { headers: {} }, + { headers: 1 }, + { headers: { staticHeader: 'staticHeaderValue' } }, + // The function should be resistant to being passed non objects too + Symbol('testSymbol'), + 'value', + 1234, + ]; + + testValues.forEach((testVal) => { + expect(handleDynamicHeaders(testVal)).toBe(testVal); + }); + }); + + it('should pass back a new object with the headers handled, and other values preserved, when the headers field is a function', () => { + const testValues = [ + {}, + { body: 'mockBody' }, + { options: { staticOption: 'staticOptionValue' } }, + { symbol: Symbol('testSymbol') }, + ]; + + testValues.forEach((testVal) => { + const valWithDynamicHeader = { + ...testVal, + headers: jest.fn(() => ({ + dynamicHeader: 'dynamicHeaderValue', + })), + }; + + const result = handleDynamicHeaders(valWithDynamicHeader); + + // a new object has been created + expect(result).not.toBe(valWithDynamicHeader); + + // the headers are no-loger a function + expect(result.headers).toEqual({ + dynamicHeader: 'dynamicHeaderValue', + }); + + // all other keys are preserved + Object.keys(testVal).forEach((testValKey) => { + expect(result[testValKey]).toBe(testVal[testValKey]); + }); + }); + }); +}); diff --git a/packages/fetchye/__tests__/useFetchye.spec.jsx b/packages/fetchye/__tests__/useFetchye.spec.jsx index e1d84ac..b60893a 100644 --- a/packages/fetchye/__tests__/useFetchye.spec.jsx +++ b/packages/fetchye/__tests__/useFetchye.spec.jsx @@ -113,6 +113,37 @@ describe('useFetchye', () => { } `); }); + it('should call fetch with the right headers when passed dynamic headers', async () => { + let fetchyeRes; + global.fetch = jest.fn(async () => ({ + ...defaultPayload, + })); + render( + + {React.createElement(() => { + fetchyeRes = useFetchye('http://example.com', { + headers: () => ({ + dynamicHeader: 'dynamic value', + }), + }); + return null; + })} + + ); + await waitFor(() => fetchyeRes.isLoading === false); + expect(global.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "http://example.com", + Object { + "headers": Object { + "dynamicHeader": "dynamic value", + }, + }, + ], + ] + `); + }); it('should return data success state when response is empty (204 no content)', async () => { let fetchyeRes; global.fetch = jest.fn(async () => ({ @@ -247,6 +278,34 @@ describe('useFetchye', () => { ] `); }); + it('should return data when run method is called with dynamic headers', async () => { + let fetchyeRes; + global.fetch = jest.fn(async () => ({ + ...defaultPayload, + })); + render( + + {React.createElement(() => { + fetchyeRes = useFetchye('http://example.com/one', { defer: true, headers: () => ({ dynamicHeader: 'dynamic value' }) }); + return null; + })} + + ); + await fetchyeRes.run(); + expect(global.fetch.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "http://example.com/one", + Object { + "defer": true, + "headers": Object { + "dynamicHeader": "dynamic value", + }, + }, + ], + ] + `); + }); it('should use fetcher in hook over provider fetcher', async () => { const customFetchClient = jest.fn(async () => ({ ...defaultPayload, diff --git a/packages/fetchye/src/handleDynamicHeaders.js b/packages/fetchye/src/handleDynamicHeaders.js new file mode 100644 index 0000000..88bd4cf --- /dev/null +++ b/packages/fetchye/src/handleDynamicHeaders.js @@ -0,0 +1,25 @@ +/* + * Copyright 2023 American Express Travel Related Services Company, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +export const handleDynamicHeaders = (options) => { + if (typeof options.headers === 'function') { + return { + ...options, + headers: options.headers(), + }; + } + return options; +}; diff --git a/packages/fetchye/src/useFetchye.js b/packages/fetchye/src/useFetchye.js index e17dfcc..5b79c76 100644 --- a/packages/fetchye/src/useFetchye.js +++ b/packages/fetchye/src/useFetchye.js @@ -22,6 +22,7 @@ import { } from './queryHelpers'; import { useFetchyeContext } from './useFetchyeContext'; import { defaultMapOptionsToKey } from './defaultMapOptionsToKey'; +import { handleDynamicHeaders } from './handleDynamicHeaders'; const passInitialData = (value, initialValue, numOfRenders) => (numOfRenders === 1 ? value || initialValue @@ -35,8 +36,9 @@ const useFetchye = ( const { defaultFetcher, useFetchyeSelector, dispatch, fetchClient, } = useFetchyeContext(); + const dynamicOptions = handleDynamicHeaders(options); const selectedFetcher = typeof fetcher === 'function' ? fetcher : defaultFetcher; - const computedKey = computeKey(key, defaultMapOptionsToKey(mapOptionsToKey(options))); + const computedKey = computeKey(key, defaultMapOptionsToKey(mapOptionsToKey(dynamicOptions))); const selectorState = useFetchyeSelector(computedKey.hash); // create a render version manager using refs const numOfRenders = useRef(0); @@ -53,7 +55,7 @@ const useFetchye = ( const { loading, data, error } = selectorState.current; if (!loading && !data && !error) { runAsync({ - dispatch, computedKey, fetcher: selectedFetcher, fetchClient, options, + dispatch, computedKey, fetcher: selectedFetcher, fetchClient, options: dynamicOptions, }); } }); @@ -76,8 +78,16 @@ const useFetchye = ( numOfRenders.current ), run() { + const runOptions = handleDynamicHeaders(options); + const runComputedKey = typeof options.headers === 'function' + ? computeKey(key, defaultMapOptionsToKey(mapOptionsToKey(runOptions))) + : computedKey; return runAsync({ - dispatch, computedKey, fetcher: selectedFetcher, fetchClient, options, + dispatch, + computedKey: runComputedKey, + fetcher: selectedFetcher, + fetchClient, + options: runOptions, }); }, };