A ‘Lessons Learned’: vanilla React vs. React+Redux+TypeScript

Add long term developer stability with a moderate upfront cost

(Please note this is a summary of personal experiences and impressions. It is not meant as a thorough introduction to something specific. Primarily meant as a reference point for consideration, this article may differ with your experience with these technologies.)

Purpose:

To serve as reference to anyone writing a new React application and deciding on its configuration, particularly whether to incorporate Redux and/or TypeScript

Introduction:

In February 2017, I wrote a version of 2048 using React. The game seemed like a perfect way to experiment with React’s unidirectional data flow structure: a user action triggers a change in the internal state (i.e. a keyboard press results in a changed game board), and the changes are rendered by React. Satisfied by the result, I re-wrote the application from scratch in April, writing in TypeScript and incorporating a Redux store for state management.


The clear downside of incorporating the two aforementioned technologies was the initial learning curve. While there is a plethora of resources available, the ability to comfortably write code for Redux or in the style appropriate for TypeScript may require additional time for acclimation, which will inevitably depend further on one’s experience and aptitude. That said, for the most part the skills transfer well between projects so the subsequent projects are going to require less time and effort to enter the flow of programming.

Further details that are specific to Redux or TS are described below.

Redux:

The obvious reality is that introducing redux to an application will inevitably increase its complexity. From my experience, the complexity has two parts: one is the added infrastructure in terms of folder and file structuring in your source code, the other is the conceptual complexity dealing with the inherent flow to Redux.

In theory, the former of the two can be simplified down since all that is really required to have a functional Redux workflow is a store and a reducer, which technically can be written in a single file. That said, it is clearly frowned upon to bundle up reducers or forgo action creators since doing so will complicate the code itself and require closer attention for debugging or onboarding new developers. Therefore, it is easier to assume this complexity as a buy-in cost for Redux, which, depending on the scale of the application, may or may not be all that significant.

Now, the latter is where the learning curve generally applies. If not cautious, it is easy to be lost in the overall flow of state changes: translating inputs to actions, using data from those actions in reducers, etc.

The benefits of Redux and a separation of application states is apparent when adding features throughout the development process. In my example, I had an application that handled just the game board as the first release of the game. To add other functionalities such as score tracking and user login, it is significantly easier to write the React components, add Redux handlers and connect the new parts to the existing parts as opposed to reconfiguring the existing React components to reflect additions to the state. This can be especially beneficial for teams with multiple developers since it can reduce the amount of potential merge conflicts by minimizing the changes to existing code.

TypeScript:

TypeScript’s learning curve is more gradual since there is no strict rules on its implementation; you can use as little (or as much) of its features as necessary, which means it allows for gradual additions to your code as your understanding of TS matures.

The more obvious benefits of TS is a factor of the degree of object oriented programming the application requires. For the 2048 game, the data required to track the state of the game is stored as a custom defined object. The board class in TS has the structure as described below:

Please note that many of the constructs used here are not TS specific code; class, get and set were added in ES2015 as syntactic sugar on the existing prototype inheritance system.

What TS enables here is two-fold: implementation of further features available in other object-oriented languages such as C# and type checking ahead of time. This class incorporates encapsulation which is not part of JavaScript’s standard object behavior; while there are workarounds in JavaScript to get similar results, it adds significant code complexity. At multiple points, the type specificity of TS prevents erroneous code: in Redux reducers, for example, this can guarantee that the return type of the reducer will at least remain the same type after an action is handled.

(N.b. the above example most likely cannot prevent anti-patterns such as mutating the state in the reducer. For that, you may need to implement classes with private instance variables and a read-only getter method.)

The less obvious benefits are specific to React. React components uses generics when used with TypeScript, which means developers are expected to provide the shapes of props.

This ensures that each instance of your React components are provided appropriate props; if anything is missing, the application will fail to compile.

The major downside of adding TypeScript to a React project has to do with the realities of an open-source product. In my example, I gave up the initial plan to implement React Router, in large part because with the latest version 4 of React Router the available type definitions are not fully functional; with the maintainers of React Router uninterested in maintaining type definitions themselves, it is up to the open-source community to maintain type definitions, which is bound to vary in terms of quality. The rule of thumb seems that any package available for JavaScript development through NPM is going to have similar issues unless the package is in high demand (e.g. React) or the maintainers of packages use TypeScript themselves (e.g. Angular).

Notes:

  • The dispatch method of a Redux store returns the action that was passed to the method as an argument. (i.e. dispatch(A) => A) This means that state change through dispatch is theoretically a side effect. It is surprisingly easy to be confused by this since usually dispatch does not get called with an action but with the return value of an action creator. (i.e. dispatch(creator(B)) => A where creator(B) => A, so observed result A of the dispatch function looks exactly like the result of the action creator by definition.)
  • TypeScript throws errors ahead of time; this means any situation that may require error handling in runtime will still require the appropriate code, e.g. throw, try, catch, etc.
  • For purposes of Redux, it may be useful to implement different interfaces inherited from the Action interface. If you do so, you can assert your types in the reducer using as.

Conclusion:

While both Redux and TS incur an initial upfront cost in the form of additional dependencies, structural additions and developer knowledge, there are still plenty of benefits regardless of the size or scale of your application. In the case of 2048, the frequency of user inputs made it clear that separating state management was a benefit that surpassed the costs and further helped as the application took on more features.

To-Do:

You may have noticed that this write-up does not discuss performance. I have not performed extensive tests on the speed/resource aspects of the two apps in time of writing this article. The omission was in large part due to the unlikelihood of performance differences, especially in the scale and scope of the project; both versions are simple enough to be quite performant without additional optimizations.

The Redux library itself is quite lightweight in nature which leads one to suspect the overall difference should be minimal; that said, I am not going to proclaim either as truth without actual data.

Resources: