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.