Skip to content

Commit b8ad902

Browse files
committed
Reorganize the file structure and replace the entire API.
Also removes Babel and related dependencies and config. Fixes #24 , fixes #53 , fixes #55 , and fixes #56 .
1 parent f009519 commit b8ad902

95 files changed

Lines changed: 9366 additions & 5373 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.eslintignore

Lines changed: 0 additions & 3 deletions
This file was deleted.

.gitignore

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,2 @@
11
node_modules
22
.DS_Store
3-
/private
4-
/public
5-
/test

changelog.md

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,89 @@
66

77
- Stopped supporting Internet Explorer.
88
- Updated the [`react`](https://npm.im/react) and [`react-dom`](https://npm.im/react-dom) peer dependencies to `16.14 - 17`.
9-
- Updated the Babel config to use [the new JSX transform](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html).
10-
- Removed the function `ssr` in favor of the function [`waterfallRender`](https://github.com/jaydenseric/react-waterfall-render#function-waterfallrender) from [`react-waterfall-render`](https://npm.im/react-waterfall-render), fixing [#57](https://github.com/jaydenseric/graphql-react/issues/57).
11-
- Reorganized file structure. Deep import paths beginning with `graphql-react/universal` must be updated to `graphql-react/public`.
9+
- Use [the new JSX runtime](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html).
10+
- Reorganized the file structure and replaced the entire API:
11+
12+
- Removed all of the previous public exports for the old API:
13+
- `GraphQL`
14+
- `GraphQLContext`
15+
- `GraphQLProvider`
16+
- `hashObject`
17+
- `reportCacheErrors`
18+
- `useGraphQL`
19+
- `ssr`
20+
- Added public exports for the new API, available as named imports from the index and as deep imports from `graphql-react/public/` `.js` CJS modules:
21+
- `Cache`
22+
- `CacheContext`
23+
- `HYDRATION_TIME_MS`
24+
- `HydrationTimeStampContext`
25+
- `Loading`
26+
- `LoadingCacheValue`
27+
- `LoadingContext`
28+
- `Provider`
29+
- `cacheDelete`
30+
- `cacheEntryDelete`
31+
- `cacheEntryPrune`
32+
- `cacheEntrySet`
33+
- `cacheEntryStale`
34+
- `cachePrune`
35+
- `cacheStale`
36+
- `fetchGraphQL`
37+
- `fetchOptionsGraphQL`
38+
- `useAutoAbortLoad`
39+
- `useAutoLoad`
40+
- `useCache`
41+
- `useCacheEntry`
42+
- `useCacheEntryPrunePrevention`
43+
- `useLoadGraphQL`
44+
- `useLoadOnDelete`
45+
- `useLoadOnMount`
46+
- `useLoadOnStale`
47+
- `useLoading`
48+
- `useLoadingEntry`
49+
- `useWaterfallLoad`
50+
- The [`waterfallRender`](https://github.com/jaydenseric/react-waterfall-render#function-waterfallrender) function from [`react-waterfall-render`](https://npm.im/react-waterfall-render) should now be used for server side rendering, fixing [#57](https://github.com/jaydenseric/graphql-react/issues/57).
51+
- In addition to the previously required globals, consider polyfilling:
52+
- [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
53+
- [`CustomEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent)
54+
- [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event)
55+
- [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget)
56+
- [`performance`](https://developer.mozilla.org/en-US/docs/Web/API/Window/performance)
57+
58+
The API for the cache (centered around a `Cache` instance provided in the `CacheContext` React context) is separated from the API for loading (centered around a `Loading` instance provided in the `LoadingContext` React context). Although the new loading system should work well for everyone, it could be totally avoided in an app that implements a custom alternative.
59+
60+
Instead of using the old [`mitt`](https://npm.im/mitt) dependency for events, the `Cache` and `Loading` classes extend the native [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) global available in modern browsers and Node.js; a powerful and familiar event system with zero bundle size cost.
61+
62+
The new API avoids class methods that add to bundle size regardless if they are used, in favor of focused functions that can be imported to process instances as arguments. For example, one route in your app may only render a cache entry, while another may have a form that makes the global cache stale. If the functionality to make the cache stale was a `Cache` instance method, it would increase the bundle size for the entire app, whereas a function imported in the second route will only grow the bundle size for that route. Features can be added to the API over time without growing everyone’s bundles.
63+
64+
There are now functions that can be imported to directly manipulate the cache. The functions `cacheEntrySet` and `cacheEntryDelete` update a particular entry, and `cacheDelete` deletes all cache.
65+
66+
There is a new approach for dealing with stale cache. The function `cacheEntryStale` signals a single entry is stale, and `cacheStale` does the same for all entries (useful after a mutation). These functions don’t actually update cache entries; they simply dispatch cache entry stale events and it’s up to components to listen for this event and reload the cache entry in response, typically via the `useLoadOnStale` React hook.
67+
68+
Cache entries that are not relevant to the current view can now be pruned on demand using the functions `cacheEntryPrune` for a single entry, or `cachePrune` for all entries, fixing [#55](https://github.com/jaydenseric/graphql-react/issues/55). These functions work by dispatching cache entry prune events on the `Cache` instance, and for each event not cancelled by a listener with `event.preventDefault()`, the cache entry is deleted. The `useCacheEntryPrunePrevention` React hook can be used to automatically cancel pruning of a cache entry used in a component.
69+
70+
Cache keys are now manually defined instead of automatically derived from `fetch` options hashes, fixing [#56](https://github.com/jaydenseric/graphql-react/issues/56). This is easier to understand, is faster to render, and results in a smaller bundle size without the old [`fnv1a`](https://npm.im/fnv1a) dependency for hashing.
71+
72+
Instead of one `useGraphQL` React hook with complex options that all add to a component’s bundle size regardless if they are used, there are now several more focused React hooks that can be composed to do exactly the work required, fixing [#53](https://github.com/jaydenseric/graphql-react/issues/53).
73+
74+
The React hooks can be composed with custom ones to load and cache any type of data, not just GraphQL, using any method, not just `fetch`.
75+
76+
The new loading system provides the ability to abort loading at any time, implemented using the native [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) global available in modern browsers and Node.js, fixing [#24](https://github.com/jaydenseric/graphql-react/issues/24). Many of the new React hooks leverage this for features such as automatically aborting loading a cache entry when the component loading it unmounts. The new API makes it trivially easy to build features as auto-suggest search inputs that abort the last loading on new input, or page queries that abort loading if the user abandons the route.
77+
78+
While the new API may seem to have an intimidating number of public exports, the average Next.js app that queries and renders data from a GraphQL API will only use a few. For inspiration, see the readme “Examples” section.
79+
80+
- Published modules now contain JSDoc comments, which might affect TypeScript projects.
1281

1382
### Patch
1483

1584
- Updated dependencies.
16-
- Replaced [`babel-eslint`](https://npm.im/babel-eslint) dev dependency with [`@babel/eslint-parser`](https://npm.im/@babel/eslint-parser).
85+
- Removed Babel and related dependencies and config.
1786
- Updated GitHub Actions CI config:
1887
- Updated `actions/checkout` to v2.
1988
- Updated `actions/setup-node` to v2.
2089
- Don’t specify the `CI` environment variable as it’s set by default.
21-
- Use a new [`@arr/flatten`](https://npm.im/@arr/flatten) dev dependency to flatten arrays in tests.
2290
- Stop using [`hard-rejection`](https://npm.im/hard-rejection) to detect unhandled `Promise` rejections in tests, as Node.js v15+ does this natively.
2391
- Test the bundle size manually using [`webpack`](https://npm.im/webpack) v5, and remove [`size-limit`](https://npm.im/size-limit) related dev dependencies, config, and scripts.
24-
- Added a test for `FirstRenderDateContext` used as a React context.
2592
- Tweaked the package description.
2693
- Readme edits, including:
2794
- Updated the Relay and Apollo URLs.

package.json

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,40 +50,30 @@
5050
"react-dom": "16.14 - 17"
5151
},
5252
"dependencies": {
53-
"@babel/runtime": "^7.13.9",
5453
"extract-files": "^9.0.0",
55-
"fnv1a": "^1.0.1",
56-
"mitt": "^2.1.0",
57-
"prop-types": "^15.7.2",
54+
"isobject": "^4.0.0",
5855
"react-waterfall-render": "^1.0.0"
5956
},
6057
"devDependencies": {
61-
"@arr/flatten": "^1.0.1",
62-
"@babel/cli": "^7.13.0",
63-
"@babel/core": "^7.13.8",
64-
"@babel/eslint-parser": "^7.13.8",
65-
"@babel/plugin-transform-runtime": "^7.13.9",
66-
"@babel/preset-env": "^7.13.9",
67-
"@babel/preset-react": "^7.12.13",
58+
"@testing-library/react-hooks": "^5.1.1",
59+
"abort-controller": "^3.0.0",
6860
"coverage-node": "^4.0.0",
6961
"disposable-directory": "^3.0.0",
7062
"eslint": "^7.24.0",
7163
"eslint-config-env": "^19.0.0",
7264
"eslint-config-prettier": "^8.2.0",
65+
"eslint-plugin-compat": "^3.9.0",
7366
"eslint-plugin-import": "^2.22.1",
7467
"eslint-plugin-jsdoc": "^32.3.0",
7568
"eslint-plugin-node": "^11.1.0",
7669
"eslint-plugin-prettier": "^3.4.0",
7770
"eslint-plugin-react": "^7.23.2",
7871
"eslint-plugin-react-hooks": "^4.2.0",
7972
"fetch-blob": "^2.1.1",
80-
"formdata-node": "^2.4.0",
81-
"graphql": "^15.5.0",
82-
"graphql-api-koa": "^6.0.0",
73+
"filter-console": "^0.1.1",
74+
"formdata-node": "^2.5.0",
8375
"gzip-size": "^6.0.0",
8476
"jsdoc-md": "^9.1.1",
85-
"koa": "^2.13.1",
86-
"koa-bodyparser": "^4.3.0",
8777
"node-fetch": "^3.0.0-beta.9",
8878
"prettier": "^2.2.1",
8979
"react": "^17.0.2",
@@ -94,11 +84,9 @@
9484
"webpack": "^5.33.2"
9585
},
9686
"scripts": {
97-
"prepare": "npm run prepare:clean && npm run prepare:babel && npm run prepare:jsdoc && npm run prepare:prettier",
98-
"prepare:clean": "rm -rf private public test",
99-
"prepare:babel": "babel src -d . --keep-file-extension",
87+
"prepare": "npm run prepare:jsdoc && npm run prepare:prettier",
10088
"prepare:jsdoc": "jsdoc-md",
101-
"prepare:prettier": "prettier --write private public test readme.md",
89+
"prepare:prettier": "prettier --write readme.md",
10290
"test": "npm run test:eslint && npm run test:prettier && npm run test:api",
10391
"test:eslint": "eslint --ext mjs,js .",
10492
"test:prettier": "prettier -c .",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
3+
/**
4+
* Creates an argument error message for a production environment.
5+
* @kind function
6+
* @name createArgErrorMessageProd
7+
* @param {number} argNumber Argument number (starts at 1).
8+
* @returns {string} Error message.
9+
* @ignore
10+
*/
11+
module.exports = function createArgErrorMessageProd(argNumber) {
12+
// Argument checks are skipped for this function as it’s supposed to be ultra
13+
// lightweight for production, and all the times it’s used in the project are
14+
// tested anyway.
15+
return `Argument ${argNumber} type invalid.`;
16+
};

private/useForceUpdate.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
3+
const { useReducer } = require('react');
4+
5+
/**
6+
* A React hook to force the component to update and re-render.
7+
* @kind function
8+
* @name useForceUpdate
9+
* @returns {Function} Forces an update.
10+
* @see [React hooks FAQ](https://reactjs.org/docs/hooks-faq.html#is-there-something-like-forceupdate).
11+
* @see [Gotcha explanation](https://github.com/CharlesStover/use-force-update/issues/18#issuecomment-554486618).
12+
* @ignore
13+
*/
14+
module.exports = function useForceUpdate() {
15+
return useReducer(() => Symbol())[1];
16+
};

public/Cache.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
'use strict';
2+
3+
const isObject = require('isobject/index.cjs.js');
4+
const createArgErrorMessageProd = require('../private/createArgErrorMessageProd');
5+
6+
/**
7+
* Cache store.
8+
* @kind class
9+
* @name Cache
10+
* @param {object} [store={}] Initial [cache store]{@link Cache#store}. Useful for hydrating cache data from a server side render prior to the initial client side render.
11+
* @example <caption>Ways to `import`.</caption>
12+
* ```js
13+
* import { Cache } from 'graphql-react';
14+
* ```
15+
*
16+
* ```js
17+
* import Cache from 'graphql-react/public/Cache.js';
18+
* ```
19+
* @example <caption>Ways to `require`.</caption>
20+
* ```js
21+
* const { Cache } = require('graphql-react');
22+
* ```
23+
*
24+
* ```js
25+
* const Cache = require('graphql-react/public/Cache');
26+
* ```
27+
* @example <caption>Construct a new instance.</caption>
28+
* ```js
29+
* const cache = new Cache();
30+
* ```
31+
*/
32+
module.exports = class Cache extends EventTarget {
33+
constructor(store = {}) {
34+
super();
35+
36+
if (!isObject(store))
37+
throw new TypeError(
38+
typeof process === 'object' && process.env.NODE_ENV !== 'production'
39+
? 'Constructor argument 1 `store` must be an object.'
40+
: createArgErrorMessageProd(1)
41+
);
42+
43+
/**
44+
* Store of cache [keys]{@link CacheKey} and [values]{@link CacheValue}.
45+
* @kind member
46+
* @name Cache#store
47+
* @type {object}
48+
*/
49+
this.store = store;
50+
}
51+
};
52+
53+
/**
54+
* Signals that a [cache store]{@link Cache#store} entry was set. The event name
55+
* starts with the [cache key]{@link CacheKey} of the set entry, followed by
56+
* `/set`.
57+
* @kind event
58+
* @name Cache#event:set
59+
* @type {CustomEvent}
60+
* @prop {object} detail Event detail.
61+
* @prop {CacheValue} detail.cacheValue Cache value that was set.
62+
*/
63+
64+
/**
65+
* Signals that a [cache store]{@link Cache#store} entry is now stale (often due
66+
* to a mutation) and should probably be reloaded. The event name starts with
67+
* the [cache key]{@link CacheKey} of the stale entry, followed by `/stale`.
68+
* @kind event
69+
* @name Cache#event:stale
70+
* @type {CustomEvent}
71+
*/
72+
73+
/**
74+
* Signals that a [cache store]{@link Cache#store} entry will be deleted unless
75+
* the event is canceled via `event.preventDefault()`. The event name starts
76+
* with the [cache key]{@link CacheKey} of the entry being pruned, followed by
77+
* `/prune`.
78+
* @kind event
79+
* @name Cache#event:prune
80+
* @type {CustomEvent}
81+
*/
82+
83+
/**
84+
* Signals that a [cache store]{@link Cache#store} entry was deleted. The event
85+
* name starts with the [cache key]{@link CacheKey} of the deleted entry,
86+
* followed by `/delete`.
87+
* @kind event
88+
* @name Cache#event:delete
89+
* @type {CustomEvent}
90+
*/

public/CacheContext.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
3+
const { createContext } = require('react');
4+
5+
/**
6+
* React context for a [`Cache`]{@link Cache} instance.
7+
* @kind member
8+
* @name CacheContext
9+
* @type {object}
10+
* @prop {Function} Provider [React context provider component](https://reactjs.org/docs/context.html#contextprovider).
11+
* @prop {Function} Consumer [React context consumer component](https://reactjs.org/docs/context.html#contextconsumer).
12+
* @example <caption>Ways to `import`.</caption>
13+
* ```js
14+
* import { CacheContext } from 'graphql-react';
15+
* ```
16+
*
17+
* ```js
18+
* import CacheContext from 'graphql-react/public/CacheContext.js';
19+
* ```
20+
* @example <caption>Ways to `require`.</caption>
21+
* ```js
22+
* const { CacheContext } = require('graphql-react');
23+
* ```
24+
*
25+
* ```js
26+
* const CacheContext = require('graphql-react/public/CacheContext');
27+
* ```
28+
*/
29+
const CacheContext = createContext();
30+
31+
if (typeof process === 'object' && process.env.NODE_ENV !== 'production')
32+
CacheContext.displayName = 'CacheContext';
33+
34+
module.exports = CacheContext;

public/HYDRATION_TIME_MS.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
/**
4+
* Number of milliseconds after the first client render that’s considered the
5+
* hydration time; during which the
6+
* [`useAutoLoad`]{@link useAutoLoad} React hook won’t load if the
7+
* cache entry is already populated.
8+
* @kind constant
9+
* @name HYDRATION_TIME_MS
10+
* @type {number}
11+
* @example <caption>Ways to `import`.</caption>
12+
* ```js
13+
* import { HYDRATION_TIME_MS } from 'graphql-react';
14+
* ```
15+
*
16+
* ```js
17+
* import HYDRATION_TIME_MS from 'graphql-react/public/HYDRATION_TIME_MS.js';
18+
* ```
19+
* @example <caption>Ways to `require`.</caption>
20+
* ```js
21+
* const { HYDRATION_TIME_MS } = require('graphql-react');
22+
* ```
23+
*
24+
* ```js
25+
* const HYDRATION_TIME_MS = require('graphql-react/public/HYDRATION_TIME_MS');
26+
* ```
27+
*/
28+
module.exports = 1000;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
3+
const { createContext } = require('react');
4+
5+
/**
6+
* React context for the client side hydration [time stamp]{@link HighResTimeStamp}.
7+
* @kind member
8+
* @name HydrationTimeStampContext
9+
* @type {object}
10+
* @prop {Function} Provider [React context provider component](https://reactjs.org/docs/context.html#contextprovider).
11+
* @prop {Function} Consumer [React context consumer component](https://reactjs.org/docs/context.html#contextconsumer).
12+
* @example <caption>Ways to `import`.</caption>
13+
* ```js
14+
* import { HydrationTimeStampContext } from 'graphql-react';
15+
* ```
16+
*
17+
* ```js
18+
* import HydrationTimeStampContext from 'graphql-react/public/HydrationTimeStampContext.js';
19+
* ```
20+
* @example <caption>Ways to `require`.</caption>
21+
* ```js
22+
* const { HydrationTimeStampContext } = require('graphql-react');
23+
* ```
24+
*
25+
* ```js
26+
* const HydrationTimeStampContext = require('graphql-react/public/HydrationTimeStampContext');
27+
* ```
28+
*/
29+
const HydrationTimeStampContext = createContext();
30+
31+
if (typeof process === 'object' && process.env.NODE_ENV !== 'production')
32+
HydrationTimeStampContext.displayName = 'HydrationTimeStampContext';
33+
34+
module.exports = HydrationTimeStampContext;

0 commit comments

Comments
 (0)