A React journey: from vanilla, to type-safe, to monadic
About the possible evolution steps of a simple React application.
door Giuseppe Maggiore, Digital Entrepreneur & CTO
The journey starts here
In this article we will discuss the possible evolution steps of a simple React application, featuring some clickable counters. We will start from a fairly simple implementation built in JavaScript (ES6) and then add powerful abstractions on top of it in order to reduce bugs and take advantage of some generalizations in order to write less boilerplate code. The first element we will introduce is static typing, in combination with TypeScript, and we will observe how the additional structure given by an expressive and non-intrusive type system saves us from some categories of bugs. As we keep using TypeScript and React together, we will also start noticing some patterns which emerge in multiple places in our code. In order to express these patterns directly, and therefore write shorter and more directly useful code, we will introduce some higher-order reasoning in the form of monads, and specifically the monadic react library (obtainable via npm and github).
We will assume some basic understanding of JavaScript’s JSX, and while we will explain the basic concepts of React, we will not dive in the depth of the framework and assume that aspects about React that the reader still finds unclear are better explained elsewhere.
Vanilla react
Let us begin with a simple introduction to React. React is a framework that introduces the notion of renderable components which are assembled together in a template language (JSX) in order to define UI’s for webpages. Such renderable components, in the object-oriented fashion, encapsulate some state, which is stored inside each component in order to represent information local to it. Components can also instantiate each other, and while doing so also provide read-only information to each other. This read-only information that a component receives during instantiation is known as the properties of a component. So, state is mutable, the properties are read-only, and the component can render itself and while doing so instantiate other components. This deceivingly simple model allows us to quickly build components and their dependencies. Let us begin with a hello world:
Suppose we wanted to add some state to this component. We could, for example, add a counter, and a button to increase the counter. Let us begin by just adding the counter to the state of the component, thereby changing the constructor, and then modifying the render method:
Of course, the state does not change anywhere in this example, going a bit against the purpose of adding state in the first place. To obviate this issue we add a button which, when clicked, will increase the counter in the state by one. Information about the callback to invoke when the user clicks is given in the form of a property to the button:
At this point we notice something peculiar: inside the button onClickcallback, instead of simply modifying the state directly, we have to invoke the setState method in order to do it for us. This betrays the hybrid functional/object-oriented nature of the framework, as it lets us see that operations which affect state must be mediated by a layer which will handle change propagation for us, and which effectively makes React behave as if setState were re-instantiating and re-rendering components with the new state, instead of performing a simple local mutation of an instance variable.
A non-trivial example
Suppose now that we wanted to build an application with more than one counter. This means that we might want to move our counter functionality outside of the main container (which we called Sample in the previous section), say in a separate Counter React component, and to invoke it from Sample. Note that we are now calling Sample a container: containers are React components which also contain and manage internal state, whereas the simpler components perform no noteworthy state management.
The Counter component will need to be reasonably dumb, and will contain no state of its own. It will receive information about its counter in the form of properties passed by its parent (whether the parent is a container or a component itself does not matter), and whenever it wants to modify the counter it will simply invoke a callback. The Counter component makes no assumption about how its counter data is stored, and this makes it referentially transparent: if we instantiate the Counter with the same properties, it will behave exactly in the same way (or: the behavior of the Counter is entirely determined by its properties).
This leads us to the following implementation of the Counter:
Notice that the state of the counter is now empty, and rendering the message looks for the value of the counter in this.props.counter instead of this.state.counter. Moreover, when the +1 button gets clicked, we simply invoke this.props.increment, thereby delegating to the parent component the task of incrementing the counter. This frees the Counter component from any knowledge about storage and retrieval of data, meaning that we have acquired the flexibility of storing the counter in a database, a local variable, or any other storage system we might think of.
Of course, in our simple implementation storage will simply be done in the state of the parent component, which is, therefore, also a container. The state will feature multiple counters, and will therefore instantiate the Countercomponent multiple times:
Notice that each Counter component receives what we could see as a getter/setter pair in the properties: counter is a readonly variable, therefore a getter, whereas increment is a method for writing to the counter, therefore a (constrained) setter.
The first issue
The implementation above has a nice property: the isolation of the Counterfrom the Sample via a clear interface. This guarantees a desirable lack of unpredictable interference between the various instances of Counter. Each instance of Counter acts as a fully isolated, self-contained unit, which we can easily test and which we can quickly ascertain correctness.
Unfortunately, the correctness of the interface of Counter is not guaranteed in any way upon instantiation. Suppose we were to decide that we are done with building the Counter, and we moved its implementation to another file, or even a separate npm package (this would be admittedly a bit too much, but you never know). A new developer could get hired, start working on the code base, and then mistakenly assume that the name of the increment property is actually incr, leading us to the following, bugged implementation of the container:
You might be noticing that in order to spot the bug, you must read the code quite carefully. The lack of “vertical alignment” helps a bit, but at this point you might want to use your imagination and suppose that we were dealing with a much larger codebase, where the code instantiating the various counters is spread throughout the whole application and does not fit on a single screen. In such a case, the only way to spot the bug would be to actually trigger it (we now have three counters, all showing up correctly, two of which are fully working as expected!) and this might require extensive testing and might even slip past through our testing department leading us to deployment of a slightly broken application.
Enter type checking
A naive solution could be to simply assume we might need more testing. While more testing will certainly increase our chances to find bugs, it offers us no absolute guarantees. The number of possible combinations of user interactions of any non-trivial program will count in the billions of billions (if not infinite!), meaning that in order to have coverage of all possible edge cases of our application we might need an impossibly large number of tests. This is simply not feasible.
A simpler solution would be to take advantage of the compiler itself, in order to automate this sort of small check. We might simply “annotate” our components in a way that specifies what kind of data they expect in the properties, and then let the language validate correctness before even running the program, and notifying us of the issues found without even having to run a single test.
React already offers a partial solution for this: PropTypes. PropTypes are a form of annotation that specifies the expected properties of a component, so that when the component gets instantiated the properties are validated against our annotated specification. This is handy, and does partially solve the problem, but unfortunately if the component does not get instantiated (and this might depend on a complex user interaction which we do not test and which does not occur often) then this validation routine will not be invoked and we still run the risk of shipping a program with a hidden bug.
What we want is a type checker that will be run as we edit, so that structural validations are performed before even running the program for the first time. If there are structural issues in our program, we want to know as early in the development cycle as possible.
TypeScript to the rescue
TypeScript is a statically typed extension of JavaScript. This means that next to JavaScript code, which is a subset of TypeScript, we also add type annotations that specify the structures we expect our data (variables, parameters, etc.) to respect.
This means that whenever we declare a new symbol (usually a variable), we also specify its type after a colon:
The language will then make sure that anything we choose to assign to a variable actually respects the structure stated in the type declaration. If the compiler notices a mismatch, then a compiler error will be output to the user:
We can of course also define custom types, the simplest of which are interfaces. Interfaces are defined with the keyword interface or type, and then upon assigning objects to the interface the compiler will verify that the assigned expression is indeed compatible with the type declaration:
Indeed, the following assignment would produce a compiler error, because even though we state the variable to be a Teacher, we then assign a value with the wrong fields:
React and TypeScript
We can of course combine TypeScript and React. We do this by using an external library that contains the type definitions for the various React data. Based on these definitions we can guide the TypeScript compiler so that it validates our programs using React by comparing our invocations with the prescribed structures.
Since we know that React components contain both state and properties, these will now be specified as generic type parameters to the React.Component class. Even though the code for the Sample container is mostly the same, we can now specify the type of the properties and the state explicitly:
If we tried now to modify the state initialisation in the constructor to something meaningless, then we would get a compiler error:
The Counter component also receives the same treatment, by specifying that it has no internal state and a specific interface comes in via the properties:
Should we use the properties incorrectly inside the Counter component, then a compiler error would warn us of the issue and prompt us to fix it. Moreover, if we were to instantiate the Counter component from the Samplecomponent but with the wrong properties, then we would also get an error, thereby ensuring that the boundary expectations between components are not violated:
With relatively little effort (the code is, indeed, largely the same as it was in its first version) we have ensured that our components already have the equivalent of a 100% test coverage on some basic tests guaranteeing structural integrity. Of course we are not protected from logic errors, but we can still enjoy the equivalent of a lot of free tests.
Monadic React
The last issue that remains in React, even when combined with TypeScript and thus framed in a type-safe context, is the flow of data between components. A component will often instantiate child components on a subset of its state or properties, and will usually accept input from the child components in the form of callbacks. This process becomes slightly cumbersome at some point, because its indubitable added value (clearly specifying the boundaries of the interactions between parent and child) is muddied by verbosity.
Moreover, extra verbosity comes in play when we factor in the need to declare two types (one for the properties, one for the state), one class with its constructor, etc.
Monadic React is a library that wraps TypeScript and React in a dataflow library that emphasizes type-safe composition of React components via the most ubiquitous mechanism of all: functions. A Monadic React component is seen as something that takes as input data in one format, say T, and produces outputs in another format, say U. In TypeScript and React this would become at least one property of type T, and at least one callback of type U, joined in a component inheriting from React.Component<{ in: T, out:U }, {}>.
Monadic React recognizes that this pattern is so crucial, that it is embedded in a much simpler signature: T => C<U>, that is a function which takes as input a value of type T (it is an input after all!) and returns a C<U>, (C is an actual Monadic React data structure) which is the Monadic React wrapper of a React component which produces (in a callback) values of type U which are the result of the internal processing performed by the component. How this result is obtained (user input, API calls, …) is abstracted away at this level: we only care about the fact that a component produces data of some type which is then forwarded into other components. C<A> is, in some sense, very close to a type such as Promise<A>, but wrapped in a React flavor. C<A>, just like Promise<A>, features a chaining method (called then), and will produce values of type A and pass them to the callback given to then. The major difference between Promise and C is that C might also render something to the user, whereas Promise will not. For the rest they are equivalent, and indeed it is possible to convert a Promise<A> into a C<A> without loss of information and functionality (whereas the opposite is not possible, since Promise cannot render anything).
Primitives
Primitive datatypes are managed by ready-made components. These components all have the same signature: A => C<A>, where A is the primitive type being processed. The input is the value that we want to show the user, and of course each user action on this component produces a new value of type A.
For example, a Monadic React component that both shows and lets manipulate to the user a string of text will have signature string => C<string>, a number component will have signature number => C<number>, and so on.
The string component is the one working on strings, but before giving us the desired string => C<string>, it requires some configuration parameters. The full signature of the string component is therefore:
First we pass the Mode as input, which is either "view" or "edit". Then we define the type of the input object, for example "text", "url", etc. Finally, we provide the React key, which React needs to consistently map this component to the right DOM elements.
An example usage of string would, be, quite simply:
which would show us the expected usual greeting message.
We can embed this in an existing React application by invoking:
anywhere in our regular React code, thereby obtaining:
Thanks to type inference we do not need to specify the generic argument to simple_application, since the compiler can easily guess it to be string.
Notice that in the JavaScript console we now get some output, because the string component is passing its input through, which MonadicReact.simple_application is then passing to its second parameter (a callback that prints the value for our debugging convenience).
We might want to suppress this behavior by asking the string component to never broadcast any data, by simply stating:
The console is now empty:
State/repeat
Monadic React also emphasizes transformation of components via higher order components, which take as input existing components and augment them so that they achieve more than their original functionality.
One such component we will need right away solves the problem of managing state. For example, suppose we changed the previous example in order to show an input field instead of just some text in a div:
Disappointingly, as we try to edit, we will notice that the text in the input box does not change. If we open the console though, we will see that the component is producing some output after each attempted edit. It does make sense then that the component is still showing "Hello world!" (that is exactly what the code is stating!), and broadcasting edits to whoever is listening. Our goal then becomes to adjust our string component instantiation so that the result of each edit is also fed back into the string so that it can update itself.
This sort of “data loop” is implemented by a higher order component: repeat. repeat feeds data back into a component, and also broadcasts all changes to its own listeners. By simply writing:
Then we achieve the desired result:
Notice a few peculiarities about the code above. First of all, we specify the type of data that repeat will store as a generic parameter. Since types and values exist in separate namespaces, there is no confusion between string(the type) and string (the Monadic React component).
Also, notice that we are now passing the initial string "Hello world" to repeat, and not to string itself. This way we simply tell repeat what is the initial state, which is passed to the internal component right away, and then whenever the internal component produces a new string then we discard the old and use the new one instead.
Multiplexing/any
Ok, so far so good: we can put a string on the screen and make it editable. We can also use other combinators such as number, date_time, bool, and even play around with selector or multi_selector. The basic elements are all there, with a proper monadic interface.
Suppose that we wanted to coordinate multiple such elements, for example to build a form featuring a string and a number. Then we would need one string component and one number component “running in parallel”, both displaying their result and waiting for it to be broadcast. Let us begin with the readonly version of the form.
We define a datatype containing the form data:
We then use the any combinator to instantiate the two components on the right element of the state. any takes as input an array of functions of generic type A => C<B>, and merges them together into a single A => C<B>, which passes its input of type A to all the functions of the array of parameters, and then yields as its own output the output of the first of its parameter function which produced an output itself. This is very similar to Promise.race.
This results in the following code:
Notice that the any combinator wraps functions where the input and output data might differ, therefore we must specify two generic type arguments: one for the input, one for the output. Since we are not caring about the output, we use an empty type {}.
To make this form editable, we cannot “just” change the mode from "view" to "edit". We must also convert the output of both the string and numbercomponents back into a FormData. This is simply done by mapping each component so that it yields a FormData:
As a last step, we must wrap the whole any inside a repeat in order to save the state. The initial form data, which we just passed to any, is now passed to repeat, since repeat will take care of passing to any the most up to date state:
Thanks to the fact that each component and combinator (also the two maps) has a key, we get no annoying React warning, and we get the desired result:
Conversion/retract
The individual elements of the form we have just built follow a pattern which is so common that it has its own combinator. Let us dissect the pattern first:
We start with “too much data”: the fd parameter, which has type FormData, contains more than string needs. string can only draw text, not FormData, so we need a way to perform the conversion FormData => string. We see this implicitly in action in the expression fd.s, which projects the needed data from the larger structure. When we have a result from editing, in our case a new string s, we need to put it back in the FormData. We do this by building a new instance of FormData which contains all the original data, with one change: s is updated. We see this inside the map combinator, which embeds s inside fd.
This pattern is implemented by the retract combinator, which explicitly supports the projection/embedding of a data structure inside another. Its signature is a bit of a mouthful, but everything maps directly to the concepts we have just seen:
This version is not necessarily much better than the previous one, but it has the advantage that we can keep the inner component (in our case the stringon the third line) cleanly isolated, and should the need arise we could also move it to a separate definition.
Back to the counter
The components we have seen so far cover almost all there is to Monadic React, to the exclusion of routing and of course some extra utility combinators. The extended core of combinators that we have seen is, in any case, more than enough to go back to the original problem we had to face: building a form with some counters.
Let us begin with the counter itself: we need a proper type for its state (I find that defining a type even for wrapping a simple number is handy: insight does change over time, and this way it is easier to add more information to the definition). The counter state is therefore:
The counter component is simply the any of a string (which tells us how many times we have counted so far, and is closed by a never since we do not care about its output) and a button (which increases the counter by one):
Notice that button simply takes a value (in our case n+1) and just yields it when it gets clicked.
We then wrap the two counters in a separate component, with its separate state:
This produces the desired final result:
More HTML combinators
Of course, we want to give our webpage a desired structure or add specific HTML elements which do not count for the data flow, such as labels and titles. There are numerous Monadic React “pass-through” decorators that produce no data of their own, but instead just output some HTML elements and then delegate to some inner component the job of actually doing data manipulation. For example, the div decorator has the following signature:
Notice that a div simply takes as input a component p, and simply decorates it. The div pretends then to have the same signature as p, but in reality all inputs of type A given to the div will go to p, and all outputs of type Bproduced by the div actually come from p.
As a final note: at the time of writing, React 16 has just been released. React 16 makes it possible to use less div elements in abstract elements such as retract, any, etc. A porting is underway which will lead to much more beautiful HTML without lots of noisy monadic elements which are only needed as glue. This version of Monadic React is still not publicly available, but will be extensively tested and published in the coming weeks.
Conclusion
As a quick note, observe the information density of our Monadic React code: the sample counts roughly half as many lines of code as the typed version, while being at least as much type safe (arguably more, since also the composition and transformation of components is statically typed). By using frameworks with a higher information density, we can focus very quickly on expressing the information we care about, without being bogged down by boilerplate. This can be quite evident in practice: an actual refactoring in a real project cut more than 400 lines of TypeScript and React down to less than 20 Monadic React lines.
Moreover, actual usage of Monadic React in practice shows that the high abstraction level forces developers to “design their code first”, and when all the data dependencies and transformations are understood then implementation becomes a trivial matter. I will not make any claim that “Monadic React cuts your development time by 3000%” or any such triumphalism, but I will claim that the emphasis on composition and data flow makes the interesting and complex bits of the problem jump to the eye, leading us to work in a more direct, goal-oriented fashion that, in the hands of a seasoned software engineer does lead to improved productivity and a quite pleasant experience.
Note: all code written in this article is available.
Over de auteur
Guiseppe Maggiore is Digital Entrepreneur & CTO bij Hoppinger.
Mis niets
Schrijf je in voor onze nieuwsbrief en laat ons jouw gids zijn in een complexe digitale wereld.