UNPKG

11.8 kBMarkdownView Raw
1# Redux-Tiles
2
3[![Build Status](https://travis-ci.org/Bloomca/redux-tiles.svg?branch=master)](https://travis-ci.org/Bloomca/redux-tiles)
4[![npm version](https://badge.fury.io/js/redux-tiles.svg)](https://badge.fury.io/js/redux-tiles)
5
6Redux is an awesome library to keep state management sane on scale. The problem, though, is that it is toooo verbose, and often you'd feel like you are doing literally the same thing again and again. This library tries to provide minimal abstraction on top of Redux, to allow easy composability, easy async requests, and sane testability.
7
8>**[More about rationale behind this library](http://blog.bloomca.me/2017/06/02/why-i-created-redux-tiles-library.html)**<br>
9>**[Examples](./examples)**
10> - [hacker news API](./examples/hacker-news-api)
11> - [calculator](./examples/calculator)
12
13## Installation
14
15To install latest stable version, run:
16```shell
17npm install --save redux-tiles
18```
19
20This package was built with the idea in mind, that people will use it usually using some bundling tool – [Webpack](https://webpack.js.org/), [Browserify](http://browserify.org/) or [Rollup](http://rollupjs.org/). The package itself is written in TypeScript, and therefore provides typings out of the box.
21
22If you for some reason don't use bundler, you can use UMD builds, which are located in [dist folder](https://unpkg.com/redux-tiles@0.6.0/dist/). Just include it in your page via `script` tag, and then you will have it under `window.ReduxTiles` global variable.
23
24## TOC:
25
26- [Example of use](#user-content-example-of-use)
27- [Rationale](#user-content-rationale)
28- [Integration API](#user-content-integration-api)
29- [Tiles API](#user-content-tiles-api)
30- [Nesting](#user-content-nesting)
31- [Middleware](#user-content-middleware)
32- [Server-side Rendering](#user-content-server-side-rendering)
33- [Selectors](#user-content-selectors)
34- [Tests](#user-content-tests)
35
36## Example of use
37
38```javascript
39import { createTile, createSyncTile } from 'redux-tiles';
40
41// sync tile to store information without any async stuff
42const loginStatus = createSyncTile({
43 type: ['user', 'loginStatus'],
44 fn: ({ params: status }) => ({
45 status,
46 timestamp: Date.now(),
47 }),
48});
49
50// request to the server
51// it is absolutely separated, so it is very easy
52// to compose different requests
53const authRequest = createTile({
54 type: ['user', 'authRequest'],
55 fn: ({ params, api, dispatch, actions }) => api.post('/login', params),
56});
57
58// actual business logic
59const authUser = createTile({
60 type: ['user', 'auth'],
61 fn: async ({ params, api, dispatch, actions, selectors, getState }) => {
62 // login user
63 await dispatch(actions.tiles.user.authRequest(params));
64
65 // check the result
66 const { data: { id }, error } = selectors.tiles.user.authRequest(getState());
67
68 if (error) {
69 throw new Error(error);
70 }
71
72 // set up synchronously user status
73 dispatch(actions.tiles.user.loginStatus({ true }));
74
75 return true;
76 },
77});
78```
79
80## Rationale
81
82There are enough projects around to keep your state management clean (for [example](https://github.com/erikras/ducks-modular-redux)), but they are mostly about organizing, rather than removing burden of repetitive stuff from the developer. Other packages offer you full-fledge integration with REST-API, [normalizing](https://github.com/paularmstrong/normalizr) your entities, building relations between models, etc. There is nothing like this here – in fact, if you need something like this, with the ability to query your local "database", I highly advise you to create your own solution, which will be custom-tailored to your specific problem.
83
84This package focuses on very basic blocks, which are good for pretty simple applications (e.g. login/logout, fetch client data, set up calculator values).
85
86## Integration API
87
88Despite being easy-to-use package to write new modules, you'd have to do some work to integrate it into your project. In a nutshell, you have to have a middleware which will handle returned functions from dispatched actions (one is provided in this package, but [redux-thunk](https://github.com/gaearon/redux-thunk) will suffice as well), and then you have to combine all modules to create actions & reducers.
89It is better to see in a small example:
90
91```javascript
92import { createTile, createEntities, createMiddleware } from 'redux-tiles';
93import { createStore, applyMiddleware } from 'redux';
94
95const clientDataTile = createTile({
96 type: ['client', 'data'],
97 fn: ({ api, params}) => api.get('/client/info'),
98});
99
100const tiles = [
101 clientDataTile,
102];
103
104const { actions, reducer, selectors } = createEntities(tiles);
105const { middleware } = createMiddleware({ actions, selectors });
106
107createStore(reducer, applyMiddleware(middleware));
108```
109
110## Tiles API
111
112Tiles are the heart of this library. They are intended to be very easy to use, compose and to test.
113There are two types of tiles – asynchronous and synchronous. Modern applications are very dynamic, so async ones will be likely used more often.
114
115```javascript
116import { createTile } from 'redux-tiles';
117
118const photos = createTile({
119 // they will be structured api.photos inside redux state,
120 // and also available under actions and selectors as:
121 // actions.tiles.api.photos
122 type: ['api', 'photos'],
123
124
125 // params is an object with which we dispatch the action
126 // you can pass only one parameter, so keep it as an object
127 // with different properties
128 //
129 // all other properties are from your middleware
130 // fn expects promise out of this function
131 fn: ({ params, api }) => api.get('/photos', params),
132
133
134 // to nest data:
135 // { 5:
136 // 10: {
137 // isPending: true,
138 // data: null,
139 // error: null,
140 // },
141 // },
142 // if you save under the same nesting array, data will be replaced
143 // other branches will be merged
144 nesting: (params) => [params.page, params.count],
145
146
147 // unless we will invoke with second parameter object with asyncForce: true,
148 // it won't be requested again
149 // dispatch(actions.tiles.api.photos(params, { asyncForce: true }))
150 caching: true,
151});
152```
153
154We also sometimes want to keep some sync info (e.g. list of notifications), or we want to store some numbers
155
156```javascript
157import { createSyncTile } from 'redux-tiles';
158
159const notifications = createSyncTile({
160 type: ['notifications'],
161
162
163 // all parameters are the same as in async tile
164 fn: ({ params, dispatch, actions }) => {
165 // we can dispatch async actions – but we can't wait
166 // for it inside sync tiles
167 dispatch(actions.tiles.user.dismissTerms());
168
169 return {
170 type: params.type,
171 data: processData(params.data),
172 };
173 },
174
175
176 // nesting works the same way
177 nesting: ({ type }) => [type],
178});
179```
180
181## Nesting
182
183Very often we have to separate some info, and with canonical redux we have to write something like this:
184```javascript
185case ACTION.SOME_CONSTANT:
186 return {
187 ...state,
188 [action.payload.id]: {
189 [action.payload.quantity]: {
190 ...state[action.payload.id],
191 ...action.payload.data,
192 },
193 },
194 };
195```
196
197Or with `Object.assign`, which will make it even less readable. This is a pretty common pattern, and also pretty error prone – so we have to cover such code with unit-tests, while in reality they don't do a lot of intrinsic logic – just merge. Of course, we can use something like `lodash.merge`, but it is not always suitable. In tiles we have `nesting` property, in which you can specify a function from which you can return an array of nested values. The same code as above:
198
199```javascript
200const infoTile = createTile({
201 type: ['info', 'storage'],
202
203 // params here and in nesting are the same object
204 fn: ({ params, api }) => api.get('/storage', params),
205
206 nesting: ({ quantity, id }) => [id, quantity],
207});
208```
209
210## Middleware
211
212In order to use this library, you have to apply middleware, which will handle functions returned from dispatched actions. Very basic one is provided by this package:
213
214```javascript
215import { createMiddleware } from 'redux-tiles';
216
217// these are not required, but adding them allows you
218// to do Dependency Injection pattern, so it is easier to test
219import actions from '../actions';
220import selectors from '../selectors';
221
222// it is a good idea to put API layer inside middleware, so
223// you can easily separate client and server, for instance
224import api from '../utils/api';
225
226
227// this object is optional. every property will be available inside
228// `fn` of all tiles
229// also, `waitTiles` is helpful for server-side-rendering
230const { middleware, waitTiles } = createMiddleware({ actions, selectors, api });
231applyMiddleware(middleware);
232```
233
234Also, [redux-thunk](https://github.com/gaearon/redux-thunk) is supported, but with it you can't provide your own properties. There is nothing bad to just import actions and selectors on top of the files, but then testing might require much more mocking, which can make your tests more brittle.
235
236## Server-side Rendering
237
238Redux-tiles support requests on the server side. In order to do that correctly, you are supposed to create actions for each request in Node.js. Redux-Tiles has caching for async requests (and keeps them inside middleware, so they are not shared between different user requests) – it keeps list of all active promises, so you might accidentaly share this part of the memory with other users!
239
240```javascript
241import { createMiddleware, createEntities } from 'redux-tiles';
242import { createStore, applyMiddleware } from 'redux';
243import tiles from '../../common/tiles';
244
245const { actions, reducer, selectors } = createEntities(tiles);
246const { middleware, waitTiles } = createMiddleware({ actions, selectors });
247const store = createStore(reducer, {}, applyMiddleware(middleware));
248
249// this is a futile render. It is needed only to kickstart requests
250// unfortunately, there is no way to avoid it
251renderApplication(req);
252
253// wait for all requests which were fired during the render
254await waitTiles();
255
256// this time you can safely render your application – all requests
257// which were in `componentWillMount` will be fullfilled
258// remember, `componentDidMount` is not fired on the server
259res.send(renderApplication(req));
260```
261
262There is also a package [delounce](https://github.com/Bloomca/delounce), from where you can get `limit` function, which will render the application if requests are taking too long.
263
264## Selectors
265
266All tiles provide selectors. After you've collected all tiles, invoke `createSelectors` function with possible change of default namespace, and after you can just use it based on the passed type:
267
268```javascript
269import { createTile, createSelectors } from 'redux-tiles';
270
271const tile = createTile({
272 type: ['user', 'auth'],
273 fn: ...,
274 nesting: ({ id }) => [id],
275});
276
277const tiles = [tile];
278
279const selectors = createSelectors(tiles);
280
281// second argument is params with which you dispatch action – it will get data
282// for corresponding nesting
283const { isPending, data, error } = selectors.user.auth(state, { id: '456' });
284```
285
286## Tests
287
288Almost all business logic will be contained in "complex" tiles, which don't do requests by themselves, rather dispatching other tiles, composing results from them. It is very important to pass all needed functions via middleware, so you can easily mock it without relying on other modules. All passed data is available in tiles via `reflect` property.
289
290```javascript
291import { createTile } from 'redux-tiles';
292
293const params = {
294 type: ['auth', 'token'],
295 fn: ({ api, params }) => api.post('/token', params),
296};
297const tile = createTile(params);
298
299// same object
300assert(tile.reflect === params); // true
301```
302
303## Contributing
304
305All suggestions or participating are welcome! Also, if you have any idea about improving API, or bringing some common functionality, don't hesitate, but please create an issue!
306
307## LICENSE
308
309MIT