The chosen stack
In deciding which packages to add on top of React to achieve our goals, we had to not only consider which capabilities we need in our app, but for each of them, which specific package to go with.
For each decision, there were many alternatives available with competing philosophies and compelling arguments (see my earlier point about “decision fatigue”).
Our rubric was relatively straightforward: make sure the packages work well with each other, have an active community (that we could participate in as well), and are well-maintained and supported.
Note that this is what worked for us given our situation and requirements, but YMMV!
Finally, here’s the React stack that we settled on:
- Redux for state management
- ImmutableJS for immutability
- Reselect for accessing memoized derived data from the store
- Redux-Saga for async side-effects
- React Router v4 for URL routing
- React Router Redux for binding routes with Redux
- Jest for snapshot testing and as a test runner
- Enzyme for traversing and testing a component’s DOM
- Helmet for <meta> tags management
With that, let’s get into the details about each decision.
Why we decided on the above
Redux — State Management
Redux is a simple, but opinionated state management library most commonly used with React. It greatly simplifies how you think about state in your app.
Redux maintains a single store for the entire state of your app, and it is read-only. Any changes to the state are dispatched through “action” objects that carry the necessary information needed to change the state. The dispatched actions run through a reducer whose job is to use the information in the action to return a new state for your app.
This unidirectional flow of state changes has couple benefits:
- Makes state changes extremely clear and explicit — no direct manipulation of the state, everything happens through action objects enforcing a clear protocol
- Allows for “time travel debugging” as you step back and forth between each variation of the state since dispatched actions can be simply replayed
Goal wins: Helps build a scalable architecture, bringing sanity to state management, as well as making the app more testable and easier to debug!
Alternatives: MobX, Relay, this.setState (i.e. React’s built-in state)
Why Redux: Strong community support and popularity over competing packages. Way more scalable architecture than managing state using the built-in React APIs.
ImmutableJS — Immutable State Objects
ImmutableJS (mostly just called ‘Immutable’) is a collection of immutable data structures with a comprehensive set of available operations.
Using immutable type for your state ensures you never accidentally modify your state directly, as well as allows for efficient change detection and memoization wins.
Goal wins: Makes the app more performant through reference equality checks instead of value equality checks for detecting when to re-render!
Alternatives: seamless-immutable, sheer willpower (no immutable type, being careful to not mutate data directly)
Why ImmutableJS: Pretty good built-in support for common operations (think: built-in lodash), and strong evidence of successful use with React.
Reselect — Memoized derived data from the store
While you can access store data directly from your components, most likely in a real-world app, you’ll have some derived data that you compute from your state and want consistent access to in your components. Additionally, depending upon the complexity, you’d want it to be memoized for performance reasons.
This is where Reselect comes in. It acts like an access layer around your Redux store giving you memoized access to direct and derived data from the store. It’s really useful, and you most likely want it.
Goal wins: Big performance boost through memoization, especially when getting into complex selectors that can get expensive to re-compute!
Redux-Saga — Async Side-Effects
Sagas are basically middleware for your Redux app that can listen for certain dispatched actions and trigger any ancillary operations you want as a result of those actions being fired.
Some examples of when to use sagas:
- Handling API requests to the server
- Recording analytics events
- Firing notifications on certain triggers
Goal wins: Helps in scaling architectural complexity as the interface components gets separated from any business logic, allowing the components to only worry about dispatching actions to indicate what happened and side-effects take care of updating state and performing other actions as necessary.
Alternatives: Redux-Thunk, Redux-Observable, redux-loop
Why Redux-Saga: Easier to test, consolidated place for your app’s side-effects. Also seems to be the most popular and well supported choice from a community point of view.
React Router v4 + React Router Redux — URL Routing
React Router is the de-facto choice when it comes to handling URL routing in your React app. The real question was whether to go with the older v2/v3 API or the complete re-write that is v4.
Pretty much the entire design and philosophy of React Router changed with v4, making it practically a different library from before.
React Router v4 offers some advanced techniques with routing, allowing you to make routing decisions nested deep within your app, even as the app is rendering. We’re not doing any of that for our purposes yet, but it’s something worth evaluating for your app as it can be a powerful tool.
React Router Redux is an add-on package (now folded into React Router repo), that binds the location information with your Redux store, allowing for your selectors for example to easily access the location information. This is useful if you have some information in the URL (e.g. /<entity-id>/foo) necessary for your app that you would like direct access to in your store.
Alternatives: React Router v3, redux-little-router
Why React Router v4 and react-router-redux: It’s the latest and greatest, since we were starting a new project from scratch, we wanted to adopt it now and not worry about the deprecation that’s bound to happen.
Deciding over redux-little-router was harder and in full honesty, there’s still a chance we’d evaluate switching to over, as there are some quirks and odd behaviors in react-router-redux that can be very frustrating to debug if you’re not fully aware of the intricate details of their working. We’ll go over some of the gotchas in more detail in part 2 of this post.
Jest + Enzyme — Testing Solution
Jest is a universal testing platform that works especially well as a test-runner for React with a pretty good built-in support for things like snapshot testing. When combined with Enzyme, you get access to a convenient way of traversing, manipulating and asserting the output of your React components.
Goal wins: Makes the components easy to test, allowing us to move fast with confidence!
Alternatives (to Jest): Mocha
Why Jest and Enzyme: Tight support for React in Jest, and Enzyme’s amazing support for selectors and DOM traversal to easily target elements and use simple jQuery-like syntax made it a no-brainer to go with this setup.
Helmet
A super-simple helper library to manage your <title> and <meta> tags (description, keywords, etc.) for your app. It’s the simplest (see examples), most popular library for doing this, so if you’re interested in managing and updating your document head from within the React app, you should get it.