Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 34 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save kevinpschaaf/995c9d1fd0f58fe021b174c4238b38c3 to your computer and use it in GitHub Desktop.
Save kevinpschaaf/995c9d1fd0f58fe021b174c4238b38c3 to your computer and use it in GitHub Desktop.
Custom Elements + Redux toolbox & examples

An approach to binding Redux to custom elements

The code here captures some of the patterns I used in the "real estate" demo app discussed in my talk End to End Apps with Polymer from Polymer Summit 2017.

There are many ways to connect Redux to custom elements, and this demonstrates just one pattern. The most important aspects are to try and lazily-load as much of the otherwise global state management logic along with the components that need them (as shown via the lazyReducerEnhancer and addReducers calls in the connected components), and to consider the tradeoffs you make in terms of coupling components to the store.

The pattern shown here of creating a stateless component and then a subclass that connects it to the store addresses a potential desire to reuse app-level stateless components between more than one application context, so the subclass provides a degree of decoupling from the concrete store, at the expense of more boilerplate. If app component portability is not a constraint you need, you could just connect components directly and avoid the subclass & potentially unnecessary added event dispatching overhead. The stateless/connected pattern also allows the stateless component to be connected to alternative state management systems as well.

Likewise, there are pros/cons to having many "connected" components, vs. e.g. just the top-level app component having the binding to the store. In the former case (many connected components), you avoid the need to bind properties down the tree as elements can pull their property values directly out of the store, but at the cost of more components tightly-coupled to the store and loss of tree context that you gain with the latter approach (one connected component, with all other components receiving properties via data binding).

It's difficult to declare a "one-size-fits-all" approach, so I'd recommend trying to understand the approaches and tradeoffs and make a good judgement for your application scenario. Once the full polymer-re app is cleaned up enough to be a good "demo app" that folks can copy/paste from, I do plan to open source it, and we may roll some of these patterns into future application templates as well.

Note: These examples are mildly pseudo-codey and assume an ES module context and some ES Next features (object spread) -- the main point is to convey the concepts. The concepts still apply e.g. using HTML imports and loading Redux using a UMD build via a global.

// This Redux "enhancer" adds a basic `store.addReducers()` method to the
// store, so that top-level slice reducers can be lazily added to the store,
// in the PRPL pattern spirit of "don't load anything the user didn't ask for"
import {combineReducers} from './path/to/redux.js';
export function lazyReducerEnhancer(nextCreator) {
return (origReducer, preloadedState) => {
let lazyReducers = {};
const nextStore = nextCreator(origReducer, preloadedState);
return {
...nextStore,
addReducers(newReducers) {
this.replaceReducer(combineReducers(lazyReducers = {
...lazyReducers,
...newReducers
}));
}
}
}
}
// Example of creating an initial store with no reducers, which can be
// lazily extended with reducers along with components that need them
import {createStore, compose as origCompose, applyMiddleware} from './path/to/redux.js';
import {lazyReducerEnhancer} from './lazy-reducer-enhancer.js';
// Use devtools if installed (https://github.com/zalmoxisus/redux-devtools-extension)
const compose = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || origCompose;
// Initial no-op reducer; all reducers will be lazily added using `addStore`
const reducer = (state, action) => { return {}; };
// Create basic store which can be extended with `addStore`
const store = createStore(reducer, {},
compose(lazyReducerEnhancer, applyMiddleware(/* ...any other middleware... */)));
// Example of a "stateless" component that accepts properties & fires events
// without mutating data locally, so that all state mutations can be handled
// by redux
import {PolymerElement} from './node_modules/@polymer/polymer/polymer-element.js';
export class MyElement extends Polymer.Element {
static get is() { return 'my-element'; }
static get properties() { return { counter: Number }; }
static get template() {
return `
<div>[[counter]]</div>
<button on-click="handleIncrement">Increment</button>
`;
}
handleIncrement() {
this.dispatchEvent(new CustomEvent('increment-counter', {bubbles: true, composed: true}));
}
}
// Could register it as a generic element, or not register it and
// let the "connected" subclass use its name... lots of ways to go
// Example of a Redux-"connected" element (meaning it subscribes to the store and sets
// properties, and dispatches actions based on DOM events) using the raw Redux API's
import {store} from './example-store.js';
import {MyElement} from './example-stateless-element.js';
import {counter} from './counter-reducer.js'; // example, not shown
// Lazily add reducer logic
store.addReducers({counter});
// Subclass and subscribe to store and set properties + dispatch actions based on events
class ConnectedMyElement extends MyElement {
constructor() {
super();
store.subscribe(state => {
this.setProperties({
counter: state.counter // would probably use selectors or e.g. reselect here
});
});
this.addEventListener('increment-counter', e => {
dispatch({type: 'INCREMENT_COUNTER'); // would probably use action creators here
});
}
}
customElements.define(ConnectedMyElement.is, ConnectedMyElement);
// This is a mixin that can be applied to custom elements, that codifies a simple
// pattern of connecting stateless elements to the redux store by mapping store
// state to element properties, and by mapping DOM events to store dispatch calls
// (inspired by concepts in https://github.com/reactjs/react-redux)
export const connect = (store, superClass) => {
return class extends superClass {
constructor() {
super();
// Map dispatch to events
if (this._mapDispatchToEvents) {
const eventMap = this._mapDispatchToEvents(store.dispatch);
for (let type in eventMap) {
this.addEventListener(type, event => {
event.stopImmediatePropagation();
eventMap[type](event);
});
}
}
// Map state to props
if (this._mapStateToProps) {
const setProps = this.setProperties ?
props => this.setProperties(props) :
props => Object.assign(this, props);
const update = () => setProps(this._mapStateToProps(store.getState()));
// Sync with store
store.subscribe(update);
update();
}
}
}
}
// Example of a Redux-"connected" element, using the `connect` mixin from above
// (alternative to example 4. above)
import {connect} from './connect-element-mixin.js';
import {store} from './example-store.js';
import {MyElement} from './example-stateless-element.js';
import {counterReducer} from './counter-reducer.js'; // example, not shown
// Lazily add reducer logic
store.addReducers({counterReducer});
// Subclass and implement `connect` callbacks
class ConnectedMyElement extends connect(store, MyElement) {
_mapStateToProps(state) {
return {
counter: state.counter // would probably use selectors or e.g. reselect here
};
}
_mapDispatchToEvents(dispatch) {
return {
'increment-counter'(e) {
dispatch({type: 'INCREMENT_COUNTER'); // would probably use action creators here
}
};
}
}
customElements.define(ConnectedMyElement.is, ConnectedMyElement);
@alex-ander-lim
Copy link

Where can we get the source of the End to End Apps with Polymer from Polymer Summit 2017, please?

@jparish3
Copy link

A demo-app would within this architecture would be extremely beneficial in providing some much needed perspective. The concept of end to end apps was very compelling, but unclear as to the viability with the 3.0 and lit-html transitions. hopefully apps like the real-estate demo remain viable in some form

@CaptainCodeman
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment