diff --git a/README.md b/README.md index 65b8af6d8c..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) @@ -254,6 +255,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`: 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..d8375693bd --- /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] ) ); +} 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..50b8aa993b --- /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]; + } +} diff --git a/test/utils/arrayEqual.spec.js b/test/utils/arrayEqual.spec.js new file mode 100644 index 0000000000..6f3e81b63a --- /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); + }); + }); +}); diff --git a/test/utils/createBuffered.spec.js b/test/utils/createBuffered.spec.js new file mode 100644 index 0000000000..9d4d36de62 --- /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); + }) + }); +}); diff --git a/test/utils/createSelector.spec.js b/test/utils/createSelector.spec.js new file mode 100644 index 0000000000..ea4b668c30 --- /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); + }); + }); +});