Elm vs React + Redux
I’ve been using Elm in production for 1-2 years now. Love it. Advantages?
- blazing speed,
- small file sizes,
- bug free code
- and a very pleasant developer experience.
Elm is like Singapore: everything is pristine and perfect, and PURE. This purity is tightly protected: Elm does not want dirty mud-bloods from tarnishing its pure Aryan environment: if you want to use Javascript, you’ll have to do so via “ports”.
In other words, it’s difficult to write buggy code in Elm. My confidence in it is so great that I don’t write tests, because the results are immediately self evident, and secondly, the compiler helpfully reminds you if you’ve missed something. But, the corresponding difficulty is that if you want to do something remotely complex where ports simply will not do - where you need to use existing Javascript libraries, then you’re stuck. You’ll either have to do a LOT of work, or you’ll have to ditch Elm for a Javascript solution (or another solution which compiles to Javascript, or otherwise uses WebAssembly to do much the same thing). I cannot emphasize this enough. There are costs to using Elm which you may not realise up front.
Enter: React + Redux. It is Elm born-again, except more complicated. It uses Javascript - so you loose the benefits of a pure functional programming paradigm. Sadly, in addition to the increased complexity, you’ll also need a whole host of third party tools in order to work effectively:
- redux
- redux thunk
- react-redux
- Redux Dev Tools
- Redux Form
(All of this is extra over-head, mostly needless, which is a headache for developers)….And you’ll also have to deal with the minutae of all the rules which come with it. It can get tricky: I will give you some examples:
(A) Extra Developer Overhead
(1) Debugging via Time Traveling
If you want to save your current redux state, you can do so with a query string at the end of your url: http://localhost:3000/?debug_session=test - and now your redux state is saved as “test”, allowing you to go back and forth between different states. Admittedly, you can’t do that in Elm (AFAIK), but you do get time traveling for free. The point being, it’s one less thing to think about when using Elm.
(2) Accessing History Objects
What if I want to control navigation after an action
has been dispatched? Now I need access to a history object. How am I meant to get access to that, when using a BrowserRouter
? Not to worry, simply remove the BrowswerRouter
from your app, use a standard Router
instead, and pass in your own history object as a props to that Router.
// history.js
import { createBrowserHistory } from 'history';
export default createBrowserHistory();
And handle that in your app.js like so:
<Router history={history}>
<div>
<Route path="/" exact component={StreamList} />
<Route path="/streams/new" component={StreamCreate} />
</div>
</Router>
Again, this is the type of thing you don’t need to worry about in Elm.
(3) Additional Libraries
What if you want to omit a key from an object in Javascript? Oh, let me grab Lodash for that. With Elm, the core libraries are pretty good. But you can always grab helper libraries if need be.
(4) Managing State
We want component level state. Where do we set state again? Was it inside the class? Or outside?
And do we use setState({...etc})
or can we use this.state = {...etc}
? Oh that’s right, we can directly set the state in the constructor, but not outside.
But do we have to specify a constructor? Ok, we don’t. But if we do, then we have to remember the props
parameter, and then pass that into super
. Got it!
Are you getting the picture? All of this is extra over-head: needless things that the developer must remember, that adds little or no value. But if you forget, then you’re going to have problems, and that means means extra time debugging. There are perhaps many, many little rules and opportunities to be tripped up when using React and Redux…
class App extends React.Component {
constructor(props) {
super(props); // Don't forget
// the only time when we can do this:
// in the constructor. Outside the constructor
// we should only amend state with the
// this.setState method:
this.state = {greeting: "hello world!"}
}
greeting(){
this.state = {greeting: "Don't set state like this!"}; // Nooooo!
// very bad things happen when you do this.
}
// or you can set initial state like this:
state = {greeting: "set state like this - is another option!"}
// simply dump in your class.
// it acts as syntactic sugar for the above.
greeting2(){
// are we allowed to set state while relying on our existing state?
this.setState({greeting: concat("hello ", this.state.greeting)})
// Noo! We must pass in a function, if we want to use our existing state
// to update state.
this.setState((state, _) => ({greeting: concat("hello ", this.state.greeting)}) )
}
// too much to remember!
Or here is another example from the React documentation:
// what's wrong with this?
class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
// This syntax ensures `this` is bound within handleClick
return (<button onClick={() => this.handleClick()}> Click me </button>);
// the problem is that the a different call back is created each time.
// so you'll have to remember to use an arraow function or bind the
// handleClick method to this in the constructor.
}
}
Or can we simply return false
in an event handler to prevent default behaviour? Nope: you have to call: event.preventDefault();
. Another thing to remember, and another opportunity to cock it up.
The Elm API simplifies all of that. It’s much harder to make a mistake or forget something.
(B) Maintaining Discipline
React requires developers to maintain a lot of more discipline than when writing in Elm. How so? Some examples:
(1) Pure Functions in Reducers
Reducers are meant to be pure; that is, they should be pure functions. But you have no way of enforcing this, and the only way you would know about this is if you read forums/posts/documentation about React best practices. Reducers are the React equivalent of an Elm update function. On the other hand, in Elm, everything is pure. You can’t really mutate anything. Where there is impurity, there are “commands” - the Elm API gently guides you down the path you must follow. React on the other hand gives you a long rope, which could be used to your detriment, if you are not careful.
(2) Each Component needs to maintain its own state
These problems are largely avoided in Elm, because we have a God state object, and we are largely forced to think about situations which arise where certain assumptions might fail - and we’re forced to handle them. The compiler incessantly reminds us: while annoying at times, this is also what makes Elm apps super robust.
(3) Make Impossible States Impossible
This is easy in Elm with variants of custom types. Example:
type PremierOfVictoria =
| GreatMemory
| Honest
| Genius
| ReceivesAndRespondsToADFMemosWithAlacrity
| Sober
(4) Redux Actions are POJO (Plain Ol’ Javascript Objects)
You whip up a few objects, and then realise: what if I make a typo? Unlike in Elm, if you make a typo and use a Message or type that is not recognised, then the Elm compiler will warn you. Not so in Javascript. So now what will you do? You will have to have the discipline to create your own actions, and your own
It’s much harder to do that in React Redux, making reasoning about your state correspondingly difficult.
(5) Maintaining and Updating State
In Elm, all state lives in a God Object: the “Model”. It is a mix of: (i) data pertaining to records, and (ii) data pertaining to views. React does not prescribe where state lives. If you’re not careful you can have states living in multiple places at once: (i) in props, and/or (ii) in a state management library (e.g. Redux), or (iii) React contexts. Keeping it simple requires discipline.
Both approaches are light years ahead of WPF models, where it’s easy to confuse yourself about state. e.g. Is our state in the input tag, or is it in the ViewModel? Right, let’s update the ViewModel with the values contained in the input tag. We need two-way data binding. This becomes a lot trickier than what it should.
(C) Costs in using Elm
I’ve alluded to this above. Ports. Anything more complex and you’re out of luck.
In addition, there are some implementation decisions which might not suit you. For example, I wanted to calculate SHA1 hashes for files which are dragged and dropped into a browser. Doing this in Elm. Painfully slow - it might have been the implementation itself, but I suspect that this is due to the inherent limitations in Elm). What about a Javascript implementation? that was faster, but it was still slow. In the end, I settled for a WebAssembly implementation with background workers, and used Ports to communicate with Elm.
All in all, the experience is a lot more complicated than it should be.