Redux bundles

I've introduced many individual patterns in previous chapters. They each stand on their own. But you may have noticed that so far, I've largely ignored file structure and app organization topics. In this chapter, I'm going to introduce the tools I've created for myself that apply these patterns for building applications with Redux. If you're averse to developers who even appear to be pitching their own tools and libraries you may want to stop reading here, or consider this chapter a free bonus.

However, even if you opt not to use the specific tools I introduce here, the idea of bundling your Redux code in a similar manner may be of value. I used various portions of these organizational methods before I formalized any of it into a library. I'd also like to point out that this is not theory; these are not hypotheticals. This is how I actually build apps for myself and my clients. Additionally, some of these ideas, although not the library itself, have been proven to work well in a large team environment at Starbucks.

Convincing you to use my particular library isn't the goal here. Anyway, enough disclaimers, let's get on with it. As I've applied the patterns described in previous chapters to various applications, I've eventually grown tired of re-implementing them. So, I decided to do something about it for myself. What came out of it was a library that I call Redux-bundler that I've been personally using for well over a year. As of this writing, I've built four applications with it. It's also what powers my online donation platform Speedy (https://speedy.gift).

The basic idea is a bit like what has been called the "redux-ducks" pattern where you organize Redux-related code around functionality instead of by types such as constants, action creators, selectors, reducers, etc. What I've done is essentially formalized the "ducks pattern" into something I call "bundles." By keeping related functionality in a single file and exporting an object with a particular structure, I can then pull all those bundles together to compose a store.

I've written a library that does this that I use for all my Redux applications called: redux-bundler. There's also a documentation site available: https://reduxbundler.com

To understand why this type of approach may be beneficial, first, let's discuss some of the challenges of using Redux as your application grows.

So much boilerplate!

One of the primary complaints you'll hear about Redux is the amount of boilerplate code required to do simple things. I'd like to point out that this is inherent with any low-level tool. Make no mistake, Redux is a low-level tool and as we've discussed it really doesn't do all that much.

But as a result even if you want to do something as seemingly simple as fetching some data, you may well write:

  1. A set of action types
  2. A reducer to store the resulting data
  3. An action creator that dispatches the appropriate action types
  4. A set of selectors to extract relevant data from each of these
  5. You need to import and add the reducer to your root reducer.
  6. You need to import selectors and action creators into any components that you want to connect()
  7. You need to import selectors anywhere you want to combine them with other selectors as we discussed earlier.

If you do what many folks do and split everything by functionality type, then you're adding stuff to perhaps 6 or 7 files just to add some simple functionality! For a large application, this can quickly lead to all manner of challenges:

  1. Difficulty when needing to refactor.
  2. Reusability of Redux-related functionality becomes difficult.
  3. Building selectors that depend on each other can easily cause circular import issues, especially when you're trying to derive higher-level "answers" from your state. This can force you to organize selectors in odd ways just to avoid these issues.
  4. Very little isolation if working on an application with a team. Since there are a lot of files to update any time you want to change anything, you're touching lots of files that may well be used by other parts of the app. This is not ideal for team environments where lots of folks are contributing because it increases the number of merge conflicts exponentially.
  5. The "import section" at the top of major components in your app can start to get a little ridiculous. If you're also using PropTypes for validation, selectors for data, then defining a select function, then importing and injecting all your relevant action creators, pretty soon you've got a ton of imports and code in those component files just to make sure you can get a component with all the right props.

Over time, as applications grow, these problems only get worse. Let's dive a bit deeper to see how we might address some of these challenges.

Easier Refactoring and More Flexibility

Change is the only constant. If your code is expected to survive beyond a weekend hackathon, it's likely going to have to endure changes in design, significant iterations on functionality, or even complete redesigns.

The prevailing trend seems to be that we should be organizing code using the concept of a "component." From my experience, this sounds great in theory but can be quite limiting when looking to implement significant changes.

For me, the concept of a component as a means of describing application behavior has never felt right. For example, the idea of using a custom element to perform an ajax request, or rendering a <Redirect/> tag to trigger a redirect baffles me. Some folks in the React community would counter that you should be splitting your app into "container components" and "view components." Where a container manages behavior such as data fetching and renders simpler, presentational child components that simply display stuff. Personally, I'm more in the camp of thinking "get your app out of my components!" Personally, I feel the idea of a behavioral component is like trying to fit a square peg into a round hole because:

  1. You've now coupled everything to the UI framework.
  2. You can't easily unit test your logic in node.js without also having to stub out a bunch of browser functionality, mess with "shallow rendering" of components, compiling JSX.
  3. You can't easily make significant changes to the structure of the UI without also breaking your application logic.

By contrast, thinking about behavior and state management outside of components allows you all manner of flexibility. For example, you actually could choose to run your store, reducers, and action creators entirely in a WebWorker. Or what if you decide to switch to a different UI library entirely? There are many alternatives to React that are a fraction of the file size. Personally, I use Preact for almost everything.

Perhaps most commonly, however, is that you decide to make significant changes to your application UI. Most application architectures simply do not allow you to do this without essentially starting from scratch. But when you have this level of separation between app functionality and the UI you can run the entire "app" in "headless" mode if you want. You don't even need to have built the UI yet, and you can still "run your app." It will trigger fetches, update URLs, etc. If you can do that, it's not hard to see how it would be much easier to make significant UI changes.

This isn't just theoretical. I did exactly this a few weeks ago. There is an application I've built that I'm running in production. The UI had a bit of a Designed By a Developer™️ feel to it. It was good enough to help me make a business case for the app, but I had no external design input when I built it. In fact, I didn't even have a logo. But, once I had real users using and enjoying the app despite its lack of visual polish, I was willing to spend money to have a designer help me create a better UI. It wasn't just a fresh "coat of paint" so to speak, it a completely different layout. Once I had the new design concepts, in a week and a half I completely re-built the UI and barely had to change any of the application code logic at all. This application was part of my donation platform, so it had to keep working because it was handling money.

Making huge changes like this just wouldn't have been possible in this time frame if I hadn't used an architecture with such a strict separation of concerns. But, when your app logic no longer lives in your components, it's easier to experiment with alternate UI approaches or completely change your UI without needing to break all your application logic.

This kind of adaptability is worth real money.

Re-usability of non-UI code

I think application development, in its ideal state, should feel like building Legos. Neatly encapsulated "bricks" of functionality composed into something beautiful. I don't think anyone would disagree, and most frameworks provide some mixture of existing "bricks" and a way to write your own.

But, the problem I seem to always run into when trying to create these re-usable pieces is that I want the functionality to be re-usable, not always the UI. In fact, I want to be able to re-use functionality irrespective of the UI technology I'm using to render things.

The problem of a re-usable date picker is solved. Grab your favorite React component, Web Component, Angular Component, Ember Component, jQuery plugin, etc.

But what I'm more interested in is how I can go about reusing behavior that isn't UI code.

  • What about bundling up code that knows how to initiate OAuth redirects and token retrieval against your auth system? Something like this involves reading values from the URL, triggering redirects, etc.
  • What about reusing logic that lets us retrieve, store, and observe the status of a user's geolocation as retrieved by navigator.geolocation.getCurrentPosition()?

These things are simply not components. No matter how much we pretend they are, representing behavior as "components" has always felt awkward to me. The typical answer to this problem is: "Write a generic JavaScript library."

But, who cares?! I don't need a library to call navigator.geolocation.getCurrentPosition() for me, what I need is a way to integrate that information usefully into my application logic without having to do a bunch of "wiring" or writing "glue code." I just want something I can drop in that gives my app the capability of knowing about, asking for, and observing changes to the user's geolocation!

I want to be able to add a geolocation thing that seamlessly adds these capabilities into my apps in a way that it is ready to consume. Plus, whatever that thing is, should be a tiny little piece of code that isn't tied to the UI at all!

In short, I want the Geolocation Lego Piece™.

What might such a piece look like?

Something to store the following data (a reducer):

  • successfully retrieved coordinates and data
  • a flag to indicate whether the request for geolocation was rejected by the user
  • metadata about when it was last requested, whether it was successful, etc.

A set of actions I can initiate (action creators):

  • doRequestGeolocation

A set of questions I can ask (selectors):

  • Did this get rejected permanently?
  • Do we know the user's coordinates
  • What are the user's coordinates
  • How old is the geolocation data we do have
  • Based on my "staleness tolerance" should we ask the user again?

Additionally, we may want some reactors:

  • If we have coordinates, the user's permission to get geolocation, and our data is a bit stale, we may wish to automatically dispatch doAskGeolocation again, right?

After trying to build these types of pieces a few different ways, I settled on something that feels like a good API for this kind of thing.

It looks like this:

// the reducer function
const reducer = () => { ... }

// the action creator for triggering things
const doRequestGeolocation = (dispatch) => {
  // assuming we've got a little promise-based geolocation lib
  dispatch({type: 'GEOLOCATION_START'})
  getGeolocation()
    .then(payload => {
      dispatch({type: 'GEOLOCATION_SUCCESS', payload})
    })
    .catch(error => {
      dispatch({type: 'GEOLOCATION_ERROR', error})
    })
}

// for reading and being able to "connect" this
// to components
const selectCoordinates = createSelector( ... )

// we export something of a bundle
// aka Lego Brick ;-)
export default {
  name: 'geolocation',
  reducer,
  doRequestGeolocation,
  selectCoordinates
}

If our application logic is contained as a set of these little bundles of functionality, we can now write a function that takes these bundles and returns a complete Redux store ready to go.

create-store.js

import { composeBundles } from 'redux-bundler'
import { geolocationBundle } from './bundles/geolocation'

export default composeBundles(geolocationBundle, otherBundle, yetAnother)

Getting all decorative!

In the previous example, the composeBundles function would be able to iterate through the bundles and attempt to extract things by convention. If you've worked with Redux, you can probably guess what such a composeBundles would do with the reducers. It would need to extract the reducer and name properties from each bundle and use Redux's combineReducers to lump them together. But it may not be obvious what it would do with selectors and such.

Let's think about what combineReducers does for just a minute. It essentially folds the reducer functionality into the resulting Redux store, right? The individual reducer functions become part of the store. Why not do the same with action creators and selectors? After all, these are arguably just as integral to building an application with Redux as the reducers!

If you think about it, much of the boilerplate in Redux comes from the fact that you have to import and bolt-on all these other things when writing components. Anytime we want to use an actionCreator it has to be imported and then "bound" to the store anyway. If we have our code organized into bundles, we can aggregate all of our action creators up front. We can bind them to the store and attach them to the store instance as methods! One way to do this is to make up a convention. I loop through all the bundles and look for keys in that start with do then I turn them into pre-bound methods on the store. So if any of the bundles have a key called doRequestGeolocation, we end up with a method on the store that we could call like this: store.doRequestGeolocation().

We can do the same with selectors. As long as our selectors are all written to expect the entire application state as an argument, we can create a method on the store for each one that calls store.getState() and passes it to our selector. Then we can call store.selectIsLoggedIn() as a method on the store and get the result based on the current state!

Using similar conventions, we can let our bundles define other things too. They could define reactors, middleware, an init method or anything else we can dream up. So, for example, if a bundle exports an object with a key that starts with react we could assume it's a reactor that should be monitored for potential actions to dispatch.

Henrik you're nuts, you'll make a mess of the store instance!

It does sound a bit messy, but with a bit of convention, it's quite manageable. Sure you'll have a bunch of methods on the store instance, but they'll either start with select or do which keeps things quite tidy. Plus, as it turns out, this makes a lot of other things way less messy.

For example, a connected component when using plain Redux sometimes starts looking like this:

import {
  someSelector,
  someOtherSelector,
  evenMoreSelect,
  dataAllTheThingsExclamationPointSelector
  // ...
  // ...
  // ...
} from '../../selectors'
import {
  doSomething,
  doSomethingElse
} from '../../actions'

// the actual component
const MyComponent = ({some, someOther, evenMore, data, doSomething, doSomethingElse}) => (
  <div>

  </div>
)

const select = state => {
  some: someSelector(state),
  someOther: someOtherSelector(state),
  evenMore: evenMoreSelect(state),
  data: dataAllTheThingsExclamationPointSelector(state)
  // ...
  // ...
  // ...
}

export default connect(select, {doSomething, doSomethingElse})(MyComponent)

But, remember how <Provider> already has put our Redux store into context, right?

If that store already has all our selectors and action creators attached, we can write a smarter connect() function that just grabs what we need from the store by name! Using a bit of convention (note the naming convention of doX/selectX) things clean up pretty well:

import { connect } from 'redux-bundler-react'

// Names of props derived from selector string
// by convention start all selector names with
// `select` so `selectSomething` would inject
// a prop called `something`
// Start all action creators with `doX` inject those as is.
// We need to distinguish between them because we need to
// be able to run the selectors to determine if the
// component needs to be re-rendered or not.
export default connect(
  'selectSomething',
  'selectSomethingElse',
  'selectOtherThing',
  'doSomething',
  'doSomethingElse',
  // we can just inline the component here, or obviously we could still
  // store it as a variable.
  ({ something, somethingElse, otherThing, doSomething, doSomethingElse }) => (
    <div>...</div>
  )
)

By using strings and a bit of convention (selectX and doX) we don't have to worry about maintaining direct references. Some people won't like this just because it's less explicit. But, it makes me happy. It provides such a nice, declarative API with so much less boilerplate. Most of the resistance to this type of thing is from folks who say that not importing things is less explicit and therefore harder to track down errors. But, as it turns out if the strings reference things that don't exist on the store, we can easily just throw an Error and tell you exactly what's wrong. The thing is our store is already in the context anyway! So if you try to connect something that doesn't exist, our connect function can tell you that before you even try to use it.

Now, all of a sudden, you don't have to import all those selectors and action creators at the top of your files. This has some additional performance benefits because having lots of imports is not "free." I've seen demonstrations where simply walking the dependency tree of a large app was taking around 400ms on a mediocre phone. In addition, you don't have to repeatedly bind action creators to the store because you've already done that. Also, when you want to restructure your code base, you don't have to maintain all those import paths. If you've ever done a large-scale refactor you know this can be a pain.

Note: It's worth noting that you can write a simple bundling approach without attaching everything to the store. We did this when I was at Starbucks. We used the concept of bundles to organize our code base, but we didn't attach everything to the store as I'm describing here. To support the more straightforward binding mechanism shown above, we need to use redux-bundler's version of connect(). But, if you don't attach everything you can still use the standard react-redux or preact-redux library.

What about circular imports?

Another problem we mentioned was circular imports for selectors. If you start leaning heavily on selectors that depend on one another, you're very likely to run into this issue. However, at the point where we decide everything will get aggregated onto the store, we can write selectors that also use string names to reference their dependencies. Then we can resolve those input functions when we've gathered them all as part of creating the store.

So, if you have a bundle that needs a selector from another bundle, you could certainly just import that function directly, but we could write a smarter createSelector that can resolve string references too.

Instead of doing:

import { createSelector } from 'reselect'
import selectIsLoggedIn from './other-selectors'

// a basic input selector
export const selectUserData = state => state.user

export const shouldFetchData = createSelector(
  selectUserData,
  selectIsLoggedIn,
  (loggedIn, userData) => {
    if (loggedIn && !userData) {
      // ...
    }
  }
)

We can skip the import of the other selector.

// the version in redux-bundler has a resolver
// mechanism.
import { createSelector } from 'redux-bundler'

// a basic input selector
export const selectUserData = state => state.user

export const shouldFetchData = createSelector(
  selectUserData,
  // now this is just a string reference too
  'selectIsLoggedIn',
  (loggedIn, userData) => {
    if (loggedIn && !userData) {
      // ...
    }
  }
)

At first blush, this may seem a bit wonky, but just as with strings for connect there's still enough context here to where this we can provide useful errors.

The function that composes bundles can extract an object of all the selectors, then run a dependency resolution algorithm that is tolerant of circular imports. This algorithm replaces the string references with the actual selector functions. If this fails because a certain selector doesn't exist, it can give you a useful error to help diagnose the issue.

By the way, if you're interested, I also broke this selector resolution functionality out into a separate library: https://github.com/HenrikJoreteg/create-selector

Loading more Redux bundles later

By removing the direct coupling of bundles between one another, and between components (using names instead of direct imports) it also makes it a lot simpler to upgrade our Redux store after the fact. We can lazy-load additional Redux-related functionality. You may have heard it said that Redux makes it more challenging to do code splitting. But since everything gets consolidated into the store, we can expose a way to merge in additional bundles after the store has already been instantiated.

Splitting Redux code into chunks is not something I've personally done in production because I haven't felt the need for it. The entire Redux portion of the code base is typically in the tens of kilobytes, so it hasn't been worthwhile to split it further. Also, this is the "brains" of the app, so it's nice to have the full awareness of its capabilities present in the browser from the beginning. I tend to use code splitting to separate out the UI components and UI libraries instead. If you want to do this, a great place to start is the react-loadable library. You can use it to wrap the components you return from your route selector so that they can be isolated into distinct chunks and loaded asynchronously.

Regardless, this architecture allows for splitting and lazy-loading of Redux related functionality too, if you need it.

Higher order bundles.

Something else interesting becomes possible when you introduce this formalized bundle abstraction: dynamic bundles. Or using our previous terminology "higher-order bundles." As we've said, with vanilla Redux, there are quite a few things we need to do just to do an async data fetch. But, at the point where we're using this bundle pattern, we can write a function that returns a whole bundle.

We could write a function that returns an "async resource bundle," that we can configure with a few options. Plus, since a bundle is just a JavaScript Object, nothing is stopping us from further modifying the result with other selectors, etc.:

import {
  composeBundles,
  createSelector,
  createAsyncResourceBundle
} from 'redux-bundler'

const usersBundle = createAsyncResourceBundle({
  name: 'users',
  actionBaseType: 'USERS',
  getPromise: ({ apiFetch }) => apiFetch('/users'),
  staleAge: 900000 // fifteen minutes
})

// We can add a reactor to the resulting bundle
// that specifies the *exact* conditions which
// should cause a fetch. The `selectShouldUpdateUsers`
// created by the bundle and encapsulates all the logic
// for whether it should be updated based on its age.
// But we could also add whatever other derived conditions
// here too. Such as checking to see if you're on a certain
// route or if you're logged in, etc.
usersBundle.reactShouldFetch = createSelector(
  'selectShouldUpdateUsers',
  shouldUpdate => {
    if (shouldUpdate) {
      return { actionCreator: 'doFetchUsers' }
    }
  }
)

export default composeBundles(
  usersBundle
  // ... other bundles
)

Boilerplate you say? The boilerplate is virtually gone. With these few lines of code we've now added a "users bundle" that will automatically fetch a resource when the app starts, cache it when successful, re-fetch whenever it's older than 15 minutes, and automatically re-try failed requests. Plus we have a set of selectors for reading the data, checking if it's currently fetching, etc.

Since it's just returning an object, we can overwrite or extend the result however we wish, as I did in that example to add a reactor that defines the conditions we use to trigger a fetch.

I'm not saying we should create these async resource bundles for everything, but it's a powerful example of the types of abstractions we can create by grouping things into bundles of related functionality like this. We can create abstractions at whatever level makes sense for our applications.

Check out the "honey-badger" example at https://reduxbook.com/honey-badger to see what you can do with this approach.

Routing and URLs

Since URL management is such a crucial part of a JavaScript application, there are a couple of "batteries included" bundles that ship with Redux-bundler that you can optionally use to do routing and URL management.

The included URL bundle has selectors and action creators for reading and updating various parts of the URL like pathname, query parameters, etc. It can do this by replace or push, and it supports fine-grained control of everything from manipulating query parameters as an object or updating hash values.

There's also a helper function for creating a routing bundle, which works like this:

import { createRouteBundle } from 'redux-bundler'
import HomePage from './components/home-page'
import UserListPage from './components/user-list'
import UserDetailPage from './components/user-detail'

export default createRouteBundle({
  // this value could be *anything*
  // it could be a component like we're
  // doing here.
  // But it could be any "result" you
  // want to be able to select when
  // it matches this route.
  '/': HomePage,
  '/users': UserListPage,
  // you can also specify parameters
  // and then use `selectRouteParams`
  // to retrieve their values
  '/users/:userId': UserDetailPage
})

This will generate a route bundle with a selectActiveRoute selector that will return whatever the current matched route result is.

We can use it inside an "app component" to retrieve and render the right component:

import { connect } from 'redux-bundler'

const App = ({ route }) => {
  const CurrentPageComponent = route
  <div>
    <nav>
      <a href='/'>home</a>
      <a href='/other'>other</a>
    </nav>
    <main>
      <CurrentPageComponent />
    </main>
    <footer><a href='/about'>about</a></footer>
  </div>
}

export default connect('selectRoute', App)

Since selectRoute will return whatever the value is from that route object we passed to createRouteBundle, we could also use it to return an object full of metadata about the route to perhaps do something like set the page title, etc.

There's no need to pass any props to the selected component. Instead, the individual route components can just connect() to grab whatever else it may need to know from the state.

The batteries included approach

Nothing is saying that you have to use all the bundles that ship with Redux-bundler. If all you want is the bundle composition, there's a composeBundlesRaw function that gives you just the composition pieces. But since everything is in these nice little bundles, I include a few optional bundles in the same repo. That way, they're there if you need them and with "tree-shaking" becoming increasingly common in build tools, this means it won't bloat your application code size if you don't use it.

Debugging

Bundling all our code together makes it so much easier to debug things. We don't even actually need to have built a UI to trigger an action creator. Redux bundler includes an optional "debug" bundle that is designed to be able to be left in your codebase in production. By default, it doesn't do anything, but if you set localStorage.debug to a truthy value and refresh your app, you'll see a lot of logging information. Also, it will bind the store as window.store.

As a result, you can use your JS console in your browser to see everything that your app can do.

// you can trigger actions
> store.doFetchUsers()

// and select things
> store.selectRoute()

Giving credit where credit is due

Django: Before I started writing JS full time (roughly when node.js came out) I was a Django developer. For those who are not familiar, Django is a Python-based application framework for creating dynamic server applications. Django has a concept they call "reusable Django apps".

These are also bundles of functionality that may or may not have related UI, may or may not have related database models, and may or may not have been included as part of the core Django library.

Some of them require that others are also installed, but generally, they all play by the same rules and get composed into an application by being "installed" a.k.a. listed in a logical order, in the settings.py file. They're not nestable; it's just a flat list of "apps" you can add, each adding a set of functionality in a Django-esque way to your app.

For this to work, they need to be structured a certain way. Just like Redux-bundler, it's an API based on conventions. Some people scoff at this, but conventions are ridiculously powerful in reducing complexity, ahem!. This type of bundling, where you compose a set of chunks into a single app, is something I've missed since Django. It works so well, while still being simple to understand.

Starbucks: As I've mentioned a few times in the book, I recently worked with Starbucks to build a PWA that is now the default user experience for people in the US who log into their accounts on Starbucks.com.

The basic idea of this type of bundling was influenced by my work there. I was asked to solve the problem of trying to re-use a bunch of functionality we'd written for one app (including UI components) as a part of another app so that they could organize teams around their established business structures. We primarily focused on re-using large complete chunks of usable UI and all the underlying functionality. These "sub apps" as we called them also managed market-specific configuration, declared routes, handled internationalization messages, and shared a UI shell. We then did all the composition of these sub-apps into a "shell" helper that came pre-built with a bunch of stuff we knew we'd need—things like a standard UI for notifications, etc. That shell did a lot more than bundle up a few reducers and selectors, but the basic idea worked quite well and reminded me of Django. So I started spending nights and weekends hacking on this idea and using it for personal projects.

How big is Redux bundler!?

I'm a bit tired of installing lots and lots of independent dependencies. So I just made redux-bundler include everything (even redux itself!), so I can pluck out what I need and get rid of the rest with tree-shaking. It weighs in at 9 kb min+gzip total. This number includes:

  1. Redux itself
  2. A very slightly tweaked redux-thunk (it changes how the withExtraArgs stuff works).
  3. the bundle composition code
  4. A declarative local caching solution.
  5. A configurable URL bundle
  6. A helper for generating a routing bundle
  7. The reactor bundle is what enables all bundles to export reactors. This bundle will monitor them and dispatch their results as discussed in previous chapters.
  8. A leave-in-able debugging module that will tell you:

    • what bundles are in use
    • what reactions will be triggered based on the current state
    • what the result of all known selectors are at each iteration (only in debug mode)
    • it also exposes the store itself to window so you can call its action creators and selectors from the JS console.
  9. An async-count bundle so you can store.selectAsyncActive() to see if any asynchronous actions are in progress. This can also be modified to take a callback or return a promise when all async actions have completed which is useful for SSR if you're into that sort of thing.

  10. A createAsyncResourceBundle helper for generating cache aware bundles for remote data that you want to keep up-to-date.
  11. A configurable geolocation bundle (built on createAsyncResourceBundle) for requesting and keeping up-to-date user geolocation from the browser.

That size includes all of those things and their dependencies.

For comparison, as of this writing, React, and React-DOM is about 34kb min+gzip (prod build), and React-Router is about 8.5kb.

If you pair redux-bundler with Preact, and money-clip for caching, you're only at about 14kb total! This size is closer to where I feel we need to be for library/FW code. I've talked about the importance of code size for performance reasons several times.

Another key thing to understand: init

To allow consolidation of features inside a bundle you often need a way to initialize something.

Say you want to write a viewport bundle that listens for resize events on the window and then reports those back to Redux so it can be used as inputs to selectors or whatnot. You need to actually run the window.addEventListener('resize', ...) code somewhere.

To accommodate this, you can export an init function. After the store is created, the composeBundles() function will run any init functions and pass it the store reference. This way you can register listeners and dispatch things as a result.

Structuring your files in your code base

If we impose a rule on ourselves that anything Redux related happens in a bundle file, then our file structure becomes incredibly simple again:

/src
  /bundles
    users.js
    redirects.js
    routes.js
    index.js
  /components
    /pages
      home.js
      user-list.js
      user-detail.js
      not-found.js
    root.js
    layout.js

What I like to do is use the /src/bundles/index.js file to compose all the bundles. Here's an example of what my bundles/index.js file tends to look like

import usersBundle from './users'
import routeBundle from './routes'
import redirectsBundle from './redirects'

export default composeBundles(
  usersBundle,
  routeBundle,
  redirectsBundle
  // ... other bundles
)

The result of composeBundles is a function that optionally takes starting state as an argument and returns the store. Then from my root component, I can kind of pull it all together, pass in any starting data, etc.

Wrapping things up

That concludes the book. I really hope you enjoyed it and got something out of it! If you have any feedback or other questions for me, don't hesitate to email me at henrik@joreteg.com. I occasionally post longer-form content on my blog: joreteg.com, but the best way to keep up with my latest thoughts on building awesome web stuff is to follow me on Twitter: @HenrikJoreteg. Thanks again, let's keep making the web faster, better, and even more amazing <3

Chapter recap

  1. It feels less chaotic to organize Redux code around features and related functionality instead of grouping by types, such as reducers, action creators, selectors, etc.
  2. I've written a tool called redux-bundler that I use to compose these "bundles" of related functionality into a Redux store. There's a documentation site with much more detail at http://reduxbundler.com
  3. Just like combineReducers is used to compose and fold functionality into the Redux store, we can take a similar approach to action creators and selectors and attach them to the store directly, as well.
  4. This has many benefits:
    1. Less boilerplate
    2. Looser coupling
    3. Better performance
    4. Simpler connect()
    5. Easier debugging
    6. The ability to call action creators and selectors easily from the JS console of your browser without needing to build UI for it.
  5. Redux-bundler includes tools for URL management and routing, but takes a "batteries included" approach of supplying these pieces, but not forcing you to use them. Instead, you can use composeBundlesRaw() to compose only the pieces that you want to use.
  6. We can use the names of selectors to allow them to depend on each other without causing circular import issues.
  7. In the same way, just as we can programmatically generate reducers, we can generate entire configurable bundles to create very powerful abstractions without deviating from the Redux patterns we've already introduced.
  8. If we make all Redux-related functionality live in a "bundle," then our code structure becomes really simple and flat.
  9. It's nice to use the index.js in the /src/bundles folder to compose all the bundles and export a function ready to receive initial state and return the store.

results matching ""

    No results matching ""