Skip to content

Commit 8392667

Browse files
authored
Merge pull request #10 from Bloomca/improvement/change-caching
Improvement/change caching
2 parents aaba5c9 + 43c49a8 commit 8392667

File tree

5 files changed

+81
-10
lines changed

5 files changed

+81
-10
lines changed

docs/api/createTile.md

+15-2
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,28 @@ const userTile = createTile({
5555
// downloaded, then it won't be downloaded again at all, unless
5656
// we will invoke with the second parameter `forceAsync: true`:
5757
// dispatch(actions.hn_api.user({ id: 'someID' }, { forceAsync: true }));
58+
//
59+
// also, it means that there will be only one simulatenous request
60+
// other dispatches will return exactly the same promise
5861
caching: true,
5962
});
6063
```
6164

6265
## Caching
6366

64-
As it was already mentioned, you can set property `caching` to `true`, and it will make same requests to query only once, and after you will have to send an object with a key `forceAsync: true` as a second parameter to invoked function, to execute your function again.
67+
Asynchronous tiles support caching out of the box, you just have to set property `caching` to `true`. It will make two things happen – it won't invoke the same function if the data is already presented there, and also it will prevent the same function be invoked again in case it is already being processing (but dispatched action will return exactly the same promise, so you can safely await for it, and then query the state - it will be an updated value). The latter case is interesting – it basically means that we get rid of race conditions, and we are safe to query same endpoints in a declarative way, without worrying of several requests to same endpoints.
68+
69+
If you have already dispatched an action with enabled caching, and you want to invoke this action again, then you would have to send an object with a key `forceAsync: true` as a second parameter to invoked function:
70+
```js
71+
dispatch(actions.api.users({ id: 'someID' }, { forceAsync: true }));
72+
```
73+
74+
Though the same promise thing might seem as a magical part, it is not! In order to make it work for each requests in Node.js, we keep this object inside middleware (so it belongs to the instance of a store), and it means that in order to make it work we have to use [redux-tiles' middleware](./createMiddleware.md), or pass `promisesStorage` object to [redux-thunk](https://github.com/gaearon/redux-thunk):
75+
```js
76+
applyMiddleware(thunk.withExtraArgument({ promisesStorage: {} }))
77+
```
6578

66-
But also there is another caching, which is enabled if you use [our middleware](./createMiddleware.md). It tracks all requests based on type of the module and nesting, and stores promises, and in case it was invoked again, it will simple wait existing promise, so you can await result without any additional requests. Because of that, you should be very careful on Node.js – please, instantiate store for each request, in case you want to dispatch some async requests.
79+
Redux-tiles' middleware will inject this object automatically. This object is also crucial for server-side rendering, in case we want to prefetch data – this object collects all requests (regardless of caching; it will just filter same actions if caching is enabled) and `waitTiles` will await all of them.
6780

6881
## Function
6982

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "redux-tiles",
3-
"version": "0.4.10",
3+
"version": "0.5.0",
44
"description": "Library to create and easily compose redux pieces together in less verbose manner",
55
"jsnext:main": "lib/es2015/index.js",
66
"module": "lib/es2015/index.js",

src/tiles/actions.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,13 @@ export function asyncAction({
5151
const path: string[]|null = nesting ? nesting(params) : null;
5252

5353
const getIdentificator: string = createType({ type, path });
54-
const activePromise: Promise<any>|undefined = promisesStorage[getIdentificator];
5554

56-
if (activePromise) {
57-
return activePromise;
55+
if (caching) {
56+
const activePromise: Promise<any>|undefined = promisesStorage[getIdentificator];
57+
58+
if (activePromise) {
59+
return activePromise;
60+
}
5861
}
5962

6063
if (caching && !forceAsync) {
@@ -70,10 +73,7 @@ export function asyncAction({
7073
payload: { path }
7174
});
7275

73-
const promise: Promise<any> = fn({ params, dispatch, getState, ...middlewares });
74-
promisesStorage[getIdentificator] = promise;
75-
76-
return promise
76+
const promise: Promise<any> = fn({ params, dispatch, getState, ...middlewares })
7777
.then((data: any) => {
7878
dispatch({
7979
type: SUCCESS,
@@ -89,6 +89,10 @@ export function asyncAction({
8989
});
9090
promisesStorage[getIdentificator] = undefined;
9191
});
92+
93+
promisesStorage[getIdentificator] = promise;
94+
95+
return promise;
9296
});
9397
}
9498

test/actions.spec.ts

+15
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,21 @@ test('action should detect thunk middleware', () => {
2424
expect(fn.calledWith({ dispatch, getState, params })).toBe(true);
2525
});
2626

27+
test('action should detect thunk middleware with additional params', () => {
28+
const fn = stub().returns(Promise.resolve());
29+
const tile = createTile({
30+
type: 'Some',
31+
fn
32+
});
33+
34+
const dispatch = () => {};
35+
const getState = () => {};
36+
const params = {};
37+
const additionalParams = { some: true };
38+
tile.action(params)(dispatch, getState, additionalParams);
39+
expect(fn.calledWith({ dispatch, getState, params, some: true })).toBe(true);
40+
});
41+
2742
test('action should detect our middleware', () => {
2843
const fn = stub().returns(Promise.resolve());
2944
const tile = createTile({

test/tiles.spec.ts

+39
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createTile, createSyncTile, createEntities, createMiddleware } from '../src';
22
import { createStore, applyMiddleware } from 'redux';
3+
import { sleep } from 'delounce';
34
import { spy } from 'sinon';
45

56
test('createTile should be able to reflect all passed info', () => {
@@ -146,4 +147,42 @@ test('createTile should update values after dispatching action with rejection co
146147
await store.dispatch(actions.some('some'));
147148
const result = selectors.some(store.getState());
148149
expect(result).toEqual({ isPending: false, data: null, error: { some: true } });
150+
});
151+
152+
test('createTile should keep only one active request if caching', async () => {
153+
const someTile = createTile({
154+
type: 'some',
155+
caching: true,
156+
fn: async () => {
157+
await sleep(10);
158+
159+
return { some: true };
160+
}
161+
});
162+
const tiles = [someTile];
163+
const { reducer, actions, selectors } = createEntities(tiles);
164+
const { middleware } = createMiddleware();
165+
const store = createStore(reducer, applyMiddleware(middleware));
166+
const promise1 = store.dispatch(actions.some('some'));
167+
const promise2 = store.dispatch(actions.some('some'));
168+
expect(promise1).toBe(promise2);
169+
});
170+
171+
test('createTile should keep different requests if caching', async () => {
172+
const someTile = createTile({
173+
type: 'some',
174+
caching: true,
175+
fn: async () => {
176+
await sleep(10);
177+
178+
return { some: true };
179+
}
180+
});
181+
const tiles = [someTile];
182+
const { reducer, actions, selectors } = createEntities(tiles);
183+
const { middleware } = createMiddleware();
184+
const store = createStore(reducer, applyMiddleware(middleware));
185+
const promise1 = store.dispatch(actions.some('some'));
186+
const promise2 = store.dispatch(actions.some('some'));
187+
expect(promise1).toBe(promise2);
149188
});

0 commit comments

Comments
 (0)