I'm on my 3rd or 4th iteration teaching a week-long course on web development using React.js and Redux with redux-bundler and about half way through the last class I had the idea to whiteboard a diagram describing my mental model of how a redux-bundler app is structured.  I can't believe I didn't do this sooner, I think it made a lot of the big-picture concepts a little easier to grasp.

I've been using the React / Bundler approach for building apps since mid 2018 and I've been able to build or helped build probably 10 - 12 applications in that time. These apps vary in complexity, but for a sense of scale, the latest one has 80 bundles with right around 1000 unique selectors.  I don't think any other approach I've used in the past would allow me to build out this complex of an application in such a short time, and with the confidence that it'll "just work".  

Because I've been building apps this way for a little while, I think I've got a pretty good mental model of how all the moving parts fit together, but I'm finding that this big picture is one of the steeper parts of the learning curve when getting new developers started with the stack.  Hopefully this post will help smooth it out a little bit.

I'm not going to go into the full detail for each of the frameworks in this post, that would be a much longer read... neither of us want that...  But I will try to give enough detail that you hopefully get an idea of how I think about these kinds of apps.  I highly recommend you read the docs for bundler, React and Redux if you want to get the full details.

So, the overall picture looks something like this:

Let's dive into each individual part in a little more detail.

Bundles

Let's start at the left side of the diagram.  In most of the redux-bundler (or bundler as I usually say) apps, the majority of the business logic lives inside our bundles. Bundles are typically organized topically, or by database "entity" that they are related to, it doesn't really matter as long as the bundles are organized in a way that makes sense to you and your team.  An individual bundle is written as a simple Javascript object, which is awesome since that enables you to do things like follow the bundle creator pattern that I like to use to reuse logic and functionality between bundles (this should be the subject of an upcoming post).

There are three key things we need to know about bundles:

  • Each bundle must have a key of "name" which is set to a value that is unique for the application.  The name that you assign will be used to hold all of the data related to that bundle inside the main Redux store.
  • Each bundle must have a reducer ("reducer":...) or a function that returns a reducer ("getReducer":...).
  • Each bundle can contain any number of action creators, selectors and reactors that follow a naming convention to describe which they are do..., select..., and react... respectively.

As our application starts up we want to take all of our bundles and consolidate them into a single redux store, this is where the composeBundles() function comes in.  composeBundles() takes any number of arguments which should each be an individual bundle, and outputs a common Redux store.

That's pretty cool, we are able to separate out different logical parts of our application into bundles that then get all piled together for us in a single store that we can use in our React (or Preact or other framework) application.

Redux Store

We don't really need to care too much about the Redux store itself as we build our application since it get's constructed from our bundles.  All of the action creators and selectors that we added to our bundles are now available on the store itself, and the data associated with each bundle is stored in the Redux store state object as a key matching the name of that bundle.

One thing to note here is that all of our action creators and selectors (and reactors etc...) are added at the same level in the store object, so they have to be globally unique.  This can get a little bit painful when you start getting a lot of bundles going inside your application, so I typically use a namespacing convention inside my bundles to help avoid any conflicts.  Each of the names or keys for the action creators or selectors in a bundle start with the keyword associated with their type, then I add the name of the bundle, then a description of what they are doing or selecting.

For example inside a bundle with the name of auth I might have an action creator called doAuthLogin or doAuthLogout and selectors with names like selectAuthUsername.  This convention can result in some verbose names, but overall the benefits far outweigh any inconvenience.  Globally unique names allow us to easily search through a project for uses of a selector or action creator and be confident that we find all of them quickly.  Another interesting side affect of the repeated parts of our verbose names is that they gzip really well, helping us keep those resource sizes down.

To get access to our action creators and selectors inside our React components, we use the connect() function from the redux-bundler-react module.  Typically I'll wrap a React component with connect() in the export statement like this:

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

class CurrentUser extends React.Component {
  render(){
    const { authUsername } = this.props;
    return <div>{ authUsername }</div>  	
  }
}

export default connect(
  'selectAuthUsername',
  CurrentUser
);

If you want to get fancy and play with functional components you can use the connect function like this as well:

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

export default connect(
  'selectAuthUsername',
  ({ authUsername }) => {
    return <div>{ authUsername }</div>
  }
);

React App

There are countless explainers on React.js out there so we won't get too deep in how React apps work, but I do want to highlight a couple of things.

At the top level of our application, we want to wire up our React app to the Redux bundle.  Bundler comes with a <Provider store={store}/> component that we can use to wrap the rest of our application with to allow us to connect each component to our store.  This is what allows the connect() function mentioned above to know what store to connect to.

Bundler also has the best, simplest client-side router that I'm familiar with built into it.  This is a great alternative to react-router when you don't need all the weight that library adds to your application.  The router built into bundler is a simple pattern matcher that returns whatever you tell it for a given pattern in the URL, this could be, and generally is, a React component, but could be whatever you want.  

Since the router is just another bundle, the same rules apply, you interact with it in exactly the same way you do with your own bundles, through action creators (doUpdateUrl()) and selectors (selectRoute()).  Integrating the current route root is as easy as connecting to the selectRoute() selector and then just rendering whatever get's returned as your component in the root of the application (or wherever you want the route to be reflected in your UI).

Bundler is just one of a large number of state management solutions for the React ecosystem, I'm pretty enamored with the way you can build out application logic that resides outside of your UI code, which makes changing the UI so much easier than it used to be.  React itself has an internal state management approach where each component has an internal state object to store information and can use props to pass information down to their children components.  You can use state directly when using class based components, and through hooks when using functional components, we're not going to get into the nitty gritty here.  

What is important is that when using bundler with React you want to make sure that you aren't duplicating storage of state in a bundle as well as in component state. Component state should only be used for data that is local to that particular component, such as toggle state on a button, or internal temporary data in a form. The main rule of thumb is that if any other part of the app could possibly care about the state of something, then it belongs in a bundle, and if it's in a bundle, use the state directly from the props you get out of selectors vs. putting the data into internal state.

Browser DOM (Document Object Model)

The final piece to our React / Bundler application is the DOM or Document Object Model.  This is the in-memory tree representation of the HTML elements that make up our application.  If everything goes well, we don't really need to deal with the DOM directly, but it is important to understand how the things we write in our React components get transformed into HTML for the browser.

The ReactDOM render() function is how we go from our fancy React components to the DOM.  The beauty of React is how the library takes changes to state and props inside our components and translates them into changes in the actual DOM, it does this pretty efficiently, and if everything goes well, your app can be doing a lot under the hood without the user feeling it in a janky UI.  

Thanks

That's it for now.  If you have any questions hit me up @willbreitkreutz most everywhere, and if you're interested in training, I typically host 3-4 courses throughout the year that are open to any federal employee, there's no tuition, but travel and labor must be covered by your office.  I'm also available for training at your organization.