Initial Thoughts on Redux
I've started creating a sample application in Redux. You can follow the progress on my github repo.
It's a simple combat initiative tracker for Dungeons and Dragons Fifth Edition. Combat takes place in rounds, with players rolling an 'initiative' score to determine their order in the round. My application presents the ordered round list and cycles through each combatant, where they can do damage to other combatants.
Once I've finished the main part of the app I'll swap out the pre-fix combatants with what I'm calling an "onboarding" wizard - essentially a series of menus that lets a player add players and enemies. I plan on adding a server-side component too, so that the monster list can be based on real data.
There's enough in place at the time of writing to present some initial comments on Redux. I'll prefix everything that I say by saying that these opinions may change as I become more familiar with the library. But first, a quick overview on what's interesting about Redux, with a bit of history!
I use React and Flux in my day job and it's surprisingly effective. The project is six months old now and everything is holding up pretty well. We're considering moving our Flux code to Redux, so I thought I'd try it out.
React
React only concerns itself with the 'view' in a typical MVC application, with its intent to be a pure functional transformation between a piece of state (i.e a JavaScript object) and a piece of DOM. This makes React fast, especially when it only applies the minimal necessary DOM changes to the real DOM, in response to a re-render call.
Commonly seen in React code is JSX, a syntax that effectively lets you write close to DOM markup, interspersed with JavaScript where necessary:
render() {
return (
<section>
<h1>Round {this.props.round}</h1>
<ul>
{ this.props.combatants.map(combatant =>
<li className={this.props.currentPlayerId === combatant.id ? 'active' : ''}
key={combatant.id}>
{combatant.name}
</li>
</ul>
</section>
);
}
I find this immensely powerful and satisfying. Some really disagree with melding your presentation logic in this way, preferring something like Hyperscript, which would instead look like:
render() {
return h('section',
h('h1', this.props.round),
h('ul', this.props.combatants.map(combatant =>
h(this.props.currentPlayerId === combatant.id ? 'li.active' : 'li', combatant.name)
))
);
}
I don't think it matters that much. I prefer to write what's going to be output, even if it means taking up some extra space with closing tags. Choose what grooves with you.
React is a brilliant little library for producing functional, composable and clean UI code that is performant at scale.
Flux
Flux aims to provide the big picture. It's a pattern rather a framework, as such there are many different implementations, not all of which use React as a view component. d3 or angular can do just fine!
Flux returns to the roots of MVC. It favours a unidirectional data flow through your application. That's a fancy way of saying that all changes to the application state, whether instigated from the UI, networking code, etc, must all be posted as actions. Actions are simply a JavaScript object, normally with a type
property whose value is an ID (e.g END_TURN
), and some other properties with data.
Actions are handled by a single 'bus' object, known as a dispatcher. In a Flux architecture your model objects are held in Stores - an object representing a particular domain in your application. Stores only have getters, no setters! In order for their state to be changed, they must register with the dispatcher. Doing so will let them receive actions and they can update their state appropriately.
When a store is updated as a result of an action, it raises some event that your UI code listens to, which triggers a re-render.
The hard part of Flux is figuring out when to split up a Store - they end up being a bit larger in size than what you're used to. There's no hard and fast rule, it's purely a judgement call for you to make.
I have found that Flux architectures are pleasant to work in. Provided that you stick to the pattern and ensure all state changes occur via actions, Flux can scale well and be tested easily.
Redux
Onto Redux. You can think of Redux as an implementation of the Flux pattern, but likewise it constitutes a slight departure.
The key difference here is that instead of having many stores, there's one. It contains all of the application state, as a large JavaScript object. As there's only one store, you don't need an independent dispatcher. That becomes part of this monolithic store too!
But this isn't as bad as it looks. The job of a store is twofold: 1) it must store the application state, and 2) it must handle requests to change this state, via actions.
Redux splits these two responsibilities. Your singular store stores the application state. A new tree of components, called reducers handle the requests to change the state. A reducer is a function: reducer(oldState, action) => newState
. Reducers can be arranged into a tree-like structure, so that your application can be effectively namespaced. Split your reducers by responsibility. By applying this separation, you get several benefits:
- The creation of a store is automatic, just give it your top-level reducers.
- During development time, you can change a single reducer and have it hot reloaded into your running app, without having to rebuild the entire codebase! You can get some incredibly fast feedback, without having the state of the application reset or the browser refreshed. How cool is that?!
- As actions are an audit trail of state changes to your application, you can time travel, undoing and re-applying state changes. Combined with hot reloading, you can discover a bug in a reducer, fix it, then replay the actions to verify it's fixed.
So far the most difficult aspect for me at the moment is modelling my data in such a way that it allows for multiple reducers to act on it, without treading on each other's toes. Redux seems to advocate a heavily-normalized state object, so I'm going to look into normalizing my data even further.
On principle I'm fully on board with the advantages that Redux appears to offer. Now whilst at a small size, the usage of Containers with their mapStateToProps
and mapDispatchToProps
functions seems like total overkill, such abstractions are essential for larger applications.
What Redux doesn't handle, almost in any way, is how asynchronous actions should be handled. In fairness, neither does Flux. But several innovative ideas have formed in this area - in particular Redux Thunk, Redux Saga and Redux Loop.
I'll try each of those for my onboarding wizard.