sketchboard blog

How to use Vue.js with JSX and TypeScript - Migrating from AngularJS to Vue.js

Never being in a situation where some framework melts down under you, and there is no simple path to upgrade it. Sketchboard, like many other projects, needs to make a decision how to move forward from AngularJS (Angular 1.x). It doesn’t make any easier that JavaScript front-end libraries/frameworks come and go very quickly. And sometimes it is like in the wild wild west.

Using Vue.js with JSX and TypeScript

For a while, we have built UI features using microservices outside the core Sketchboard application and postponed the decision. That is still ongoing and a valid plan. We chose to use React, JSX, TypeScript with Redux. Approach with microservices works well for features that are not part of the core functionality and can be on the administrative side of the application. This has given time to experiment and understand new UI toolkits/frameworks before making the decision how to move forward with the core Sketchboard app on the front end side.

React, JSX and TypeScript give you a great developer experience (that is subjective). You get to navigate using VSCode from HTML to TypeScript code and have a strong static type support like renaming possibilities and tool to show the places where API has changed. For us, it gives an easier entry for maintenance when you come back to the code later. That is the real test, how easy it is to make changes and add new features later.

We are pretty happy with the React, JSX and TypeScript combination, though it was a surprise how big a JavaScript file size becomes over time. We wanted to look if there would be a smaller alternative with some additional requirements.

For the administrative side of the application, it is not that crucial how fast complementary side of the application loads, but it matters a lot how fast your core application loads. The first experience is really important.

Our wishes for the UI library

  • Framework/library size should be relatively small to work side by side with AngularJS, hoping to get less than 100KB as minified. Letting us gradually migrate legacy AngularJS code feature by feature to Vue.
  • Load components dynamically, not to increase Sketchboard initial load time. Over time there will be lots of new functionality.
  • Work side by side with AngularJS router
  • Improve overall frontend performance, since we have some UI performance challenges with AngularJS
  • Support TypeScript
  • Suberb developer experience (subjective for our taste)
  • Straightforward to refactor legacy AngularJS code

React, JSX, TypeScript, and Redux set the bar quite high. Developer experience is excellent, and we wanted to have something like that, a simple, straightforward view layer, with router and state support. Angular 4 is probably a good candidate, but the development experience is very different than with React + JSX. Also, the framework file size is biggish, though it diminished in Angular 4. Angular 2 was about 566 KB, and some sources say that Angular 4 is about half of it.

Evaluating Vue.js

While getting used to writing React application, we decided to look Vue.js that also offers JSX support having only 58.8K file size on Vue version 2.4.2. That is a very tempting file size. Developer context switch between React + JSX to Vue.js + JSX wouldn’t be that big either. Both are written in TypeScript.

We also have a straightforward path to migrate Vue.js code to React after the evaluation. Latest news from React 16 is that the file size has reduced significantly and there are performance improvements. React + react-dom is 109 kb (34.8 kb gzipped).

Eventually, some features need to be in the core application. We balanced out whether our move board to a team feature is more on the administrative side of the app or a core functionality, in the end, we decided to build it as part of Sketchboard core application, since utility functionality related to the feature was already built on the core Sketchboard backend. The feature is also small enough to evaluate Vue.js.

In addition we have written a Vue.js, JSX and TypeScript Example Application. Full source code can be found on GitHub.

To be honest, we are looking for React, but a little bit smaller and easier to load parts dynamically.

Vue.js looked that it could provide most of these, but it wasn’t that straightforward, and clearly, it doesn’t have that big community. It is harder to find Vue.js resources especially for JSX with TypeScript. Some work was needed to get the experience close to what React has.

Vue.js Pros

  • Lightweight
  • Router and state libraries are recommended by the framework and are designed to work together
  • Vue router supports out of the box dynamic component loading with webpack 2 code splitting
  • Vuex supports dynamic state loading and adding new modules on runtime

Vue.js Cons

  • JSX and TypeScript support has not taken that far, since those are second-class citizens, compared to Vue.js templates and Vue.js DSL
    • Vue.js has TypeScript typings, but lacks e.g. typings for HTML elements. Static type checking for component properties is also missing, and now we are using our custom thin layer to make Vue TypeScript Components to get the same type checking for component properties as React has.
  • The Vue.js community is smaller than React, and there are less 3rd party libraries available. But more comes out of the box, like the bundled router and state management.

Vuex

Vuex is also an excellent piece of software. It allows keeping the core application size very small when store modules can be added at runtime. With the help of Webpack code splitting, state-related functionality is also bundled as part of UI component javascript file.

You can get type support by using generics with the dispatch and commit functions.

First, we defined dispatch types. Note that we maintain the type constant twice. You could define the type as a string, but then the compiler doesn’t complain if the wrong type is used on dispatch.

type DispatchBoardId = {
  type: 'GET_PUBLISH_BOARD'
  boardId: string
}

export class Actions {
  static readonly GET_PUBLISH_BOARD = 'GET_PUBLISH_BOARD'
}

Type can be used on the client side with the dispatch function.

this.$store.dispatch<publishTypes.DispatchBoardId>({
  type: publishTypes.Actions.GET_PUBLISH_BOARD,
  boardId: currentBoard().boardId,
})

Getting TypeScript support for getters is not that convenient. We ended up having a separate interface. The downside is that we need to maintain getters tree names and interface function names in sync manually.

interface PublishGetters {
  publishIsFetching: () => boolean
}

const getters: GetterTree<PublishState, any> = {

  publishIsFetching: (state) => () => {
    return state.getPublishState.fetching ||
          state.unpublishState.fetching ||
          state.updatePublishState.fetching
  }

}

Client-side getters usage is more convenient compared to defining them, and you get to use full type checking support.

render() {
  const {publishIsFetching} = this.$store.getters as PublishGetters
}

Vue and React differences

We can write code pretty much in the same way as with React. The biggest difference is how to clue state with UI components and boilerplate code related to Redux. Vuex can use dynamic state modules, read more details on Redux vs. Vuex.

React is more mature. It also supports JSX as a first class citizen and TypeScript support is excellent, one example is how to reference child components through a parent component.

React way needs less boilerplate and is type safe.

class ChildComponent {

}

class ParentComponent {
  child: ChildComponent

  render() {
    <ChildComponent ref={c => this.child = c}>
  }
}

Vue.js way way needs more boilerplate, and you need to support type safety by your self.

class ChildComponent {

}

class ParentComponent {
  getChild(): ChildComponent {
    return this.refs.child as ChildComponent
  }

  render() {
    <ChildComponent ref='child' />
  }
}

One comparison for community size is, how easy it is to find out coding examples from stack overflow. There are more than 130,000 React related questions compared to over 14,000 results for Vue.js.

React licensing can cause dilemma for some folks. Vue.js is MIT and React is BSD with patents. This is deprecated information since React is now available with MIT license and without patents clause.

Migrating AngularJS code to Vue JSX

The process is quite straightforward. First, copy paste AngularJS HTML template under render() function. VSCode shows if there are any tag miss matches. All elements need to be closed e.g.

<input>
<!-- needs to be -->
<input />

In some situations JSX behaves differently compared to HTML, especially if you use <pre> tags, you can use HTML to JSX Compiler for the conversion.

Next format HTML in VSCode to make it more readable.

Find all places that use ng-show, ng-if or ng-model, and replace those with Vuex state or Vue.js component state. You can even keep legacy angular attributes to validate that all places are migrated correctly.

{!getPublishState.failed && !getPublishState.publishedUrlHttps &&
<div ng-show="!failed && !publishedUrlHttps">
  Press publish to embed board on an html page.
</div>
}

Code Splitting with Webpack 2

Webpack 2 code splitting is a very powerful feature. It allows you to load Vue.js components and state dynamically when needed.

Vue router also supports dynamic component loading out of the box when a path changes. Following code snippet is all that is needed.

const MoveBoard = () => import('../component/MoveBoard')
const PublishBoard = () => import('../component/PublishBoard')

export default new VueRouter({
  mode: 'hash',
  routes: [
    {
      path: '/move',
      name: 'move-board',
      component: MoveBoard
    },
    {
      path: '/publish',
      name: 'publish-board',
      component: PublishBoard
    },
    {
      path: '/',
      name: 'root'
    }
  ]
})

Babel is needed to support code splitting with TypeScript.

module: {
  rules: [
    test: /\.tsx?$/,
    exclude: /node_modules/,
    use: {
      loader: 'awesome-typescript-loader',
      options: {
        useBabel: true,
      },
    }
  ]
}

Your .babelrc would look like following.

{
  "presets": [
    "env"
  ],
  "plugins": [
    "transform-vue-jsx",
    "syntax-dynamic-import"
  ]
}

You can write the component without thinking dynamic loading. Webpack 2 includes dependencies in the bundle.

Code Splitting for Core JavaScript Files

Code Splitting for Core JavaScript Files Structure

We wanted to have core Vue.js library to be part of the vuevendor.js JavaScript file since it will not change very often. Also, we have a separate Vue application (vueapp.js) to include initialization of Vue and Vue router.

Manifest.js contains logic to load dynamic components. In this way, vuevendor.js stays as is until a new Vue.js version is used. The vueapp.js changes when new dynamic components are added to the router, and manifest.js changes when a dynamic component bundles are changed, or new ones are added.

<script src="manifest.js" data-attr="src"></script>
<script src="vuevendor.js" data-attr="src"></script>
<script src="vueapp.js" data-attr="src"></script>

webpack.config.js:


entry: {
  vueapp: './src/vueapp/index.ts'
},

output: {
  path: path.join(basePath, 'src/main/webapp/static/resources-static-1/js/app/typescript/dist'),
  filename: '[name].js',
  chunkFilename: '[name].[chunkhash].bundle.js',
},

plugins: [
  new webpack.optimize.CommonsChunkPlugin({
    // libraries
    name: "vuevendor",
    minChunks: function (module) {
      // this assumes your vendor imports exist in the node_modules directory
      return module.context && module.context.indexOf("node_modules") !== -1;
    }
  }),

  // webpack's bootstrap and module manifest.
  new webpack.optimize.CommonsChunkPlugin({
    name: 'manifest'
  })        
]

We set the path where dynamic components are loaded at runtime. The value depends on the environment. On development environment chunk bundles are loaded directly from a ‘/static’ directory, and on a production environment, dynamic components are loaded through CDN.

__webpack_public_path__ = _config.publicPath

CDN caching would cause problems after a component has been changed if the file name remains the same. To make sure that your recent changes get loaded correctly, use a hash value in the bundle name.

chunkFilename: '[name].[chunkhash].bundle.js',

Summary

It is a joy to write Vue.js applications with JSX and TypeScript. Boilerplate code is not needed due to Vuex and Vue Router are available out of the box. Vuex made a positive impact on how easy it is to write modular states with dynamic state loading.

Vue.js lacks some support for JSX and TypeScript. Potentially support will be better in the future if more users start to adapt JSX and TypeScript.

The migration path from AngularJS to Vue.js is straightforward and can be done step by step without rewriting a complete UI at once, and those work nicely together.

In the light of React 16 improvements and new licensing model, React maybe the way how core UI features will be implemented in Sketchboard, due to community size and JSX being a first-class citizen. We need to figure out a good way to reduce Redux boilerplate code and adding new states dynamically; we’ll be prototyping that in the future.