From 5bc0d106a0562f99d0a104a7aa420b609065de04 Mon Sep 17 00:00:00 2001 From: Robert Binna Date: Tue, 23 Jun 2015 17:30:26 +0200 Subject: [PATCH 1/5] added proposal for selector pattern. --- .gitignore | 1 + src/index.js | 4 ++++ src/utils/arrayEqual.js | 3 +++ src/utils/createBuffered.js | 13 ++++++++++++ src/utils/createSelector.js | 20 +++++++++++++++++++ test/utils/arrayEqual.spec.js | 33 +++++++++++++++++++++++++++++++ test/utils/createBuffered.spec.js | 21 ++++++++++++++++++++ test/utils/createSelector.spec.js | 27 +++++++++++++++++++++++++ 8 files changed, 122 insertions(+) create mode 100644 src/utils/arrayEqual.js create mode 100644 src/utils/createBuffered.js create mode 100644 src/utils/createSelector.js create mode 100644 test/utils/arrayEqual.spec.js create mode 100644 test/utils/createBuffered.spec.js create mode 100644 test/utils/createSelector.spec.js diff --git a/.gitignore b/.gitignore index 37d08f874a..111bebfb2a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ lib coverage react.js react-native.js +.idea diff --git a/src/index.js b/src/index.js index 607b83f317..9328764f4d 100644 --- a/src/index.js +++ b/src/index.js @@ -6,10 +6,14 @@ import createDispatcher from './createDispatcher'; import composeMiddleware from './utils/composeMiddleware'; import composeStores from './utils/composeStores'; import bindActionCreators from './utils/bindActionCreators'; +import createSelector from './utils/createSelector.js'; +import createBuffered from './utils/createBuffered.js'; export { + createBuffered, createRedux, createDispatcher, + createSelector, composeMiddleware, composeStores, bindActionCreators diff --git a/src/utils/arrayEqual.js b/src/utils/arrayEqual.js new file mode 100644 index 0000000000..2934c4fcb1 --- /dev/null +++ b/src/utils/arrayEqual.js @@ -0,0 +1,3 @@ +export default function arrayEquals(array1, array2) { + return array1 == array2 || ( Array.isArray(array1) && Array.isArray(array2) && array1.length == array2.length && array1.every( (value, index) => value == array2[index] ) ); +} \ No newline at end of file diff --git a/src/utils/createBuffered.js b/src/utils/createBuffered.js new file mode 100644 index 0000000000..fe8796075b --- /dev/null +++ b/src/utils/createBuffered.js @@ -0,0 +1,13 @@ +import arrayEqual from '../utils/arrayEqual.js'; + +export default function createBuffered(fnToBuffer) { + let lastParams = null; + let lastResult = null; + return (...currentArguments) => { + if(!lastParams || !arrayEqual(currentArguments, lastParams)) { + lastResult = fnToBuffer(...currentArguments); + lastParams = currentArguments; + } + return lastResult; + } +} diff --git a/src/utils/createSelector.js b/src/utils/createSelector.js new file mode 100644 index 0000000000..bdd59141a3 --- /dev/null +++ b/src/utils/createSelector.js @@ -0,0 +1,20 @@ +import identity from '../utils/identity.js'; + +export default function createSelector(...selectors) { + if(selectors.length == 1) { + selectors.push( identity ); + } + let selector = selectors.pop(); + return state => { + let selectorParams = selectors.map((inputSelector) => toSelector( inputSelector )(state)); + return selector(...selectorParams); + } +} + +function toSelector( functionOrKey ) { + if( typeof functionOrKey == 'function' ) { + return functionOrKey; + } else { + return (state) => state[functionOrKey]; + } +} \ No newline at end of file diff --git a/test/utils/arrayEqual.spec.js b/test/utils/arrayEqual.spec.js new file mode 100644 index 0000000000..96866a13c1 --- /dev/null +++ b/test/utils/arrayEqual.spec.js @@ -0,0 +1,33 @@ +import expect from 'expect'; +import arrayEqual from '../../src/utils/arrayEqual'; + +describe('Utils', () => { + describe('arrayEqual', () => { + it('should test two identical arrays for equality', () => { + let array1 = [1,2,3]; + let array2 = [1,2,3]; + + expect( arrayEqual(array1, array1)).toBe(true); + expect( arrayEqual(array1, array2)).toBe(true); + expect( arrayEqual(array2, array1)).toBe(true); + }); + + it('should test two unidentical array for unequality', () => { + let array1 = [1,2,3]; + let array2 = [3,2,3]; + let array3 = [1,2,3,4]; + + expect( arrayEqual(array1, array2)).toBe(false); + expect( arrayEqual(array1, array3)).toBe(false); + expect( arrayEqual(array2, array3)).toBe(false); + }); + + it('should test type correctness of parameters', () => { + let array = [1,2]; + + expect(arrayEqual(array, 1)).toBe(false); + expect(arrayEqual(array, null)).toBe(false); + expect(arrayEqual(array, undefined)).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/test/utils/createBuffered.spec.js b/test/utils/createBuffered.spec.js new file mode 100644 index 0000000000..83f9e0770b --- /dev/null +++ b/test/utils/createBuffered.spec.js @@ -0,0 +1,21 @@ +import expect from 'expect'; +import { createBuffered } from '../../src'; + +describe('Utils', () => { + describe('createBuffered', () => { + it('should create a memoized function with cachesize=1', () => { + let counter = 0; + let bufferedFunction = createBuffered( (input) => { + ++counter; + return input * 2; + }); + + expect( bufferedFunction(1) ).toEqual(2); + expect( counter ).toEqual(1); + expect( bufferedFunction(1) ).toEqual(2); + expect( counter ).toEqual(1); + expect( bufferedFunction(2) ).toEqual(4); + expect( counter ).toEqual(2); + }) + }); +}); \ No newline at end of file diff --git a/test/utils/createSelector.spec.js b/test/utils/createSelector.spec.js new file mode 100644 index 0000000000..6d06a405dc --- /dev/null +++ b/test/utils/createSelector.spec.js @@ -0,0 +1,27 @@ +import expect from 'expect'; +import { createSelector } from '../../src'; + +describe('Utils', () => { + describe('createSelector', () => { + it('should create a simple string based key selector', () => { + let simpleKeySelector = createSelector('x'); + + expect(simpleKeySelector({x: 2})).toEqual(2); + }); + + it('should create a chained selector', () => { + let simpleSelector = createSelector('a'); + let chainedSelector = createSelector(simpleSelector, (a) => a.x) + + expect(chainedSelector({a: {x: 2}})).toEqual(2); + + }); + + it('should create a mixed chained selector', () => { + let simpleSelector = createSelector('a'); + let chainedSelector = createSelector(simpleSelector, 'b', (a, b) => a + b) + + expect(chainedSelector({a: 1, b: 1})).toEqual(2); + }); + }); +}); \ No newline at end of file From 691b5c92598dc9e3ae3228ab67868aaa72b1c1ac Mon Sep 17 00:00:00 2001 From: Robert Binna Date: Tue, 23 Jun 2015 17:53:19 +0200 Subject: [PATCH 2/5] added selector to readme --- README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/README.md b/README.md index 65b8af6d8c..2e6286620c 100644 --- a/README.md +++ b/README.md @@ -254,6 +254,46 @@ export default class CounterApp { } ``` +#### Selectors + +Selectors let you define views on your state. They enable you to define derived data on your store's state. +In combination with memoized functions the calculation overhead can be minmized. Hence, the child components of a Connector + using memoized selectors will only be rerendered if the source data of the selector changes. This can help to + prevent store dependencies. + +It is further recommended to define complex selectors in separate modules. + +e.g. defining selectors for a Todo Store +```js +import { createSelector, createBuffered } from 'redux'; + +export let todoSelector = createSelector('todos'); +export let numberOfTodos = createSelector(todoSelector, createBuffered(todos => todos.length)); +``` + +using the selector in your Component +```js +import React from 'react'; +import { Connector } from 'redux/react'; +//import your selectors +import { numberOfTodos } from 'selectors/TodoSelectors'; + +export default class TodoCount { + render() { + return ( + //use your predefined selectors + ({ + todoCount: numberOfTodos(state) + })}> + {({ todoCount, dispatch }) => +
{todoCount}
+ } +
+ ); + } +} +``` + ### React Native To use Redux with React Native, just replace imports from `redux/react` with `redux/react-native`: From 342f98f67a61dbefbee54907c8523fef32e56c43 Mon Sep 17 00:00:00 2001 From: Robert Binna Date: Tue, 23 Jun 2015 17:57:00 +0200 Subject: [PATCH 3/5] updated toc --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2e6286620c..c3e2b5254b 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Atomic Flux with hot reloading. - [Dumb Components](#dumb-components) - [Smart Components](#smart-components) - [Decorators](#decorators) + - [Selectors](#selectors) - [React Native](#react-native) - [Initializing Redux](#initializing-redux) - [Running the same code on client and server](#running-the-same-code-on-client-and-server) From 3f39346bd0ecb04e2f197e403e2f95b016233f6f Mon Sep 17 00:00:00 2001 From: Robert Binna Date: Tue, 23 Jun 2015 18:05:20 +0200 Subject: [PATCH 4/5] removed non project-specific git ignore entry --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 111bebfb2a..769308b566 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,4 @@ dist lib coverage react.js -react-native.js -.idea +react-native.js \ No newline at end of file From f42dcab5ec7d815eb7fde4715e4ff387f0887425 Mon Sep 17 00:00:00 2001 From: Robert Binna Date: Tue, 23 Jun 2015 18:15:22 +0200 Subject: [PATCH 5/5] fixed new lines --- .gitignore | 2 +- src/utils/arrayEqual.js | 2 +- src/utils/createSelector.js | 2 +- test/utils/arrayEqual.spec.js | 2 +- test/utils/createBuffered.spec.js | 2 +- test/utils/createSelector.spec.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 769308b566..37d08f874a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ dist lib coverage react.js -react-native.js \ No newline at end of file +react-native.js diff --git a/src/utils/arrayEqual.js b/src/utils/arrayEqual.js index 2934c4fcb1..d8375693bd 100644 --- a/src/utils/arrayEqual.js +++ b/src/utils/arrayEqual.js @@ -1,3 +1,3 @@ export default function arrayEquals(array1, array2) { return array1 == array2 || ( Array.isArray(array1) && Array.isArray(array2) && array1.length == array2.length && array1.every( (value, index) => value == array2[index] ) ); -} \ No newline at end of file +} diff --git a/src/utils/createSelector.js b/src/utils/createSelector.js index bdd59141a3..50b8aa993b 100644 --- a/src/utils/createSelector.js +++ b/src/utils/createSelector.js @@ -17,4 +17,4 @@ function toSelector( functionOrKey ) { } else { return (state) => state[functionOrKey]; } -} \ No newline at end of file +} diff --git a/test/utils/arrayEqual.spec.js b/test/utils/arrayEqual.spec.js index 96866a13c1..6f3e81b63a 100644 --- a/test/utils/arrayEqual.spec.js +++ b/test/utils/arrayEqual.spec.js @@ -30,4 +30,4 @@ describe('Utils', () => { expect(arrayEqual(array, undefined)).toBe(false); }); }); -}); \ No newline at end of file +}); diff --git a/test/utils/createBuffered.spec.js b/test/utils/createBuffered.spec.js index 83f9e0770b..9d4d36de62 100644 --- a/test/utils/createBuffered.spec.js +++ b/test/utils/createBuffered.spec.js @@ -18,4 +18,4 @@ describe('Utils', () => { expect( counter ).toEqual(2); }) }); -}); \ No newline at end of file +}); diff --git a/test/utils/createSelector.spec.js b/test/utils/createSelector.spec.js index 6d06a405dc..ea4b668c30 100644 --- a/test/utils/createSelector.spec.js +++ b/test/utils/createSelector.spec.js @@ -24,4 +24,4 @@ describe('Utils', () => { expect(chainedSelector({a: 1, b: 1})).toEqual(2); }); }); -}); \ No newline at end of file +});