-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Selector proposal #169
Selector proposal #169
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,3 +6,4 @@ lib | |
coverage | ||
react.js | ||
react-native.js | ||
.idea | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may have been discussed, or I may be missing something obvious here, but why separate creating selectors from memoization? |
||
``` | ||
|
||
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 | ||
<Connector select={state => ({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if select={['todos', todos => todos.length]} You could still import an array selector from somewhere else, and testing it would require you to call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gaearon something like this would require some sort of support in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Our decion was that a selector is a function which translates an input state into an output representation. The best solution would be to just combine the selectors as plain functions. let todoSelector = state => state.todos; The main drawback in this case is that you can only memoize on individual function parameters not on the parts of the state which are used in the selector itself. The reason that it was chosen to make memoization explicit was to make it a decision to the user wether she wants to have a material view on her state or not. |
||
todoCount: numberOfTodos(state) | ||
})}> | ||
{({ todoCount, dispatch }) => | ||
<div>{todoCount}</div> | ||
} | ||
</Connector> | ||
); | ||
} | ||
} | ||
``` | ||
|
||
### React Native | ||
|
||
To use Redux with React Native, just replace imports from `redux/react` with `redux/react-native`: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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] ) ); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @speedskater can you check your editor settings? It's best to end files w/ newlines. There are a few files in here that don't have them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New lines fixed. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}) | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For non-project specific things like this, you might consider a global gitignore: https://help.github.com/articles/ignoring-files/#create-a-global-gitignore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed