Skip to content

Commit 8e94d33

Browse files
committed
New internal event system.
Fixes jaydenseric/graphql-react#10.
1 parent 73135bc commit 8e94d33

File tree

6 files changed

+113
-128
lines changed

6 files changed

+113
-128
lines changed

changelog.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Major
66

77
- The `Query` (and the internal `GraphQLQuery`) component take an `operation` prop instead of separate `variables` and `query` props. This makes the implementation a little more elegant, is more consistent with the `GraphQL.query` API and allows sending custom GraphQL operation fields.
8+
- New internal event system, fixing [#10](https://github.com/jaydenseric/graphql-react/issues/10). Now the `loading` parameter of `Query` component render functions change when identical requests are loaded elsewhere in the app.
89

910
### Minor
1011

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"extract-files": "^4.0.0",
4343
"fast-deep-equal": "^2.0.1",
4444
"fnv1a": "^1.0.1",
45+
"mitt": "^1.1.3",
4546
"object-assign": "^4.1.1",
4647
"prop-types": "^15.6.1"
4748
},

readme.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,9 @@ Queries a GraphQL server.
182182
183183
Resets the [GraphQL cache](#graphql-instance-property-cache). Useful when a user logs out.
184184
185-
| Parameter | Type | Description |
186-
| :----------------------- | :------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- |
187-
| `exceptFetchOptionsHash` | [string](https://mdn.io/string)? | A [fetch options](#type-fetchoptions) hash to exempt a request from cache deletion. Useful for resetting cache after a mutation, preserving the mutation cache. |
185+
| Parameter | Type | Description |
186+
| :----------------------- | :------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- |
187+
| `exceptFetchOptionsHash` | [string](https://mdn.io/string)? | A [fetch options](#type-fetchoptions) hash for cache to exempt from deletion. Useful for resetting cache after a mutation, preserving the mutation cache. |
188188
189189
##### Examples
190190

src/components.mjs

+56-63
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ class GraphQLQuery extends React.Component {
8484
this.state = { loading: props.loadOnMount }
8585

8686
if (props.loadOnMount) {
87+
// Populate the request cache state to render data for SSR and while
88+
// the load called in componentDidMount on the client fetches fresh data.
89+
8790
const fetchOptions = props.graphql.constructor.fetchOptions(
8891
props.operation
8992
)
@@ -95,31 +98,55 @@ class GraphQLQuery extends React.Component {
9598
)
9699

97100
this.state.requestCache = props.graphql.cache[this.state.fetchOptionsHash]
98-
99-
// Listen for changes to the request cache.
100-
this.props.graphql.onCacheUpdate(
101-
this.state.fetchOptionsHash,
102-
this.handleCacheUpdate
103-
)
104101
}
102+
103+
// Setup listeners.
104+
this.props.graphql.on('fetch', this.onFetch)
105+
this.props.graphql.on('cache', this.onCache)
106+
this.props.graphql.on('reset', this.onReset)
105107
}
106108

107109
/**
108-
* Handles [request cache]{@link RequestCache} updates.
110+
* Handles [GraphQL]{@link GraphQL} fetch
109111
* @kind function
110-
* @name GraphQLQuery#handleCacheUpdate
111-
* @param {RequestCache} requestCache Request cache.
112+
* @name GraphQLQuery#onFetch
113+
* @param {Object} details Event details.
114+
* @param {string} [details.fetchOptionsHash] The [fetch options]{@link FetchOptions} hash.
112115
* @ignore
113116
*/
114-
handleCacheUpdate = requestCache => {
115-
if (
116-
// Cache has been reset and…
117-
!requestCache &&
118-
// …the component is to load on reset cache.
119-
this.props.loadOnReset
120-
)
121-
this.load()
122-
else this.setState({ requestCache })
117+
onFetch = ({ fetchOptionsHash }) => {
118+
if (fetchOptionsHash === this.state.fetchOptionsHash)
119+
this.setState({ loading: true })
120+
}
121+
122+
/**
123+
* Handles [GraphQL]{@link GraphQL} cache
124+
* @kind function
125+
* @name GraphQLQuery#onCache
126+
* @param {Object} details Event details.
127+
* @param {string} [details.fetchOptionsHash] The cache [fetch options]{@link FetchOptions} hash.
128+
* @ignore
129+
*/
130+
onCache = ({ fetchOptionsHash }) => {
131+
if (fetchOptionsHash === this.state.fetchOptionsHash)
132+
this.setState({
133+
loading: false,
134+
requestCache: this.props.graphql.cache[fetchOptionsHash]
135+
})
136+
}
137+
138+
/**
139+
* Handles [GraphQL]{@link GraphQL} reset
140+
* @kind function
141+
* @name GraphQLQuery#onReset
142+
* @param {Object} details Event details.
143+
* @param {string} [details.exceptFetchOptionsHash] A [fetch options]{@link FetchOptions} hash for cache exempt from deletion.
144+
* @ignore
145+
*/
146+
onReset = ({ exceptFetchOptionsHash }) => {
147+
if (exceptFetchOptionsHash !== this.state.fetchOptionsHash)
148+
if (this.props.loadOnReset) this.load()
149+
else this.setState({ requestCache: null })
123150
}
124151

125152
/**
@@ -130,43 +157,13 @@ class GraphQLQuery extends React.Component {
130157
* @ignore
131158
*/
132159
load = () => {
133-
const stateUpdate = { loading: true }
134160
const { fetchOptionsHash, cache, request } = this.props.graphql.query({
135161
operation: this.props.operation,
136162
fetchOptionsOverride: this.props.fetchOptionsOverride,
137163
resetOnLoad: this.props.resetOnLoad
138164
})
139165

140-
if (
141-
// The fetch options hash has changed…
142-
fetchOptionsHash !== this.state.fetchOptionsHash
143-
) {
144-
stateUpdate.fetchOptionsHash = fetchOptionsHash
145-
146-
// Stop listening for the old request cache updates.
147-
this.props.graphql.offCacheUpdate(
148-
this.state.fetchOptionsHash, // Old hash.
149-
this.handleCacheUpdate
150-
)
151-
152-
// Listen for the new request cache updates.
153-
this.props.graphql.onCacheUpdate(
154-
fetchOptionsHash, // New hash.
155-
this.handleCacheUpdate
156-
)
157-
}
158-
159-
if (cache)
160-
// Use past cache for this request during load. It might not already
161-
// be in state if the request was cached via another component.
162-
stateUpdate.requestCache = cache
163-
164-
this.setState(stateUpdate, () =>
165-
request.then(() =>
166-
// Request done. Elsewhere a cache listener updates the state cache.
167-
this.setState({ loading: false })
168-
)
169-
)
166+
this.setState({ loading: true, fetchOptionsHash, cache })
170167

171168
return request
172169
}
@@ -190,15 +187,13 @@ class GraphQLQuery extends React.Component {
190187
* @ignore
191188
*/
192189
componentDidUpdate({ operation }) {
193-
if (
194-
// Load on cache reset enabled and…
195-
this.props.loadOnReset &&
196-
// …a load has happened before and…
197-
this.state.fetchOptionsHash &&
198-
// …props that may affect the cache have changed.
199-
!equal(operation, this.props.operation)
200-
)
201-
this.load()
190+
if (!equal(operation, this.props.operation))
191+
if (this.props.loadOnMount) this.load()
192+
else
193+
this.setState({
194+
fetchOptionsHash: null,
195+
requestCache: null
196+
})
202197
}
203198

204199
/**
@@ -208,11 +203,9 @@ class GraphQLQuery extends React.Component {
208203
* @ignore
209204
*/
210205
componentWillUnmount() {
211-
if (this.state.fetchOptionsHash)
212-
this.props.graphql.offCacheUpdate(
213-
this.state.fetchOptionsHash,
214-
this.handleCacheUpdate
215-
)
206+
this.props.graphql.off('fetch', this.onFetch)
207+
this.props.graphql.off('cache', this.onCache)
208+
this.props.graphql.off('reset', this.onReset)
216209
}
217210

218211
/**

src/graphql.mjs

+47-52
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import fnv1a from 'fnv1a'
21
import { extractFiles } from 'extract-files'
2+
import fnv1a from 'fnv1a'
3+
import mitt from 'mitt'
34

45
/**
56
* A lightweight GraphQL client that caches requests.
@@ -82,7 +83,8 @@ export class GraphQL {
8283
// eslint-disable-next-line require-jsdoc
8384
constructor({ cache = {} } = {}) {
8485
/**
85-
* GraphQL [request cache]{@link RequestCache} map, keyed by [fetch options]{@link FetchOptions} hashes.
86+
* GraphQL [request cache]{@link RequestCache} map, keyed by
87+
* [fetch options]{@link FetchOptions} hashes.
8688
* @kind member
8789
* @name GraphQL#cache
8890
* @type {Object.<string, RequestCache>}
@@ -92,63 +94,55 @@ export class GraphQL {
9294
* ```
9395
*/
9496
this.cache = cache
95-
}
9697

97-
requests = {}
98-
listeners = {}
98+
/**
99+
* Loading requests.
100+
* @kind member
101+
* @name GraphQL#requests
102+
* @type {Promise<RequestCache>}
103+
* @ignore
104+
*/
105+
this.requests = {}
99106

100-
/**
101-
* Adds a cache update listener for a request.
102-
* @kind function
103-
* @name GraphQL#onCacheUpdate
104-
* @param {string} fetchOptionsHash [fetch options]{@link FetchOptions} hash.
105-
* @param {CacheUpdateCallback} callback Callback.
106-
* @ignore
107-
*/
108-
onCacheUpdate = (fetchOptionsHash, callback) => {
109-
if (!this.listeners[fetchOptionsHash]) this.listeners[fetchOptionsHash] = []
110-
this.listeners[fetchOptionsHash].push(callback)
111-
}
107+
const { on, off, emit } = mitt()
112108

113-
/**
114-
* Removes a cache update listener for a request.
115-
* @kind function
116-
* @name GraphQL#offCacheUpdate
117-
* @param {string} fetchOptionsHash [fetch options]{@link FetchOptions} hash.
118-
* @param {CacheUpdateCallback} callback Callback.
119-
* @ignore
120-
*/
121-
offCacheUpdate = (fetchOptionsHash, callback) => {
122-
if (this.listeners[fetchOptionsHash]) {
123-
this.listeners[fetchOptionsHash] = this.listeners[
124-
fetchOptionsHash
125-
].filter(listenerCallback => listenerCallback !== callback)
126-
if (!this.listeners[fetchOptionsHash].length)
127-
delete this.listeners[fetchOptionsHash]
128-
}
129-
}
109+
/**
110+
* Adds an event listener.
111+
* @kind function
112+
* @name GraphQL#on
113+
* @param {String} type Event type.
114+
* @param {function} handler Event handler.
115+
* @ignore
116+
*/
117+
this.on = on
130118

131-
/**
132-
* Triggers cache update listeners for a request.
133-
* @kind function
134-
* @name GraphQL#emitCacheUpdate
135-
* @param {string} fetchOptionsHash [fetch options]{@link FetchOptions} hash.
136-
* @param {RequestCache} requestCache Request cache.
137-
* @ignore
138-
*/
139-
emitCacheUpdate = (fetchOptionsHash, requestCache) => {
140-
if (this.listeners[fetchOptionsHash])
141-
this.listeners[fetchOptionsHash].forEach(callback =>
142-
callback(requestCache)
143-
)
119+
/**
120+
* Removes an event listener.
121+
* @kind function
122+
* @name GraphQL#off
123+
* @param {String} type Event type.
124+
* @param {function} handler Event handler.
125+
* @ignore
126+
*/
127+
this.off = off
128+
129+
/**
130+
* Emits an event with details to listeners.
131+
* @kind function
132+
* @name GraphQL#emit
133+
* @param {String} type Event type.
134+
* @param {*} [details] Event details.
135+
* @ignore
136+
*/
137+
this.emit = emit
144138
}
145139

146140
/**
147141
* Resets the [GraphQL cache]{@link GraphQL#cache}. Useful when a user logs
148142
* out.
149143
* @kind function
150144
* @name GraphQL#reset
151-
* @param {string} [exceptFetchOptionsHash] A [fetch options]{@link FetchOptions} hash to exempt a request from cache deletion. Useful for resetting cache after a mutation, preserving the mutation cache.
145+
* @param {string} [exceptFetchOptionsHash] A [fetch options]{@link FetchOptions} hash for cache to exempt from deletion. Useful for resetting cache after a mutation, preserving the mutation cache.
152146
* @example <caption>Resetting the GraphQL cache.</caption>
153147
* ```js
154148
* graphql.reset()
@@ -168,9 +162,7 @@ export class GraphQL {
168162

169163
// Emit cache updates after the entire cache has been updated, so logic in
170164
// listeners can assume cache for all requests is fresh and stable.
171-
fetchOptionsHashes.forEach(fetchOptionsHash =>
172-
this.emitCacheUpdate(fetchOptionsHash)
173-
)
165+
this.emit('reset', { exceptFetchOptionsHash })
174166
}
175167

176168
/**
@@ -192,6 +184,8 @@ export class GraphQL {
192184
new Error('Global fetch API or polyfill unavailable.')
193185
)
194186

187+
this.emit('fetch', { fetchOptionsHash })
188+
195189
return (this.requests[fetchOptionsHash] = fetcher(url, options))
196190
.then(
197191
response => {
@@ -222,11 +216,12 @@ export class GraphQL {
222216
.then(() => {
223217
// Cache the request.
224218
this.cache[fetchOptionsHash] = requestCache
225-
this.emitCacheUpdate(fetchOptionsHash, requestCache)
226219

227220
// Clear the done request.
228221
delete this.requests[fetchOptionsHash]
229222

223+
this.emit('cache', { fetchOptionsHash })
224+
230225
return requestCache
231226
})
232227
}

src/test.mjs

+5-10
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,7 @@ t.test('Cache reset.', async t => {
572572
.use(execute({ schema, rootValue }))
573573
const port = await startServer(t, app)
574574
const graphql = new GraphQL()
575+
575576
const {
576577
fetchOptionsHash: fetchOptionsHash1,
577578
request: request1
@@ -588,10 +589,8 @@ t.test('Cache reset.', async t => {
588589
await request1
589590

590591
const cacheBefore = JSON.stringify(graphql.cache)
591-
const {
592-
fetchOptionsHash: fetchOptionsHash2,
593-
request: request2
594-
} = graphql.query({
592+
593+
const { request: request2 } = graphql.query({
595594
fetchOptionsOverride(options) {
596595
options.url = `http://localhost:${port}`
597596
},
@@ -603,17 +602,13 @@ t.test('Cache reset.', async t => {
603602

604603
await request2
605604

606-
graphql.onCacheUpdate(fetchOptionsHash1, () => t.fail())
605+
t.plan(2)
607606

608-
const request2CacheListener = new Promise(resolve => {
609-
graphql.onCacheUpdate(fetchOptionsHash2, resolve)
610-
})
607+
graphql.on('reset', () => t.pass('`reset` event.'))
611608

612609
graphql.reset(fetchOptionsHash1)
613610

614611
const cacheAfter = JSON.stringify(graphql.cache)
615612

616-
t.notOk(await request2CacheListener, 'On cache update listener didn’t run.')
617-
618613
t.equals(cacheAfter, cacheBefore, 'Before and after reset cache match.')
619614
})

0 commit comments

Comments
 (0)