Skip to content

Commit

Permalink
feat(core): default global fetchPolicy and ssrForceFetchDelay (#60)
Browse files Browse the repository at this point in the history
  • Loading branch information
jackyef committed May 24, 2020
1 parent 259fc39 commit 8fc342a
Show file tree
Hide file tree
Showing 13 changed files with 451 additions and 57 deletions.
16 changes: 13 additions & 3 deletions docusaurus/docs/others/caching.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,21 @@ description: 'Explanation about different caching strategy in react-isomorphic-d
All `GET` requests, either lazy or non-lazy ones, can be cached. By default, `react-isomorphic-data` will use `cache-first` strategy for all the data. This means that if a response for a particular URL + query params already exist in the cache, all subsequent requests will always receive a response from the cache directly, without going to the network.

## Caching Strategies
`react-isomorphic-data` comes with 3 different caching strategies, which you can choose by passing a `dataOptions.fetchPolicy` to your hook/HOC. [(More about dataOptions)](./data-options.md)
`react-isomorphic-data` comes with 3 different caching strategies, which you can choose by passing a `dataOptions.fetchPolicy` to your hook/HOC. [(More about dataOptions)](./data-options.md). If you do not pass a value to `dataOptions.fetchPolicy`, `client.fetchPolicy` will be used instead. You can set `client.fetchPolicy` when creating a `dataClient` instance.

```js
const client = createDataClient({
fetchPolicy: 'network-only', // set the default fetchPolicy to 'network-only'
});
```

> To learn more about what options can be passed to `createDataClient`, go [here](./create-data-client.md).
If you do not set `client.fetchPolicy`, it will default to `cache-first`.

1. `cache-first`

This is the default strategy used if you do not pass an explicit `fetchPolicy`. A network request will only be made if the particular URL + query params' response does not exist in the cache yet.
This is the default strategy used if you do not pass an explicit `fetchPolicy` and `createDataClient`. A network request will only be made if the particular URL + query params' response does not exist in the cache yet.

If it exists in the cache, the data from cache will be passed to your component and a network request will never be made.

Expand All @@ -25,6 +35,6 @@ All `GET` requests, either lazy or non-lazy ones, can be cached. By default, `re
The data will never be cached. Each data will only be kept until the component unmount, or until another network request is made explicitly. All non `GET` requests (`POST`, `PUT`, `DELETE`, etc.) are locked in to this strategy.

## Things to note
1. `network-only` strategy should not be used for data that are going to be fetched during server side rendering. The reason for this is because it's going to be invalidated (become `null`) the moment your React app hydrates on the client side. Doing so in development mode will show a warning message in the console. [An issue](https://github.com/jackyef/react-isomorphic-data/issues/14) for this has been opened in the repo, and will be looked at in the near future.
1. `network-only` strategy should not be used for data that are going to be fetched during server side rendering. The reason for this is because it's going to be invalidated (become `null`) the moment your React app hydrates on the client side. Doing so in development mode will show a warning message in the console. You can provide `{ ssrForceFetchDelay: 500 }` when doing `createDataClient()` to work around this.

2. All non `GET` requests are locked in to `network-only` strategy. The reason for this is because non `GET` requests are not expected to be idempotent (calling them will introduces side-effects on the server side), and therefore should not be cached.
69 changes: 69 additions & 0 deletions docusaurus/docs/others/create-data-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
id: create-data-client
title: createDataClient()
sidebar_label: createDataClient()
description: 'Various configurations you can pass to createDataClient()'
---

## `createDataClient`
Params:
* `options: createDataClientOptions`

```ts
interface createDataClientOptions {
ssr?: boolean;
ssrForceFetchDelay?: number;
fetchPolicy?: 'cache-first' | 'cache-and-network' | 'network-only';
test?: boolean;
initialCache?: Record<string, any>;
headers?: Record<string, string>;
}
```

Example usage:
```javascript
import { createDataClient } from 'react-isomorphic-data';

const client = createDataClient({
ssr: false,
});
```

### Configurations
Following is a list of all the fields possible to be configured in the `createDataClientOptions` object

#### `ssr`
* Type: `boolean`
* Default: `false`

Determines whether the `dataClient` instance will run on server-side or not.

#### `ssrForceFetchDelay`
* Type: `number`
* Default: `0`

Determines how many milliseconds the `dataClient` should prevent client-side refetching on hook/HOC using `network-only` fetchPolicy.

#### `fetchPolicy`
* Type: `'cache-first' | 'cache-and-network' | 'network-only'`
* Default: `'cache-first'`

Determines the how the default fetchPolicy to be used. A more detailed explanation can be found in [Caching](./caching.md).

#### `test`
* Type: `boolean`
* Default: `false`

Determines whether the `dataClient` instance will run in test environment.

#### `initialCache`
* Type: `object`
* Default: `{}`

An object to be used as the client's initial cache. Example usage can be seen in [Client-side hydration](../ssr/client-side-hydration.md).

#### `headers`
* Type: `Record<string, string>`
* Default: `false`

Set the default headers that will be included in all other fetch requests. Can be used on server side to forward cookies. Does not work on [prefetching](../ssr/prefetching.md) due to how `<link rel="prefetch">` works.
82 changes: 45 additions & 37 deletions docusaurus/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,44 +12,52 @@ module.exports = {
'Higher Order Components': ['hocs/withdata', 'hocs/withlazydata', 'hocs/withdataclient'],
'Server-side Rendering': ['ssr/intro', 'ssr/rendertostringwithdata', 'ssr/getdatafromtree', 'ssr/client-side-hydration', 'ssr/prefetching'],
'Testing': ['testing/writing-tests'],
'Others': ['others/caching', 'others/data-options', 'others/preloaddata', 'others/typescript', 'others/suspense', 'others/cant-find-answer'],
'Others': [
'others/caching',
'others/data-options',
'others/create-data-client',
'others/preloaddata',
'others/typescript',
'others/suspense',
'others/cant-find-answer'
],
},
// 'react-isomorphic-data': {
// Introduction: ['api/index', 'api/globals'],
// 'External Modules': [
// 'api/modules/_common_client_',
// 'api/modules/_common_context_',
// 'api/modules/_common_provider_',
// 'api/interfaces/_common_provider_.dataproviderprops',
// 'api/modules/_common_index_',
// 'api/modules/_common_types_',
// 'api/interfaces/_common_types_.dataclient',
// 'api/interfaces/_common_types_.dataclientoptions',
// 'api/interfaces/_common_types_.datacontextapi',
// 'api/modules/_hoc_index_',
// 'api/modules/_hoc_types_',
// 'api/interfaces/_hoc_types_.hocoptions',
// 'api/modules/_hoc_withdata_',
// 'api/modules/_hoc_withlazydata_',
// 'api/modules/_hooks_index_',
// 'api/modules/_hooks_types_',
// 'api/interfaces/_hooks_types_.asyncdatahookstate',
// 'api/interfaces/_hooks_types_.asyncdatastate',
// 'api/modules/_hooks_usedata_',
// 'api/modules/_hooks_uselazydata_',
// 'api/modules/_hooks_utils_usebasedata_',
// 'api/modules/_index_',
// 'api/modules/_ssr_getdatafromtree_',
// 'api/modules/_ssr_index_',
// ],
// Interfaces: [
// 'api/interfaces/_common_provider_.dataproviderprops',
// 'api/interfaces/_common_types_.dataclient',
// 'api/interfaces/_common_types_.dataclientoptions',
// 'api/interfaces/_common_types_.datacontextapi',
// 'api/interfaces/_hoc_types_.hocoptions',
// 'api/interfaces/_hooks_types_.asyncdatahookstate',
// 'api/interfaces/_hooks_types_.asyncdatastate',
// ],
// Introduction: ['api/index', 'api/globals'],
// 'External Modules': [
// 'api/modules/_common_client_',
// 'api/modules/_common_context_',
// 'api/modules/_common_provider_',
// 'api/interfaces/_common_provider_.dataproviderprops',
// 'api/modules/_common_index_',
// 'api/modules/_common_types_',
// 'api/interfaces/_common_types_.dataclient',
// 'api/interfaces/_common_types_.dataclientoptions',
// 'api/interfaces/_common_types_.datacontextapi',
// 'api/modules/_hoc_index_',
// 'api/modules/_hoc_types_',
// 'api/interfaces/_hoc_types_.hocoptions',
// 'api/modules/_hoc_withdata_',
// 'api/modules/_hoc_withlazydata_',
// 'api/modules/_hooks_index_',
// 'api/modules/_hooks_types_',
// 'api/interfaces/_hooks_types_.asyncdatahookstate',
// 'api/interfaces/_hooks_types_.asyncdatastate',
// 'api/modules/_hooks_usedata_',
// 'api/modules/_hooks_uselazydata_',
// 'api/modules/_hooks_utils_usebasedata_',
// 'api/modules/_index_',
// 'api/modules/_ssr_getdatafromtree_',
// 'api/modules/_ssr_index_',
// ],
// Interfaces: [
// 'api/interfaces/_common_provider_.dataproviderprops',
// 'api/interfaces/_common_types_.dataclient',
// 'api/interfaces/_common_types_.dataclientoptions',
// 'api/interfaces/_common_types_.datacontextapi',
// 'api/interfaces/_hoc_types_.hocoptions',
// 'api/interfaces/_hooks_types_.asyncdatahookstate',
// 'api/interfaces/_hooks_types_.asyncdatastate',
// ],
// },
};
4 changes: 4 additions & 0 deletions examples/ssr/src/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ declare global {
const dataClient = createDataClient({
initialCache: window.__cache,
ssr: false,
ssrForceFetchDelay: 1000,
headers: {
'x-foo': 'bar',
}
});

hydrate(
Expand Down
5 changes: 4 additions & 1 deletion packages/react-isomorphic-data/src/common/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import { DataClient, DataClientOptions } from './types';
export const createDataClient = (
options: DataClientOptions = {},
): DataClient => {
const { ssr, initialCache, headers, test } = options;
const { ssr, initialCache, headers, test, ssrForceFetchDelay, fetchPolicy } = options;
const subscribers: Record<string, Map<Function, Function>> = {};

return {
cache: initialCache ? { ...initialCache } : {},
ssr: ssr || false,
ssrForceFetchDelay: ssrForceFetchDelay || 0,
initTime: Date.now(),
test: test || false,
headers: headers || {},
fetchPolicy: fetchPolicy || 'cache-first',
toBePrefetched: {},
addSubscriber: (key: string, callback: Function) => {
if (!(subscribers[key] instanceof Map)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe('DataClient', () => {

expect(client.ssr).toBe(true);
expect(client.headers['x-header']).toBe('custom header');
expect(client.ssrForceFetchDelay).toBe(0);
})
test('DataClient can be created on non-SSR mode without initialCache', async () => {
const client = createDataClient({ ssr: false });
Expand All @@ -26,6 +27,12 @@ describe('DataClient', () => {
expect(client.cache.foo).toBe('bar');
})

test('DataClient can be allow to set force fetch delay', async () => {
const client = createDataClient({ ssr: false, ssrForceFetchDelay: 200 });

expect(client.ssrForceFetchDelay).toBe(200);
})

test('DataClient subscription model works properly', async () => {
const client = createDataClient({ ssr: false });

Expand Down
9 changes: 7 additions & 2 deletions packages/react-isomorphic-data/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export interface DataClient {
cache: Record<string, any>;
toBePrefetched: Record<string, boolean>;
ssr: boolean;
ssrForceFetchDelay: number;
initTime: number;
fetchPolicy: 'cache-first' | 'cache-and-network' | 'network-only';
test: boolean;
headers: Record<string, any>;
addSubscriber: (key: string, callback: Function) => void;
Expand All @@ -14,9 +17,11 @@ export interface DataClient {

export interface DataClientOptions {
ssr?: boolean;
ssrForceFetchDelay?: number;
fetchPolicy?: 'cache-first' | 'cache-and-network' | 'network-only';
test?: boolean;
initialCache?: Record<string, any>;
headers?: Record<string, any>;
headers?: Record<string, string>;
}

export interface DataContextAPI {
Expand All @@ -25,4 +30,4 @@ export interface DataContextAPI {
addToBePrefetched: (url: string) => void;
retrieveFromCache: (url: string) => any;
fetcher: (input: RequestInfo, init?: RequestInit) => Promise<Response | any>;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ const createFetchOptions = (
lazy = false,
): [RequestInit, string] => {
const finalMethod = fetchOptions.method && lazy ? fetchOptions.method : 'GET';
let fetchPolicy = dataOpts.fetchPolicy !== undefined ? dataOpts.fetchPolicy : 'cache-first';

let fetchPolicy = dataOpts.fetchPolicy !== undefined ? dataOpts.fetchPolicy : client.fetchPolicy;

const ssrOpt = dataOpts.ssr !== undefined ? dataOpts.ssr : true;
const isSSR = client.ssr && ssrOpt && typeof window === 'undefined';

Expand Down
66 changes: 66 additions & 0 deletions packages/react-isomorphic-data/src/hoc/__tests__/withData.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as React from 'react';
import { FetchMock } from 'jest-fetch-mock';
import { render } from '@testing-library/react';
import { createDataClient, DataProvider } from '../../common';
import withData from '../withData';

const fetchMock = fetch as FetchMock;

describe('withData HOC with ssrForceFetchDelay', () => {
const onError = (e: Event) => {
e.preventDefault();
};

beforeEach(() => {
// to suppress error logs from error boundaries
// https://github.com/facebook/react/issues/11098#issuecomment-412682721
window.addEventListener('error', onError);
fetchMock.resetMocks();
jest.useFakeTimers();
});
afterEach(() => {
window.removeEventListener('error', onError);
jest.useRealTimers();
});

it('should not fetch again; fetchPolicy: "network-only"', async () => {
const client = createDataClient({
initialCache: {
'http:': {
localhost: {
somewhere: {
__raw: '{"message":"initial message","randomNumber":55}',
}
}
}
},
ssrForceFetchDelay: 1000,
});

const _Comp: React.FC<{ isoData: any }> = ({ isoData }) => {
const { data, loading } = isoData;

return loading || !data ? (
<div>loading...</div>
) : (
<div>{data.message}</div>
);
};

const Comp = withData({
url: 'http://localhost/somewhere',
name: 'isoData',
dataOptions: { skip: false, ssr: true, fetchPolicy: 'network-only' }
})(_Comp);
const App = (
<DataProvider client={client}>
<Comp />
</DataProvider>
);

const { findByText } = render(App);

expect(await findByText('initial message')).toBeDefined();
});

})
Loading

0 comments on commit 8fc342a

Please sign in to comment.