Share: Follow:

One of the most promising ways to advance your skills in the language is to read the code of mid-sized or large-sized project. And React.js codebase is a great project to start with. It is written by competent JavaScript developers, well-tested and not foreign in a sense that it incorporates patterns native for JavaScript only.

In this article I want to highlight one brilliant pattern called Transactions in React.js. It has many possible applications (like most design patterns do) - from keeping invariants to managing cross-cutting concerns. Also an implementation is very simple and I’d like to dissect it. I’ll also tell you how it is used in React codebase.

Problem

There are certain concerns in your applications that cross-cuts the whole app. No matter how well your application are going to be modularized, you’ll see this concern in many places. Those concerns are usually generic things your app - like authentication, authorization or logging.

An easy example would be some kind of profiling of a piece of code. You’d like to know what is an average time of running a given piece of code - and maybe do something with it afterwards, like generating a report for developers. You can do it with the following piece of code:

import { profilingStatsOf , updateStats } from ' profiling-database ' ; function makeSomethingCritical () { /* Initialize steps */ var profilingStats = profilingStatsOf ( ' makeSomethingCritical ' ); var beforeTimestamp = new Date (); // ... do lengthy stuff /* Postcondition (`close` step) */ var afterTimestamp = new Date (); updateStats ( profilingStats , afterTimestamp . getTime () - beforeTimestamp . getTime ()); }

There are also pieces of code for which you’d like to keep your initialize and postconditions intact.

Let’s take an example of manipulating some complicated sales report. You’d like to make sophisticated computations mutate this sales report, entering new values to it. But you also need to be sure that total dollars earned is above zero. If not, you’d like to restore the previous version of this sales report:

var salesReport = { // ... totalDollarsEarned : 782 }; function calculateStateAfterRiskyInvestments ( salesReport ) { var savedOldReport = JSON . load ( JSON . stringify ( salesReport )); /* Cloning! (`initialize` step) */ /* Complicated manipulation of many fields of the sales report... */ /* Postcondition */ if ( salesReport . totalDollarsEarned < 0 ) { Object . assign ( salesReport , savedOldReport ); /* Restoration of fields (`close` step). */ } }

This pattern of conditional save-and-restore may happen in many functions, not just only one (you can have many variants of such mutations). Of course you can create a function which takes a risky investments calculation function over sales report as an input (a.k.a. higher order function). But this way you’ll force returning values from calculation functions and those rules must be set by the module you’re currently in. If you need a dynamic set of initialize steps and postconditions not defined by your module it’d be suboptimal at least. The same with a profiling example - if it’d need to be called from a different module and dynamic (so you can plug it on or off) that’d be a problem with an approach like that.

Transaction pattern will help you in such cases. It allows you to define a dynamic set of preconditions (or initialize steps) and postconditions (or close steps) and run your function within such transaction. You can then export such transaction from another function and re-use it thorough modules without exposing implementation details to other modules.

Transaction pattern - an overview

The Transaction pattern as implemented in React has one more implicit precondition - it disallows you to call a transaction if the one is running already. It is often important in an asynchronous world of JavaScript - and since React.js uses transactions internally during reconcilliation process it was a wise choice for them to add this precondition.

The transaction API consists of following pieces:

getTransactionWrappers - this is an abstract function which returns list of objects with two fields which are their methods: initialize and close . initialize are our precondition or initialize step - you can use it both to set the scene for close . close is your close step or postcondition - you have an access to the return value of the initialize method. Both of those fields are optional.

isInTransaction - checks whether another transaction is running or not.

perform - this is where you call your function. You can supply scope and arguments to it. It is very similar to calling the function using call . Even if the function or one of initialize step throws, all close steps will be ran no matter what.

reinitializeTransaction - an optional step of cleaning up all data that has been stored by initialize steps. This can also be used to prepare internal data structure (a list for storing initialize return values) to be used. I have no idea why React.js kept such inelegant solution, but I describe the pattern as-is.

There is also a complication with handling exceptions in initialize steps, performed function and close steps. Here’s how it is done:

If initialize step throws, performed function won’t be called but all other initialize steps will get called. Also all close steps for initialize steps which aren’t errorneous will get called. The first exception will be thrown outside the transaction.

If performed function throws, close steps will get called and the exception will be thrown outside the transaction.

If any of close steps throws, rest will get called and the first raised exception will be thrown outside the transaction.

In React.js implementation, the Transaction.Mixin is exposed to be used with prototype cloning technique of creating Transaction objects. The general creation of the transaction looks like this:

var Transaction = require ( ' shared/utils/Transaction ' ); var MY_TRANSACTION_WRAPPERS = [ { initialize : function () { /* ... */ }, close : function () { /* ... */ } }, // ... ]; function MyTransaction () { this . reinitializeTransaction (); } Object . assign ({}, MyTransaction . prototype , Transaction . Mixin , { getTransactionWrappers : function () { return MY_TRANSACTION_WRAPPERS ; } } ); module . exports = MyTransaction ;

So in another piece of code code you can use it like this:

var MyTransaction = require ( ' MyTransaction ' ); var transaction = new MyTransaction (); transaction . perform ( myFunction , contextObject , 1 , 2 , 3 , 4 );

Implementation dissected

You know now how to use Transactions and what is the general idea of it. It is wise to look at the code. While it is not the cleanest piece of code I’ve ever seen, I understand the rationale of keeping it that way and it is covered by good tests. So let’s dive into implementation and see how it’s done.

The full code, stripped from comments looks like this:

' use strict ' ; var invariant = require ( ' invariant ' ); var Mixin = { reinitializeTransaction : function () { this . transactionWrappers = this . getTransactionWrappers (); if ( this . wrapperInitData ) { this . wrapperInitData . length = 0 ; } else { this . wrapperInitData = []; } this . _isInTransaction = false ; }, _isInTransaction : false , getTransactionWrappers : null , isInTransaction : function () { return !! this . _isInTransaction ; }, perform : function ( method , scope , a , b , c , d , e , f ) { invariant ( ! this . isInTransaction (), ' Transaction.perform(...): Cannot initialize a transaction when there ' + ' is already an outstanding transaction. ' ); var errorThrown ; var ret ; try { this . _isInTransaction = true ; errorThrown = true ; this . initializeAll ( 0 ); ret = method . call ( scope , a , b , c , d , e , f ); errorThrown = false ; } finally { try { if ( errorThrown ) { try { this . closeAll ( 0 ); } catch ( err ) {} } else { this . closeAll ( 0 ); } } finally { this . _isInTransaction = false ; } } return ret ; }, initializeAll : function ( startIndex ) { var transactionWrappers = this . transactionWrappers ; for ( var i = startIndex ; i < transactionWrappers . length ; i ++ ) { var wrapper = transactionWrappers [ i ]; try { this . wrapperInitData [ i ] = Transaction . OBSERVED_ERROR ; this . wrapperInitData [ i ] = wrapper . initialize ? wrapper . initialize . call ( this ) : null ; } finally { if ( this . wrapperInitData [ i ] === Transaction . OBSERVED_ERROR ) { try { this . initializeAll ( i + 1 ); } catch ( err ) { } } } } }, closeAll : function ( startIndex ) { invariant ( this . isInTransaction (), ' Transaction.closeAll(): Cannot close transaction when none are open. ' ); var transactionWrappers = this . transactionWrappers ; for ( var i = startIndex ; i < transactionWrappers . length ; i ++ ) { var wrapper = transactionWrappers [ i ]; var initData = this . wrapperInitData [ i ]; var errorThrown ; try { errorThrown = true ; if ( initData !== Transaction . OBSERVED_ERROR && wrapper . close ) { wrapper . close . call ( this , initData ); } errorThrown = false ; } finally { if ( errorThrown ) { try { this . closeAll ( i + 1 ); } catch ( e ) { } } } } this . wrapperInitData . length = 0 ; }, }; var Transaction = { Mixin : Mixin , OBSERVED_ERROR : {}, }; module . exports = Transaction ;

As you can see the Transaction behavior is contained within Mixin and there is a constant called OBSERVED_ERROR which is used internally in the implementation - more about that later.

There is also a module called invariant required. It is has a signature like this:

invariant ( condition , notPassMessage )

So if condition is false , React.js will throw with a given message. It is like assert in programs written in C family of languages.

perform

Let’s start with the heart of the transaction - a perform method:

perform : function ( method , scope , a , b , c , d , e , f ) {

This is a very interesting thing - since React developers don’t need to pass more than six variables to the performed function they’ve decided to just limit the number of variables passed in to six. It can be fixed by using arguments pseudo-Array object available within every function and and replacing call with apply inside the code. But since React devs don’t need it, they’ve decided to keep it that way.

invariant ( ! this . isInTransaction (), ' Transaction.perform(...): Cannot initialize a transaction when there ' + ' is already an outstanding transaction. ' );

First of all, there is a check of an invariant about having only one function performed in a transaction at a time.

var errorThrown ; var ret ; try { this . _isInTransaction = true ; errorThrown = true ; this . initializeAll ( 0 ); ret = method . call ( scope , a , b , c , d , e , f ); errorThrown = false ; }

In this piece of code the first thing done is “locking” the transaction - after this line of code another perform can’t be made.

Then, there is quite an inelegant approach used - errorThrown local variable is used to track whether the next two following instructions threw or not. The idea is simple - since throwing an exception is breaking the execution flow if it is the case the errorThrown = false instruction won’t get reached at all. It can be then checked later whether errorThrown remained true to be sure that this piece of code threw an exception.

Then all initialize steps are called. Since as you will see later there is a small bit of recursion used in initializeAll so there is 0 passed as an argument as a starting value for iteration over an array of initialize steps. Details will be explained later.

Then, the performed function gets called with a given scope and arguments listed.

The next part is where it gets interesting:

finally { try { if ( errorThrown ) { try { this . closeAll ( 0 ); } catch ( err ) {} } else { this . closeAll ( 0 ); } } finally { this . _isInTransaction = false ; } }

This ugly double- finally piece of code is making a decision based on whether initialize steps or performed function threw. If any of these threw you don’t care whether any of close steps will throw or not because what is wanted is the first exception. Thus if initialize steps or performed function threw, all close steps exceptions are swallowed.

If it is not the case close steps get called normally. A method calling all close steps in order is made in a similar fashion to this.initializeAll so it also needs an argument of 0 .

The last finally block is for ensuring that no matter what after perform is finished the transaction is ready to be used again - so it “unlocks” it by setting this._isInTransaction to false .

The last line is returning the result of the performed function:

return ret ;

initializeAll & closeAll

initializeAll and closeAll methods are written in a similar fashion with a little twist, so here the initializeAll will be discussed in detail.

There is a design flaw in this implementation - initializeAll and closeAll are publicly visible. While React.js developers are disciplined in not using it thorough the codebase explicitly, it is still a design flaw easily fixed by providing a factory method. Reasons behind it remains unclear - it can be a clumsiness of the implementator.

initializeAll : function ( startIndex ) {

The signature looks like initializeAll is recurrent - there is startIndex passed as an argument. And indeed the recursion is used here - but not fully since it’d be inefficient (JavaScript do not support tail-call optimalization in most environments).

The next part is the iteration over all initialize steps:

var transactionWrappers = this . transactionWrappers ; for ( var i = startIndex ; i < transactionWrappers . length ; i ++ ) { var wrapper = transactionWrappers [ i ]; try { this . wrapperInitData [ i ] = Transaction . OBSERVED_ERROR ; this . wrapperInitData [ i ] = wrapper . initialize ? wrapper . initialize . call ( this ) : null ; } // ... this part discussed later }

There is a convenience assignment to avoid typing this.transactionWrappers all the time. It’d be easily replaced in ES2015 with const { transactionWrappers } = this; and it’d express the intent in a better way.

Then the iteration starts. It is started beginning from startIndex all the way to the end of transactionWrappers list.

The same trick with an exception flow is used here, but with a little more sophisticated implementation. Since there is a necessity to omit close steps of the corresponding initialize steps that threw, the initial value of returned data of i -th initialize step is a special value called OBSERVED_ERROR . It indicates that this step threw. Then the initialize step is called if it exists. Otherwise the return value of the step is set to null .

Please notice if a given initialize step throws, the last assignment won’t be finished so the value will stay as Transaction.OBSERVED_ERROR .

Let’s see what is the remainder of this try block:

finally { if ( this . wrapperInitData [ i ] === Transaction . OBSERVED_ERROR ) { try { this . initializeAll ( i + 1 ); } catch ( err ) {} } }

Here it is checked whether the i -th initialize step threw. If it is the case, the recursion style is used. this.initializeAll is called with a starting value of i + 1 , meaning “start this method with the next initialize step after the one which threw”. All further exceptions get swallowed since there is a requirement that on the outside the first exception should be visible.

The closeAll is made in a similar fashion with two differences. First of all, the checking of the existence of close in a given transaction wrapper is made like this:

var initData = this . wrapperInitData [ i ]; if ( initData !== Transaction . OBSERVED_ERROR && wrapper . close ) { wrapper . close . call ( this , initData ); }

So there is also a check if a corresponding initialize step threw. If it’s the case the close step call is omitted.

The checking for exception is also done differently. There is errorThrown variable and it is made in a similar fashion to the solution within a perform method.

The last part is “cleaning up” the wrapperInitData list made in a following way:

this . wrapperInitData . length = 0 ;

I discourage you of using the similar solution. If you find a use case like this it’d be better to store all exceptions threwn in a collection and just return the first one (or all of them if you want). You’ll avoid excessive if statements and local variables like errorThrown . But there is also a concern of conserving memory and possible memory leaks - so this implementation may be better in a general-use library like React.

reinitializeTransaction

The last interesting part is reinitializeTransaction . It is a method which in fact is both initializer and reinitializer. This is also a bad design in my opinion. It’d be much better to provide a factory function. Even if you follow the implementation and don’t provide one, it’s still a bad name for a method like this. But let’s see what is the implementation:

reinitializeTransaction : function () { this . transactionWrappers = this . getTransactionWrappers (); if ( this . wrapperInitData ) { this . wrapperInitData . length = 0 ; } else { this . wrapperInitData = []; } this . _isInTransaction = false ; },

So what this method does is setting this.transactionWrappers to a result of getTransactionWrappers which is provided by the concrete implementation of the Transaction . It is quite neat solution considering you can make this function quite sophisticated by making it a bound function. Then, there is a check whether this.wrapperInitData is already defined. If it is the case it gets cleared. This should be redundant if the Transaction API is used properly - closeAll handles that for you. But if this.wrapperInitData is empty, it gets initialized as an empty data. Also the transaction gets “unlocked”. Steps of cleaning up wrapper init data and the transaction lock should generally never be needed - it’s a double-check of invariants of these transactions. But if the Transaction API gets used in an extremely bad way due to a design flaw (like calling initializeAll without perform ) it may be handy.

This is all major parts of this implementation. Apart from the rather ugly trick of getting only the first exception bubbled it is quite good code. It uses OOP patterns native to JavaScript to achieve the goal which is also good. What I’d praise is the method of constructing your own transactions - it is a great, native JavaScript way of doing things. If you’d need to choose what should be learned from this implementation, it is certainly this pattern - it’ll get in handy in many places!

How is it used in React.js?

In React.js Transaction is used in many places in code. In the codebase there are several renderers defined - like a renderer for server-side rendering, renderer for browser (DOM) environment and so on. Each of those renderers needs to keep their invariants. Let’s take an example of DOM renderer. In ReactReconcileTransaction there is a list of transaction wrappers which looks like this:

var TRANSACTION_WRAPPERS = [ SELECTION_RESTORATION , EVENT_SUPPRESSION , ON_DOM_READY_QUEUEING , ];

This transaction is used during the reconcillation phase of React.js lifecycle - so between updates. It takes care of not losing input after excessive changes, suppressing events during the whole process (like blur events or events which may be caused by, for example, Video APIs) and enqueing certain parts of React lifecycle on situations where updated DOM is ready - one of such situation is calling componentDidUpdate .

Maintaining those things would be hard without a mechanism similar to transactions. Just think about maintaining selections - there is a daunting amount of possibilities of DOM mutations that may cause changing the focus of an input. With transaction it is trivial to keep this invariant in place.

Summary

The Transaction may be a great addition to design patterns of your application. While I find the implementation a little clumsy, it is used with a success in the codebase of React.js which is a relatively big piece of software. It is a mechanism very similar to before and after advice in a programming technique called aspect-oriented programming. Using it can make maintaining your application invariants easier - especially when you are making operations with a lot of side effects like object or DOM mutations (just what React.js does).

Share: Follow:

Please enable JavaScript to view the comments powered by Disqus.

Disqus