Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fetchye): support headers as a function #86

Merged
merged 3 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ __diff_output__
*.bak
*.cya
.vscode
.idea
*.tgz
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@
const controller = new AbortController();
useFetchye('http://example.com/api/books', { signal: controller.signal });

useEffect(() => () => controller.abort(), []);

Check warning on line 353 in README.md

View workflow job for this annotation

GitHub Actions / Node 12.x

React Hook useEffect has a missing dependency: 'controller'. Either include it or remove the dependency array

Check warning on line 353 in README.md

View workflow job for this annotation

GitHub Actions / Node 14.x

React Hook useEffect has a missing dependency: 'controller'. Either include it or remove the dependency array

Check warning on line 353 in README.md

View workflow job for this annotation

GitHub Actions / Node 16.x

React Hook useEffect has a missing dependency: 'controller'. Either include it or remove the dependency array

Check warning on line 353 in README.md

View workflow job for this annotation

GitHub Actions / Node 18.x

React Hook useEffect has a missing dependency: 'controller'. Either include it or remove the dependency array

return (
<div>
Expand All @@ -359,7 +359,7 @@
);
};
```
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
Expand Down Expand Up @@ -587,6 +587,51 @@
};
```

### 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 (<p>Loading...</p>);
}

return (
{/* Render data */}
);
};

export default BookList;
```


### SSR

#### One App SSR
Expand Down Expand Up @@ -792,6 +837,7 @@
| `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**
Expand Down
70 changes: 70 additions & 0 deletions packages/fetchye/__tests__/handleDynamicHeaders.spec.js
Original file line number Diff line number Diff line change
@@ -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]);
});
});
});
});
59 changes: 59 additions & 0 deletions packages/fetchye/__tests__/useFetchye.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<AFetchyeProvider cache={cache}>
{React.createElement(() => {
fetchyeRes = useFetchye('http://example.com', {
headers: () => ({
dynamicHeader: 'dynamic value',
}),
});
return null;
})}
</AFetchyeProvider>
);
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 () => ({
Expand Down Expand Up @@ -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(
<AFetchyeProvider cache={cache}>
{React.createElement(() => {
fetchyeRes = useFetchye('http://example.com/one', { defer: true, headers: () => ({ dynamicHeader: 'dynamic value' }) });
return null;
})}
</AFetchyeProvider>
);
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,
Expand Down
25 changes: 25 additions & 0 deletions packages/fetchye/src/handleDynamicHeaders.js
Original file line number Diff line number Diff line change
@@ -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;
};
16 changes: 13 additions & 3 deletions packages/fetchye/src/useFetchye.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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,
});
}
});
Expand All @@ -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,
});
},
};
Expand Down
Loading