Project Structure for Using Redux with Polymer 2.0

ES6 imports vs HTML imports ... fight!

Comments Posted by Captain Codeman in web-development on Jul 19, 2017

In the last post I showed how to use a pure Polymer / WebComponent approach for sharing state between components when they were separated in the DOM.

This works great for discreet isolated pieces of state but as you work with larger and more complex apps there are definite benefits to be had by using something like Redux to centralize things.

Redux of course comes from the React world, filled with JavaScript and ES6 imports - so how do you make it all play nicely with Polymer and Html Imports? Of course you could have your store, all the reducers, actions and so on as a set of JavaScript files and introduce a build step to generate the code to then include in your Polymer project but in my experience it’s complex and never feels quite right and you still have to figure out how to integrate the Redux parts with Polymer.

Polymer-Redux Library

I’m going to assume that you will use Christopher Turner’s tur-nr/polymer-redux which really does work great. As is often the case though, the examples show the basics, often all in one file - not necessarily how to structure things within a larger app.

We really don’t want to end up with a single huge my-redux-store.html file with all the actions, reducers, middleware, selectors and store contained within it.

The examples also place the actions within and as part of the elements that will dispatch them. While this is certainly an option that works, I’ve found it limiting as actions grow more complex (such as with shouldFetchPosts in the Advanced Async Actions example) and doesn’t quite lend itself to re-using actions by multiple elements.

The examples I’ve seen for selectors may also push people toward possibly including them within the elements or else setting the statePath to work directly against the store - both great for demos and fine for some things but can lead to issues later if you want to redesign the structure of your store without impacting the consumers of it or you just want to maximize re-use.

Setup Dependencies

Let’s create a Redux store for use with Polymer and make it easier to reuse things. I’m going to use ES6 but keep to browser-supported features and avoid things like the spread operator because I want to retain the lovely ability that Polymer has to just run the app directly in a browser via a local web-server with no build step.

You’ll first want to import the Redux package and some other essential Redux-related libraries:

npm install -s redux redux-thunk reselect

Yes, we’re using npm because that’s where they are. Although Polymer currently still uses Bower our elements can reference the node_modules scripts just fine and the polymer-cli will include them in any build we do for deployment.

Redux Reducer(s)

We’ll start with the reducer which really is the core piece of Redux. All our code is going to go into html files so it can be composed with Html Imports.

my-redux-reducers.html

<script src="../node_modules/redux/dist/redux.min.js"></script>

<script>
(function() {

function selectedSubreddit(state = 'PolymerJS', action) {
  switch (action.type) {
    case 'SELECT_SUBREDDIT':
      return action.subreddit
    default:
      return state
  }
}

function posts(
  state = {
    isFetching: false,
    didInvalidate: false,
    items: []
  },
  action
) {
  switch (action.type) {
    case 'INVALIDATE_SUBREDDIT':
      return Object.assign({}, state, {
        didInvalidate: true
      })
    case 'REQUEST_POSTS':
      return Object.assign({}, state, {
        isFetching: true,
        didInvalidate: false
      })
    case 'RECEIVE_POSTS':
      return Object.assign({}, state, {
        isFetching: false,
        didInvalidate: false,
        items: action.posts,
        lastUpdated: action.receivedAt
      })
    default:
      return state
  }
}

function postsBySubreddit(state = {}, action) {
  switch (action.type) {
    case 'INVALIDATE_SUBREDDIT':
    case 'RECEIVE_POSTS':
    case 'REQUEST_POSTS':
      return Object.assign({}, state, {
        [action.subreddit]: posts(state[action.subreddit], action)
      })
    default:
      return state
  }
}

MyApp.rootReducer = Redux.combineReducers({
  postsBySubreddit,
  selectedSubreddit
})

}());
</script>

Not too dissimilar from the Redux example, we’re just using regular <script> tags to load Redux instead of ES6 imports which is why we need to use the namespaced reference to Redux.combineReducers. We also don’t want to clutter the browser global object so we put everything in an Immediately Invoked Function Expression (IIFE) and set our rootReducer as a property of our existing app namespace object. For this example, I don’t see much value in having the action types defined elsewhere so I’m just using strings.

Redux Middleware

We’ll use a similar approach for middleware that we want to add. I’m going to use redux-thunk to add support for async actions and use the callAPIMiddleware example from the Reducing Boilerplate page.

my-redux-middleware.html

<script src="../node_modules/redux-thunk/dist/redux-thunk.min.js"></script>

<script>
(function() {

const callAPIMiddleware = ({ dispatch, getState }) => {
  return next => action => {
    const {
      types,
      callAPI,
      shouldCallAPI = () => true,
      payload = {}
    } = action

    if (!types) {
      // Normal action: pass it on
      return next(action)
    }

    if (
      !Array.isArray(types) ||
      types.length !== 3 ||
      !types.every(type => typeof type === 'string')
    ) {
      throw new Error('Expected an array of three string types.')
    }

    if (typeof callAPI !== 'function') {
      throw new Error('Expected callAPI to be a function.')
    }

    if (!shouldCallAPI(getState())) {
      return
    }

    const [requestType, successType, failureType] = types

    dispatch(
      Object.assign({}, payload, {
        type: requestType
      })
    )

    return callAPI().then(
      response =>
        dispatch(
          Object.assign({}, payload, {
            response,
            type: successType
          })
        ),
      error =>
        dispatch(
          Object.assign({}, payload, {
            error,
            type: failureType
          })
        )
    )
  }
}

MyApp.middleware = [
  ReduxThunk.default,
  callAPIMiddleware,
]

}());
</script>

Again, we make the middleware a property of our namespace object so we can reference it from elsewhere.

Redux Actions

The actions are either simple objects or else functions that the async dispatch can use that return simple objects so there are no dependencies to reference.

my-redux-actions.html

<script>
(function() {

function requestPosts(subreddit) {
  return {
    type: 'REQUEST_POSTS',
    subreddit
  }
}

function receivePosts(subreddit, json) {
  return {
    type: 'RECEIVE_POSTS',
    subreddit,
    posts: json.data.children.map(child => child.data),
    receivedAt: Date.now()
  }
}

function fetchPosts(subreddit) {
  return dispatch => {
    dispatch(requestPosts(subreddit))
    return fetch(`https://www.reddit.com/r/${subreddit}.json`)
      .then(response => response.json())
      .then(json => dispatch(receivePosts(subreddit, json)))
  }
}

function shouldFetchPosts(state, subreddit) {
  const posts = state.postsBySubreddit[subreddit]
  if (!posts) {
    return true
  } else if (posts.isFetching) {
    return false
  } else {
    return posts.didInvalidate
  }
}

function fetchPostsIfNeeded(subreddit) {
  // Note that the function also receives getState()
  // which lets you choose what to dispatch next.

  // This is useful for avoiding a network request if
  // a cached value is already available.

  return (dispatch, getState) => {
    if (shouldFetchPosts(getState(), subreddit)) {
      // Dispatch a thunk from thunk!
      return dispatch(fetchPosts(subreddit))
    } else {
      // Let the calling code know there's nothing to wait for.
      return Promise.resolve()
    }
  }
}

MyApp.actions = {
  fetchPosts,
  fetchPostsIfNeeded,
}

}());
</script>

As before, we hide everything within an IIFE and only expose the actions that need to be callable by our elements - some functions are just used by other actions which is what I found limiting about making them properties of an action object on individual elements.

You may have noticed that I’m not actually taking advantage of the middleware we defined earlier to do the fetch. the code examples on the Redux site don’t align and this is to show the concept, not produce a runnable app. I use a variation of the middleware in my own app and it works great for simplifying the REST API fetching and also handles adding auth tokens to the requests.

Redux Store

Finally, we have all the component pieces that we need to create our store - reducers, actions and middleware. Technically, the store isn’t dependent on the actions but this is where we’re also going to make them available to Polymer.

my-redux-store.html

<link rel="import" href="../bower_components/polymer/polymer-element.html">
<link rel="import" href="../bower_components/polymer-redux/polymer-redux.html">

<link rel="import" href="my-redux-actions.html">
<link rel="import" href="my-redux-middleware.html">
<link rel="import" href="my-redux-reducers.html">
<link rel="import" href="my-redux-selectors.html">

<script>
(function() {

  const composeEnhancers = typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
    ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
    : Redux.compose;

  const enhancer = composeEnhancers(
    Redux.applyMiddleware(...MyApp.middleware),
  );

  const store = Redux.createStore(MyApp.rootReducer, enhancer);

  const reduxMixin = PolymerRedux(store);

  /* @mixinFunction */
  const actionsMixin = (superClass) => {
    return class extends reduxMixin(superClass) {
      static get actions() {
        return MyApp.actions
      }
    }
  }

  /* @mixinFunction */
  MyApp.ReduxMixin = Polymer.dedupingMixin(actionsMixin);

}());
</script>

We also setup the middleware for the redux dev tools extension which is one of the great features of Redux to show you what’s happening within your store and allows you to perform ‘time travel’ or just view the state of your store and the actions being raised.

Once we have our redux store created we use PolymerRedux(store) to create the redux mixin. Most examples show using this with your elements but I like to add another mixin layer to make the actions available. Now there’s no need for any element to define it’s own set of actions.

We haven’t created the my-redux-selectors.html import in there yet. Even though we don’t use it directly within this file anything that imports this to use our mixin should be able to make use of what it defines so it’s the perfect place to add the import for it so save every element having to repeat it.

Redux Selectors

The redux reselect Selector library can help to abstract away the structure of your store so you can make changes to how you store and denormalize your data without needing to update your components whenever you do. It can also apply memoization for performance so unnecessary updates aren’t triggered.

my-redux-selectors.html

<script src="../node_modules/reselect/dist/reselect.js"></script>

<script>
(function() {

const getSelectedReddit = state => state.selectedReddit;
const getPostsByReddit = state => state.postsByReddit;

const posts = Reselect.createSelector(
  [getSelectedReddit, getPostsByReddit],
  (selectedReddit, postsByReddit) => {
    return postsByReddit[selectedReddit].items;
  }
);

MyApp.select = {
  posts,
}

}());
</script>

As with the other pieces, the IIFE allows us to keep internals isolated and private and we can just expose the pieces we want to via our namespace.

Consuming Element

To make any element in our app use Redux, we just need to import the store:

<link rel="import" href="my-redux-store.html">

Then apply the mixin and use the selectors we created (or store paths) in the statePath property attributes that Polymer Redux provides:

class MyView extends MyApp.ReduxMixin(Polymer.Element) {
//
  static get properties() {
    return {
      posts: {
        type: Array,
        statePath: MyApp.select.posts
      }
    }
  }
}

We can now <dom-repeat> over the posts property to list them and as the store changes for any reason - new posts are added or the selected subreddit changes, the element will be kept update. We can step through actions in the dev tools and see the result on screen.

If we need to send something to the store, we can use the this.dispatch('actionName', parameters) that Polymer Redux provides - it’s very important that we don’t do two-way binding to anything we get out of the store as this violates one of the fundamental principles that Redux is built on - immutability.

I hope you found this useful. I’ve experimented with a few different ways of using Redux with Polymer and this seems to be the one I’m most happy with but let me know if it can be improved in any way in the comments below.

polymer redux

Pinterest
Reddit
LinkedIn
comments powered by Disqus