Skip to content
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

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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));
Copy link
Contributor

Choose a reason for hiding this comment

The 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 => ({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if select actually took selectors in the same format as nuclear-js getters, that is an array. So this would be:

select={['todos', todos => todos.length]}

You could still import an array selector from somewhere else, and testing it would require you to call createSelector, but it would allow the individual connector to worry about memoziation (not sure if this is better or not...)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gaearon something like this would require some sort of support in redux itself, which could just be something along the lines of connector/select middleware

Copy link
Author

Choose a reason for hiding this comment

The 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;
let todoCountSelector = state => todoSelector(state).length;

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.
To preserve functional semantics the construct using the createSelector was chosen in favor of passing arrays around.

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`:
Expand Down
4 changes: 4 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/utils/arrayEqual.js
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] ) );
}
13 changes: 13 additions & 0 deletions src/utils/createBuffered.js
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;
}
}
20 changes: 20 additions & 0 deletions src/utils/createSelector.js
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];
}
}
33 changes: 33 additions & 0 deletions test/utils/arrayEqual.spec.js
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);
});
});
});
21 changes: 21 additions & 0 deletions test/utils/createBuffered.spec.js
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);
})
});
});
27 changes: 27 additions & 0 deletions test/utils/createSelector.spec.js
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);
});
});
});