├── .babelrc
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── examples
├── blog
│ ├── package.json
│ ├── public
│ │ └── index.html
│ ├── src
│ │ ├── actions
│ │ │ └── index.js
│ │ ├── containers
│ │ │ ├── ArticleList.js
│ │ │ └── ArticlePage.js
│ │ ├── db.json
│ │ ├── index.js
│ │ ├── reducers
│ │ │ └── index.js
│ │ └── selectors
│ │ │ └── index.js
│ └── yarn.lock
└── todos
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── public
│ └── index.html
│ ├── src
│ ├── actions
│ │ └── index.js
│ ├── components
│ │ ├── App.js
│ │ ├── Footer.js
│ │ ├── Link.js
│ │ ├── Todo.js
│ │ └── TodoList.js
│ ├── containers
│ │ ├── AddTodo.js
│ │ ├── FilterLink.js
│ │ └── VisibleTodoList.js
│ ├── index.js
│ ├── reducers
│ │ ├── index.js
│ │ └── visibilityFilter.js
│ └── selectors
│ │ └── index.js
│ └── yarn.lock
├── package.json
├── src
├── __mocks__
│ └── index.js
├── __tests__
│ ├── getActionCreators.spec.jest.js
│ ├── getSelectors.spec.jest.js
│ ├── itemsByListNameSelectorFactory.js
│ ├── listSelectorFactory.spec.jest.js
│ ├── mapSelector.spec.jest.js
│ ├── reducer.spec.jest.js
│ └── selectorsFactory.spec.jest.js
├── actionTypeHelpers.js
├── getActionCreators.js
├── getSelectors.js
├── index.js
├── reducer.js
└── selectorsFactory.js
├── webpack.config.babel.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["latest"],
3 | "plugins": ["transform-object-rest-spread"]
4 | }
5 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/node_modules/**
2 | **/dist/**
3 | **/es/**
4 | **/lib/**
5 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["standard", "standard-react"],
3 | "env": {
4 | "browser": true
5 | },
6 | "parserOptions": {
7 | "ecmaVersion": 8
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.log
3 | node_modules
4 | dist
5 | lib
6 | es
7 | coverage
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Mirakl
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # STATUS : DEPRECATED AND UNMAINTAINED
2 |
3 | We recommend you to use another lib like [react-query](https://github.com/tannerlinsley/react-query), which also do client-side
4 | caching, without using redux, in a better way.
5 |
6 |
7 | #
8 |
9 | Redux lists middleware is a tool to manage the models (like User, Article, Product for instance) in your application in an optimized and simple way.
10 |
11 | ## Installation
12 |
13 | `npm install --save redux-lists`
14 |
15 | The redux store should know how to handle actions coming from the redux-lists action creators. To enable this, we need to pass the redux-lists `reducer` to your store:
16 |
17 | ```javascript
18 | import { createStore, combineReducers } from 'redux';
19 | import { reducer as reduxListsReducer } from 'redux-lists';
20 |
21 | const rootReducer = combineReducers({
22 | // ...your other reducers here
23 | reduxLists: reduxListsReducer
24 | });
25 |
26 | const store = createStore(rootReducer);
27 | ```
28 |
29 | ## Table of contents
30 |
31 | * [Motivations](#motivations)
32 | * [Concept in draw](#concept-in-draw)
33 | * [Case - a blog app](#case---a-blog-app)
34 | + [Redux-lists in action](#redux-lists-in-action)
35 | + [Explanation](#explanation)
36 | - [Files description](#files-description)
37 | - [articleActions and articleSelectors](#articleactions-and-articleselectors)
38 | - [ArticleList workflow](#articlelist-workflow)
39 | - [ArticlePage workflow](#articlepage-workflow)
40 | + [Redux state-tree sample](#redux-state-tree-sample)
41 | - [Redux-lists state-tree evolution](#redux-lists-state-tree-evolution)
42 | * [API](#api)
43 | + [`getActionCreators(namespace, options)`](#getactioncreatorsnamespace-options)
44 | + [`getSelectors(namespace)`](#getselectorsnamespace)
45 |
46 |
47 | ## Motivations
48 |
49 | Redux-lists is useful to:
50 |
51 | - Factorize in a single place your models objects
52 | - Do optimistic updates and improve your app responsiveness very easily
53 | - Reduce the redux boilerplate: generates action creators + selectors to manage your model collections
54 | - Improve code consistency
55 |
56 | ## Concept in draw
57 |
58 | 
59 | 
60 | 
61 |
62 | ## Case - a blog app
63 |
64 | In a blogging application, we probably will have... Articles ! Using redux, we will want to do the following operations in our app:
65 |
66 | - Render a list of blog articles, with only their title and eventually a quick description
67 | - Render a single blog article, with all it's information
68 |
69 | If you are used to redux, you probably already are thinking about how you are going to store those objects in the state-tree, the action creators and the selectors that you will have to make to get that data from the tree.
70 |
71 | ### Redux-lists in action
72 |
73 | That's where redux-lists is useful, because it provides you those tools and even more!
74 |
75 | For instance, in this case we want to manage a collection of articles. Redux-lists gives you action creators and selectors to manage those articles in your application.
76 |
77 | Firstly, we need to generate those tools by giving redux-list the namespace you want them to operate in:
78 |
79 | *articleActions.js*
80 | ```javascript
81 | import { getActionCreators } from 'redux-lists';
82 |
83 | export const { setList: setArticleList, updateItems: updateArticles } = getActionCreators('ARTICLES');
84 | ```
85 |
86 | *articleSelectors.js*
87 | ```javascript
88 | import { getSelectors } from 'redux-lists';
89 |
90 | export const { listSelector: articlesListSelector, byKeySelector: articleByIdSelector } = getSelectors('ARTICLES');
91 | ```
92 |
93 | Now, we want to write our ArticleList component. Here is the mechanism that we want to build here:
94 |
95 | 
96 | 
97 |
98 | *ArticleList.js*
99 | ```javascript
100 | import React from 'react';
101 | import { bindActionCreators } from 'redux';
102 | import { connect } from 'react-redux';
103 |
104 | import { setArticleList } from './articleActions.js';
105 | import { articlesListSelector } from './articleSelectors.js';
106 |
107 | class ArticleList extends React.Component {
108 | componentDidMount() {
109 | fetch('/articles')
110 | .then(response => response.json())
111 | .then(articles => {
112 | this.props.setArticleList(articles, 'ALL');
113 | });
114 | }
115 |
116 | render() {
117 | const { articles } = this.props;
118 | if (!articles) return 'Loading';
119 | if (articles.length === 0) return 'No articles yet !';
120 |
121 | return (
122 |
130 | )
131 | }
132 | }
133 |
134 | function mapStateToProps(state) {
135 | return {
136 | articles: articlesListSelector(state, 'ALL')
137 | }
138 | }
139 |
140 | function mapDispatchToProps(dispatch) {
141 | return {
142 | setArticleList: bindActionCreators(setArticleList, dispatch)
143 | }
144 | }
145 |
146 | export default connect(mapStateToProps, mapDispatchToProps)(ArticleList);
147 | ```
148 |
149 | Now let's consider we clicked on an article item in the list because we want to access this article page.
150 |
151 | Here is a possible workflow for the ArticlePage component:
152 |
153 | 
154 |
155 | > **Note :** We just performed an optimistic update ! Keep in mind that this behaviour is not mandatory to use with redux-list, but if you want to use such a mechanism redux-list makes it easy for you.
156 |
157 | 
158 | 
159 |
160 | *ArticlePage.js*
161 | ```javascript
162 | import React from 'react';
163 | import { bindActionCreators } from 'redux';
164 | import { connect } from 'react-redux';
165 |
166 | import { updateArticles } from './articleActions.js';
167 | import { articleByIdSelector } from './articleSelectors.js';
168 |
169 | class ArticlePage extends React.Component {
170 | componentDidMount() {
171 | fetch(`/articles/${this.props.id}`)
172 | .then(response => response.json())
173 | .then(article => {
174 | this.props.updateArticles(article);
175 | });
176 | }
177 |
178 | render() {
179 | const { article } = this.props;
180 | if (!article) return 'Loading';
181 |
182 | return (
183 |
184 |
{article.label}
185 |
{article.author}
186 |
{article.createdAt}
187 |
188 |
{article.content}
189 |
190 | )
191 | }
192 | }
193 |
194 | function mapStateToProps(state, ownProps) {
195 | const articleId = ownProps.id;
196 | return {
197 | article: articleByIdSelector(state, articleId)
198 | }
199 | }
200 |
201 | function mapDispatchToProps(dispatch) {
202 | return {
203 | updateArticles: bindActionCreators(updateArticles, dispatch)
204 | }
205 | }
206 |
207 | export default connect(mapStateToProps, mapDispatchToProps)(ArticlePage);
208 | ```
209 |
210 | ### Detailed explanations
211 |
212 | Let's take some time here to process and understand what is happening here.
213 |
214 | #### Files description
215 |
216 | - **articleActions.js**
217 |
218 | If you are familiar to redux, you know what this is. If you don't, you should learn how [redux](https://redux.js.org/) works first.
219 |
220 | - **articleSelectors.js**
221 |
222 | Contains functions (*memoized*) that go through the redux state-tree to get information in it.
223 |
224 | - **ArticleList.js**
225 |
226 | A react component that fetches and renders a list of articles.
227 |
228 | - **ArticlePage.js**
229 |
230 | A react component that fetches and renders a blog article page.
231 |
232 | #### articleActions and articleSelectors
233 |
234 | In `articleActions`, we create the `setArticleList` and `updateArticles` *redux-lists* actions.
235 |
236 | `getActionCreators` takes two parameters, the first being the **namespace** and the second being an options object.
237 |
238 | Indeed, the model's objects (here the articles) are going to be stored in the state tree `@@redux-lists/namespace` (here `@@redux-lists/ARTICLES`).
239 |
240 | Because of this, we also need to create the redux-lists articleSelectors in giving it the **namespace** they will have to look in: `getSelectors('ARTICLES')`.
241 |
242 | #### ArticleList workflow
243 |
244 | When the `ArticleList` component is mounted, an AJAX request is performed to get the `articles` from the server.
245 |
246 | When this request is fulfilled, we store those articles in a **list** called `'ALL'` in dispatching `setArticleList` action.
247 |
248 | > Note: We will see how this list is structured in the redux state-tree right after, just keep in mind that we store the articles in a named list for now.
249 |
250 | The state-tree gets updated, the `mapStateToProps` function is called. We use the `articlesListSelector` to get the articles we stored in redux. `articlesListSelector` takes the redux `state` and the `listName` we stored the articles in.
251 |
252 | The articles are passed to our `ArticleList` and they are rendered in the UI.
253 |
254 | #### ArticlePage workflow
255 |
256 | Firstly, let's consider that the user has previously rendered the previous `ArticleList` to access to the `ArticlePage`.
257 |
258 | When the `ArticlePage` component is getting mounted, `mapStateToProps` function is called. Since we already have some information about the article in the redux store (label, quick description), the `articleByIdSelector` finds them.
259 |
260 | It means that the ArticlePage can render instantly and prepare the user for the rest of the information to be fetched if you want to.
261 |
262 | When `ArticlePage` is mounted, an AJAX request is performed to get the `article` full information (it's content for instance).
263 |
264 | When this request is fulfilled, we update the article already stored in redux in dispatching the `updateArticles` action.
265 |
266 | The state-tree gets updated, the `mapStateToProps` function is called. Our `articleByIdSelector` gets our article with the new information we just fetched and `ArticlePage` re-renders.
267 |
268 | ### Redux state-tree sample
269 |
270 | To get a better grasp on what's happens with your objects when you set a redux-lists or when you make an update, let's see a sample redux-lists tree that could be produced in an application blog:
271 |
272 | ```json
273 | {
274 | "@@redux-lists": {
275 | "ARTICLES": {
276 | "list": {
277 | "ALL": ["article1", "article2", "article3"],
278 | "NEW": ["article3"],
279 | "AUTHOR=MANU": ["article1", "article2"],
280 | "AUTHOR=TOTO": ["article3"]
281 | },
282 | "map": {
283 | "article1": {
284 | "id": "article1",
285 | "label": "My first article !",
286 | "description": "This is my first article",
287 | "author": "MANU"
288 | },
289 | "article2": {
290 | "id": "article2",
291 | "label": "My second article !",
292 | "description": "This is my second article",
293 | "content": "Okay, this is my first article and it's about redux-lists!",
294 | "author": "MANU"
295 | },
296 | "article3": {
297 | "id": "article3",
298 | "label": "The third article",
299 | "description": "It's the third article of the blog but my first!",
300 | "author": "TOTO"
301 | }
302 | }
303 | }
304 | }
305 | }
306 | ```
307 |
308 | Redux-lists normalizes your array of objects into a map / list structure. This is pretty convenient because it avoids repetition of information, here the articles objects are in the map and their id used as a reference in the lists.
309 |
310 | #### Redux-lists state-tree evolution
311 |
312 | Let's consider this state-tree:
313 |
314 | ```json
315 | {
316 | "@@redux-lists": {
317 | "ARTICLES": {
318 | "list": {},
319 | "map": {}
320 | }
321 | }
322 | }
323 | ```
324 |
325 | And let's consider that `fetch('/articles')` returned those `articles`:
326 |
327 | ```json
328 | [
329 | {
330 | "id": "article1",
331 | "label": "My first article !",
332 | "description": "This is my first article",
333 | "author": "MANU"
334 | },
335 | {
336 | "id": "article2",
337 | "label": "My second article !",
338 | "description": "This is my second article",
339 | "author": "MANU"
340 | },
341 | {
342 | "id": "article3",
343 | "label": "The third article",
344 | "description": "It's the third article of the blog but my first!",
345 | "author": "TOTO"
346 | }
347 | ]
348 | ```
349 |
350 | When we called `setArticleList` in our `ArticleList` component
351 |
352 | ```javascript
353 | this.props.setArticleList(articles, 'ALL');
354 | ```
355 |
356 | here is what happened to redux-lists state tree:
357 |
358 | ```json
359 | {
360 | "@@redux-lists": {
361 | "ARTICLES": {
362 | "list": {
363 | "ALL": ["article1", "article2", "article3"]
364 | },
365 | "map": {
366 | "article1": {
367 | "id": "article1",
368 | "label": "My first article !",
369 | "description": "This is my first article",
370 | "author": "MANU"
371 | },
372 | "article2": {
373 | "id": "article2",
374 | "label": "My second article !",
375 | "description": "This is my second article",
376 | "author": "MANU"
377 | },
378 | "article3": {
379 | "id": "article3",
380 | "label": "The third article",
381 | "description": "It's the third article of the blog but my first!",
382 | "author": "TOTO"
383 | }
384 | }
385 | }
386 | }
387 | }
388 | ```
389 |
390 | ## API
391 |
392 | ### `getActionCreators(namespace, options)`
393 |
394 | - (*String*) `namespace`: A namespace for the map / lists of your model
395 | - (*Object*) `options`: supported options are:
396 | * (*String*, default = 'id') `onKey`: The key used as reference for objects manipulations
397 |
398 | `getActionCreators` returns an *Object* containing the keys:
399 |
400 | - (*Function*) `setList(items, listName)`: Adds items in the collection + places references in the list
401 | * (*Array of objects*) `items`: Model objects to be placed in the list. Object must at least have an unique identifier (`id` or the one defined by the above `onKey`)
402 | * (*String*) name of the list you want to place your objects in.
403 |
404 | - (*Function*) `updateItems(items)`: Upsert items in the collection
405 | * (*Object* or *Array of objects*) `items`: Places the objects into the redux-lists map
406 |
407 | ### `getSelectors(namespace)`
408 |
409 | - (*String*) `namespace`: A namespace for the map / lists of your model
410 |
411 | `getSelectors` returns an *Object* containing the keys:
412 |
413 | - (*Function*) `listSelector(state, listName)`: Returns an array of objects previously put in the list with this *listName*
414 | * (*Object*) `state`: the entire redux state-tree
415 | * (*String*) `listName`: the list you want to extract the objects from
416 |
417 | - (*Function*) `byKeySelector(state, itemKey)`: Returns the object that have the `itemKey` key value (defined with `getActionCreators`)
418 | * (*Object*) `state`: the entire redux state-tree
419 | * (*String*) `itemKey`: the itemKey value of the object you want to read on the redux-lists store.
420 |
421 | > **Note :** The selectors are *memoized* so that if something has been processed / accessed once, it won't compute it again if it's asked another time with the same parameters.
422 |
--------------------------------------------------------------------------------
/examples/blog/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blog",
3 | "version": "0.0.1",
4 | "private": true,
5 | "devDependencies": {
6 | "react-scripts": "^1.1.4"
7 | },
8 | "dependencies": {
9 | "concurrently": "^3.5.1",
10 | "json-server": "^0.12.2",
11 | "prop-types": "^15.6.1",
12 | "react": "^16.3.1",
13 | "react-dom": "^16.3.1",
14 | "react-redux": "^5.0.7",
15 | "react-router-dom": "^4.2.2",
16 | "redux": "^3.5.2",
17 | "redux-thunk": "^2.2.0",
18 | "redux-lists": "^1.0.0"
19 | },
20 | "scripts": {
21 | "start": "concurrently \"json-server --watch src/db.json --port 3004\" \"react-scripts start\"",
22 | "build": "react-scripts build",
23 | "eject": "react-scripts eject",
24 | "test": "react-scripts test"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/examples/blog/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Blog - Redux-lists
7 |
8 |
9 |
10 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/examples/blog/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import { getActionCreators } from 'redux-lists'
2 |
3 | export const {
4 | setList: setArticleList,
5 | updateItems: updateArticles
6 | } = getActionCreators('ARTICLES')
7 |
--------------------------------------------------------------------------------
/examples/blog/src/containers/ArticleList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { bindActionCreators } from 'redux'
4 | import { connect } from 'react-redux'
5 | import { setArticleList } from '../actions'
6 | import { articlesListSelector } from '../selectors'
7 | import { Link } from 'react-router-dom'
8 |
9 | class ArticleList extends React.Component {
10 | componentDidMount () {
11 | fetch('http://localhost:3004/articles')
12 | .then(response => response.json())
13 | .then(articles => {
14 | this.props.setArticleList(articles, 'ALL')
15 | })
16 | }
17 |
18 | render () {
19 | const { articles } = this.props
20 | if (!articles) return 'Loading'
21 | if (articles.length === 0) return 'No articles yet !'
22 |
23 | return (
24 |