Polymer bindings for Redux. Bind store state to properties and dispatch actions from within Polymer Elements.
Polymer is a modern library for creating Web Components within an application. Redux is a state container for managing predictable data. Binding the two libraries together allows developers to create powerful and complex applications faster and simpler. This approach allows the elements you build with Polymer to be more focused on functionality than the applications state.
npm install --save polymer-redux@next
If an older version of Polymer is required for a project install the standard package below and refer to the current documentation.
npm install --save polymer-redux
import { createMixin } from 'polymer-redux';
import { html, PolymerElement } from '@polymer/polymer/polymer-element';
import store from './redux/store';
// Create an instance of ReduxMixin
const ReduxMixin = createMixin(store);
// Create an element with the ReduxMixin
class AcmeHello extends ReduxMixin(PolymerElement) {
static get template() {
return html`
<h1>Hello, [[name]]!</h1>
`;
}
static get properties() {
return {
name: {
type: String,
readOnly: true
}
};
}
// Return element properties from the store's state
static mapStateToProps(state, element) {
return {
name: state.name
};
}
}
// Define the Element
customElements.define('acme-hello', AcmeHello);
<acme-hello />
is now connected to Redux and can sync element properties to the store's state.
Before choosing Redux as a projects state management solution. It is important to know the limitations it brings to Polymer, see Caveats.
Polymer uses class mixins to extend and reuse functionality across elements. Polymer Redux uses this techinque to connect any extending element to a Redux store.
Polymer Redux exports createMixin
factory. This factory function is used to create a Redux mixin for Polymer elements to extend.
import { createMixin } from 'polymer-redux';
import { createStore } from 'redux';
const store = createStore(state => state);
const ReduxMixin = createMixin(store);
If an application relies on multiple stores, simply create as many mixins.
const ReduxMixinOne = createMixin(storeOne);
const ReduxMixinTwo = createMixin(storeTwo);
const ReduxMixinX = createMixin(storeX);
Elements that extend the Redux mixin can now implement static mapStateToProps(state, element)
. This method provides the logic for syncing the store's state to properties.
class AcmeUser extends ReduxMixin(ParentElement) {
static get properties() {
return {
name: {
type: String,
readOnly: true
}
};
}
static mapStateToProps(state, element) {
return {
name: state.name
};
}
}
It's considered good practice to define properties that will be mapped to Redux as readOnly
. This helps stop any updates outside of Redux.
mapStateToProps
accepts two arguments, state
and element
. Using existing properties from the element with notify
enabled, elements can react from both Polymer and Redux state changes.
class AcmeUser extends ReduxMixin(ParentElement) {
static get properties() {
return {
userId: {
type: String,
notify: true
},
name: {
type: String,
readOnly: true
}
};
}
static mapStateToProps(state, element) {
return {
name: state.users[element.userId]
};
}
}
Use this feature with caution. Notifying properties that have Redux bindings associated with them can cause a double updates.
Using selector libraries like Reselect can help with performance when mapping state to an element.
import { createSelector } from 'reselect';
const currentUserId = state => state.session.userId;
const userObjects = state => state.data.users;
const getCurrentUser = createSelector(
[currentUserId, userObjects],
(id, users) => users[id]
);
class AcmeUser extends ReduxMixin(ParentElement) {
static mapStateToProps(state, element) {
return {
user: getCurrentUser(state)
};
}
}
Along with binding properties elements can also dispatch actions to the Redux store. Elements can implement static mapDispatchToEvents(dispatch, element)
which allows custom events to dispatch actions.
class AcmeTodo extends ReduxMixin(PolymerElement) {
static get template() {
return html`
<div>
<input type="checkbox" on-change="handleDone" checked$="[[done]]" />
<span>[[task]]</span>
</div>
`;
}
static get properties() {
return {
task: {
type: String,
readOnly: true
},
done: {
type: Boolean,
readOnly: true
}
};
}
static mapStateToProps(state) {
return {
task: state.task,
done: state.done
};
}
static mapDispatchToEvents(dispatch, element) {
return {
updateTodo: event => dispatch({
type: 'UPDATE_TODO',
done: event.detail
});
};
}
handleDone(event) {
this.dispatchEvent(
new CustomEvent('update-todo', {
detail: event.target.checked
})
);
}
}
In the element example above, the returning object from static mapDispatchToEvents
is used to add event listeners to the element. When the events are fired the corresponding action will be dispatched to the Redux store. This may seem like it's just retargeting an event but it introduces Connected elements.
The object keys will become the event name but in dash-case. So updateTodo
will listen for update-todo
events.
Polymer Redux exports a helper function that can bind the dispatch call to each event handler. Using the helper listeners now just need to return the action and Polymer Redux will handle the dispatching for that element.
import { bindActionCreators } from 'polymer-redux';
class AcmeTodo extends ReduxMixin(PolymerElement) {
static mapDispatchToEvents(dispatch, element) {
return bindActionCreators(
{
updateTodo: event => ({
type: 'UPDATE_TODO',
done: event.detail
})
},
dispatch
);
}
}
Elements that extend the Redux mixin inherit the dispatchAction
method. This method is a proxy for the Redux dispatch function and can be used just like in Redux.
class AcmeTodo extends ReduxMixin(PolymerElement) {
handleDone(event) {
this.dispatchAction({
type: 'UPDATE_TODO',
done: event.detail
});
}
}
So far all the examples have been hybrid elements, meaning that an application is heavily coupled to Polymer Redux. What if the elements are already built or elements need to be decoupled for resusability? This is where connected elements helps.
A connected element holds the binding logic needed for Polymer Redux and nothing more. There is no element logic. This approach lets elements become more reusable and even been bound to another state management library. Use the base element as the parent class for the Redux mixin rather than the standard PolymerElement
.
// ./elements/acme-todo.js
export default class AcmeTodo extends PolymerElement {
static get template() {
return html`
<div>
<input type="checkbox" on-change="handleDone" checked$="[[done]]" />
<span>[[task]]</span>
</div>
`;
}
static get properties() {
return {
task: {
type: String,
readOnly: true
},
done: {
type: Boolean,
readOnly: true
}
};
}
handleDone(event) {
this.dispatchEvent(
new CustomEvent('update-todo', {
detail: event.target.checked
})
);
}
}
// ./connected/acme-todo.js
import AcmeTodo from '../elements/acme-todo';
export default class ConnectedTodo extends PolymerRedux(AcmeTodo) {
static mapStateToProps(state) {
return {
task: state.task,
done: state.done
};
}
static mapDispatchToEvents(dispatch, element) {
return {
updateTodo: event => dispatch({
type: 'UPDATE_TODO',
done: event.detail
});
};
}
}
Now AcmeTodo
is not tied to Polymer Redux anymore, it's a standard Polymer element that can be used in any Polymer project. If the project is using Polymer Redux then using ConnectedTodo
will be the same element but with Redux bindings.
Observing array mutations and sub property updates does not work with Polymer Redux. Redux state is immutable by design, reducers must always return new state. This means that when mutating arrays or objects your reducer will look something similar to below.
const reducer = (state, action) => {
switch (action.type) {
case 'push':
return {
...state,
array: [...state.array, action.value]
};
case 'sub-prop':
return {
...state,
object: {
...state.object,
subProp: action.value
}
};
}
};
As you can see state.object
and state.array
are completely new instances. When using Polymer Redux to bind these properties it will set the new value to the Element's property. The property effects of Polymer is that of a new value and not a splice or sub property change.
For a working todo list using Polymer Redux check out the demos of this project.
npm run demo
Copyright (c) 2018 Christopher Turner