Selectors in polymer-redux

Update: Jump directly to headline “Option 4: Redux-reselect” if you are looking for the short answer how to use selectors with Redux. This articles explores various options on how to emulator Redux selectors. By now, we figured out that the best approach is actually none of the initial options. I therefore added this new option to the end of the article as “Option 4”.

Polymer-redux makes using Redux with Polymer as easy as pie (learn in this post why and how). Unfortunately, the library doesn’t allow to bind Polymer properties to Redux selectors. This post discusses approaches to simulate Redux selectors in polymer-redux.

Ideal solution

Before exploring alternative approaches, let’s first imagine how the ideal solution would look like. First we define the selectors, next to our Redux reducers. For example, we map all IDs of open todos (openIds) to their data as stored in the byId lookup map:

const getOpenTodos = (state) =>
state.openIds.map(id => byId[id]);

Next, we bind the selector (which operates on the state object todos) to the property openTodos in our Polymer element:

var ReduxBehavior = PolymerRedux(MyReduxStore);
Polymer({
is: "my-element",
properties: {
openTodos: {
type: Array,
selector: "getOpenTodos(todos)",
readOnly: true
},
},
behaviors: [
ReduxBehavior
]

We want the selector only to be executed if the underlying byId map or the openIds array changes in the Redux store. This is an important property because Polymer’s data binding gets triggered whenever the selector-based property changes. This would mean that an observer on the selector-based property would otherwise get triggered even if a completely unrelated part of the state changes.

That would be the ideal solution but — as often in the developer’s life — the ideal solution isn’t supported right now. So, let’s look into alternative approaches.

Option 1: Put selector into the Polymer element

The first approach is to model the selectors as closely as possible to the classic Redux/React approach. As we don’t want to recalculate the selector for every state change, we have Polymer monitor the underlying data fields for the selector, and then trigger a recalculation whenever required:

var ReduxBehavior = PolymerRedux(MyReduxStore);
Polymer({
is: "my-element",
properties: {
byId: {
type: Array,
statePath: "todos.byId",
readOnly: true
},
openIds: {
type: Object,
statePath: "todos.openIds",
readOnly: true
},
openTodos: {
type: Array,
computed: "getOpenTodos(byId, openIds)",
readOnly: true
},
},
getOpenTodos: function(byId, openIds) {
return openIds.map(id => byId[id]);
}

Pros:

  • Uses Polymer data binding to detect relevant state changes
  • Supports (optional) selector parameters

Cons:

  • Weak separation between Redux vs. Polymer world because Polymer needs to be aware of all underlying state fields to trigger the recalculation only when the data really changed.
  • Lots of boilerplate code
  • Hard to reuse the code across elements (at least we didn’t find a way to extract the gluing code into a Polymer behavior)

Option 2: Wrapper object

Another option is wrapping the state in a wrapper object, which exposes the selector as a getter:

TodosWrapper = function(data) {
Object.keys(data).forEach(key => {
this[key] = data[key];
});
};
TodosWrapper.prototype = {
get openTodos() {
openIds.map(id => byId[id]);
}
};
const todos = (state = {}, action) => {
return new TodosWrapper({
byId: byId(state.byId, action),
openIds: openIds(state.openIds, action)
});
};

Pros:

  • The selector logic is placed near to the reducer yet separated from it as advised by Dan.

Cons:

  • The wrapper object changes at each reducer execution. This will trigger Polymer’s data binding and thereby cause the execution of the openTodos getter, i.e. there will be many unnecessary calculations.
  • Violates the best practice of using simple objects in the Redux store (for easy serialization)

Option 3: Pre-calculate selector result

Currently, our preferred approach is pre-calculating the selector results in the reducers. We first calculate the underlying data for the selector (openIds and todos). We recalculate the openTodos only if the underlying data changed. Changes can be cheaply detected by checking for object equality because the state object is read-only.

const todos = (state = {}, action) => {
const newById = byId(state.byId, action);
const newOpenIds = openIds(state.openIds, action);
const dataChanged = state.byId !== newById || state.openIds !== newOpenIds;
// Combine state and wrap it into a object that provides the Redux selectors
return {
byId: newById,
openIds: newOpenIds,
openTodos: openTodos(state.openTodos, newById, newOpenIds, dataChanged, action)
};
};

const openTodos = (state = [], newById, newOpenIds, dataChanged, action) => {
// Only recalculate openTodos if the underlying data changed
if (!dataChanged) {
return state;
}

newOpenIds.map(id => newById[id]);
}

Pros:

  • Selector logic is placed near to the reducer
  • Selector calculation is only executed if the data really changed

Cons:

  • Reducer gets to know about selector logic
  • Works only for selectors without parameters
  • Unnecessary calculations are triggered in case no component shows the results

Option 4: Redux-Reselect (update)

Update: By now, we found out that the best solution is actually redux-reselect, a library that calculates the derived data on demand and cleverly caches the results (basically a clever version of Option 1).

Writing the selector is trivial:

const openTodosSelector = Reselect.createSelector(
state => state.todos.byId,
state => state.todos.openIds,
(byId, openIds) => openIds.map(id => byId[id])
};

And using the selector is even easier:

var ReduxBehavior = PolymerRedux(MyReduxStore);
Polymer({
is: "my-element",
properties: {
openTodos: {
type: Array,
statePath: openTodosSelector,
readOnly: true
},
},

Be careful with the immutable nature of the properties though. It might e.g. make your iron-list flicker.

Others, anyone?

Is there a better way of modeling selectors in polymer-redux? We’d love to hear about your best practices!

Happy coding!

Want to learn more about Polymer? Have a look to all our Medium posts on Polymer.


Photo: Ronny Roeller