Skip to content

Commit 7a6e845

Browse files
author
nebarf
committed
Readme: caching
1 parent 9a8a53f commit 7a6e845

File tree

5 files changed

+355
-32
lines changed

5 files changed

+355
-32
lines changed

README.md

Lines changed: 274 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ yarn add react-http-fetch
7474
You can override the default configuration used by the http client to perform any request by using the `HttpClientConfigProvider`:
7575

7676
```js
77+
import React from 'react';
7778
import { defaultHttpReqConfig, HttpClientConfigProvider } from 'react-http-fetch';
7879

7980
function Child() {
@@ -128,9 +129,9 @@ Below the complete set of options you can provide to the `HttpClientConfigProvid
128129
The `useHttpClient` hook return a set of methods to perform http requests. The `request` function is the lowest level one, all other exposed functions are just decorators around it. Below a basic example using `request`:
129130

130131
```js
132+
import React from 'react';
131133
import { useHttpClient } from 'react-http-fetch';
132134

133-
134135
function App() {
135136
const { request } = useHttpClient();
136137

@@ -311,6 +312,7 @@ Returns an array of two elements, the first one embeds the state of the http req
311312

312313
### Example – Http request hook triggered automatically on component mount
313314
```js
315+
import React from 'react';
314316
import { useHttpRequest } from 'react-http-fetch';
315317

316318
function App() {
@@ -335,7 +337,7 @@ export default App;
335337
### Example – Http request hook triggered manually on component mount
336338
```js
337339
import { useHttpRequest } from 'react-http-fetch';
338-
import { useEffect } from 'react';
340+
import React, { useEffect } from 'react';
339341

340342
function App() {
341343
const [state, request] = useHttpRequest({
@@ -355,6 +357,74 @@ export default App;
355357

356358
### Example – Http post request hook
357359

360+
```js
361+
import React, { useState } from 'react';
362+
import { useHttpPost } from 'react-http-fetch';
363+
364+
function App() {
365+
const [inputs, setInputs] = useState({});
366+
367+
const handleChange = (event) => {
368+
const name = event.target.name;
369+
const value = event.target.value;
370+
setInputs(values => ({...values, [name]: value}))
371+
}
372+
373+
const [, createPostRequest] = useHttpPost({
374+
baseUrlOverride: 'https://jsonplaceholder.typicode.com',
375+
relativeUrl: 'posts',
376+
});
377+
378+
const createPost = async (event) => {
379+
event.preventDefault();
380+
const { postTitle, postBody } = inputs;
381+
382+
const reqBody = { title: postTitle, body: postBody };
383+
try {
384+
// Providing request options when running the request.
385+
// Provided options will be merged to the one provided
386+
// to the hook useHttpPost.
387+
await createPostRequest({
388+
requestOptions: { body: reqBody }
389+
});
390+
alert('Post created!');
391+
} catch (error) {
392+
console.error(error);
393+
alert('An error occured. Check the browser console.');
394+
}
395+
};
396+
397+
return (
398+
<form onSubmit={createPost}>
399+
<label style={{ display: 'block' }}>
400+
Title:
401+
<input
402+
type="text"
403+
name="postTitle"
404+
value={inputs.postTitle || ""}
405+
onChange={handleChange}
406+
/>
407+
</label>
408+
<label style={{ display: 'block' }}>
409+
Body:
410+
<input
411+
type="text"
412+
name="postBody"
413+
value={inputs.postBody || ""}
414+
onChange={handleChange}
415+
/>
416+
</label>
417+
<button type="submit">
418+
Create Post
419+
</button>
420+
</form>
421+
);
422+
}
423+
424+
export default App;
425+
```
426+
427+
358428
<br>
359429

360430
## Events
@@ -366,7 +436,7 @@ Every time a request is executed the events shown below will be emitted. Each ev
366436
| [RequestErroredEvent](src/events-manager/events/request-errored-event.ts) | [HttpError](src/errors/http-error.ts) |
367437
| [RequestSuccededEvent](src/events-manager/events/request-succeded-event.ts) |[RequestSuccededEventPayload](src/events-manager/events/request-succeded-event.ts) |
368438

369-
It's possible to subscribe a specific event using the [useHttpEvent](src/events-manager/use-http-event.ts) hook as shown below:
439+
You can subscribe a specific event using the [useHttpEvent](src/events-manager/use-http-event.ts) hook as shown below:
370440

371441
```js
372442
import { useState } from 'react';
@@ -398,6 +468,207 @@ export default App;
398468
<br>
399469

400470
## Caching
471+
Any request can be cached by setting the `maxAge` (expressed in milliseconds) parameter as part of the reuqest options as shown below:
472+
473+
```js
474+
import { useHttpRequest } from 'react-http-fetch';
475+
import React from 'react';
476+
477+
function App() {
478+
const [state, request] = useHttpRequest({
479+
baseUrlOverride: 'https://jsonplaceholder.typicode.com',
480+
relativeUrl: 'todos/1',
481+
requestOptions: { maxAge: 60000 } // Cache for 1 minute
482+
});
483+
484+
const fetchTodo = () => {
485+
const { reqResult } = request();
486+
reqResult.then(res => console.log(res))
487+
};
488+
489+
return (
490+
<>
491+
<div>
492+
{`Todo name: ${(state && state.data && state.data.title) || ''}`}
493+
</div>
494+
<button type="button" onClick={fetchTodo}>
495+
Make request
496+
</button>
497+
</>
498+
);
499+
}
500+
501+
export default App;
502+
```
503+
504+
By default the http client uses an in-memory cache, so it will be flushed everytime a full app refresh is performed. You can override the default caching strategy by providing your own cache. The example below a http cache based on session storage:
505+
506+
```js
507+
import React from 'react';
508+
import { HttpClientConfigProvider, HttpCache, useHttpRequest } from 'react-http-fetch';
509+
510+
export class HttpSessionStorageCacheService extends HttpCache {
511+
/**
512+
* The local cache providing for a request identifier
513+
* the corresponding parsed response.
514+
*/
515+
store = window.sessionStorage;
516+
517+
/**
518+
* Gets the unique key used as idenitifier to store
519+
* a cached response for the given http request.
520+
*/
521+
_getRequestIdentifier(request) {
522+
const fullUrl = request.urlWithParams;
523+
return fullUrl;
524+
}
525+
526+
/**
527+
* Tells if a cached entry is expired.
528+
*/
529+
_isEntryExpired(entry) {
530+
const nowTime = new Date().getTime();
531+
const cachedAt = entry.cachedAt instanceof Date ? entry.cachedAt : new Date(entry.cachedAt);
532+
const cachedTime = cachedAt.getTime();
533+
return cachedTime + entry.maxAge < nowTime;
534+
}
535+
536+
/**
537+
* Gets the cached entry associated with the request.
538+
*/
539+
_getEntry(request) {
540+
const reqIdentifier = this._getRequestIdentifier(request);
541+
const storedEntry = this.store.getItem(reqIdentifier);
542+
543+
try {
544+
const parsedEntry = JSON.parse(storedEntry);
545+
return parsedEntry;
546+
} catch(err) {
547+
return null;
548+
}
549+
}
550+
551+
/**
552+
* Removes a cached entry.
553+
*/
554+
_removeEntry(entry) {
555+
this.store.removeItem(entry.identifier);
556+
}
557+
558+
/**
559+
* Determines if for the given request is available a cached response.
560+
*/
561+
_has(request) {
562+
const key = this._getRequestIdentifier(request);
563+
return this.store.hasOwnProperty(key);
564+
}
565+
566+
/**
567+
* Tells if the cached request is expired or not.
568+
*/
569+
_isExpired(request) {
570+
const cachedEntry = this._getEntry(request);
571+
if (!cachedEntry) {
572+
return true;
573+
}
574+
575+
return this._isEntryExpired(cachedEntry);
576+
}
577+
578+
/**
579+
* Gets the cached entry in the map for the given request.
580+
*/
581+
get(request) {
582+
const cachedEntry = this._getEntry(request);
583+
if (!cachedEntry) {
584+
return undefined;
585+
}
586+
587+
const isExpired = this._isEntryExpired(cachedEntry);
588+
return isExpired ? undefined : cachedEntry.response;
589+
}
590+
591+
/**
592+
* Puts a new cached response for the given request.
593+
*/
594+
put(request, response) {
595+
if (!request.maxAge) {
596+
return;
597+
}
598+
599+
const reqKey = this._getRequestIdentifier(request);
600+
const entry = {
601+
response,
602+
identifier: reqKey,
603+
cachedAt: new Date(),
604+
maxAge: request.maxAge,
605+
};
606+
607+
// Update and flush the cache.
608+
this.store.setItem(reqKey, JSON.stringify(entry));
609+
610+
// Remove the entry from the cache once expired.
611+
const timerRef = setTimeout(() => {
612+
this._removeEntry(entry);
613+
clearTimeout(timerRef);
614+
}, request.maxAge);
615+
}
616+
617+
/**
618+
* Founds all expired entry and deletes them from the cache.
619+
*/
620+
flush() {
621+
this.store.forEach((entry) => {
622+
const isEntryExpired = this._isEntryExpired(entry);
623+
624+
if (isEntryExpired) {
625+
this._removeEntry(entry);
626+
}
627+
});
628+
}
629+
}
630+
631+
const httpCache = new HttpSessionStorageCacheService();
632+
633+
function Child() {
634+
const [state, request] = useHttpRequest({
635+
baseUrlOverride: 'https://jsonplaceholder.typicode.com',
636+
relativeUrl: 'todos/1',
637+
requestOptions: { maxAge: 60000 } // Cache for 1 minute
638+
});
639+
640+
console.log('Request state:', state.data);
641+
642+
const fetchTodo = () => {
643+
const { reqResult } = request();
644+
reqResult.then(res => console.log('Request res: ', res))
645+
};
646+
647+
return (
648+
<>
649+
<div>
650+
{`Todo name: ${(state && state.data && state.data.title) || ''}`}
651+
</div>
652+
<button type="button" onClick={fetchTodo}>
653+
Make request
654+
</button>
655+
</>
656+
);
657+
};
658+
659+
660+
function App() {
661+
const httpReqConfig = { cache: httpCache };
662+
663+
return (
664+
<HttpClientConfigProvider config={httpReqConfig}>
665+
<Child />
666+
</HttpClientConfigProvider>
667+
);
668+
}
669+
670+
export default App;
671+
```
401672

402673
<br>
403674

src/client/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ export interface PerformHttpRequestParams {
1717
}
1818

1919
export type HttpClientRequest = <HttpResponse = Response>(
20-
params: PerformHttpRequestParams
20+
params: Partial<PerformHttpRequestParams>
2121
) => Promise<HttpResponse>;
2222

2323
export type HttpClientAbortableRequest = <HttpResponse = Response>(
24-
params: PerformHttpRequestParams
24+
params: Partial<PerformHttpRequestParams>
2525
) => AbortableHttpRequestReturn<HttpResponse>;
2626

2727
export interface UseHttpClientReturn {

0 commit comments

Comments
 (0)