Skip to content

Kontraktor 4 React JSX

moru0011 edited this page Jan 27, 2018 · 36 revisions

React/JSX support

Kontraktor-Http comes with:

  • jnpm: npm-reimplementation for automatic download and resolvement of es6 imports
  • a JSX transpiler (for .jsx files)
  • built in bundling+minification (optional: transpilation to es-5 with integrated google clojure compiler)
  • a "live editing" / "hot reloading" feature which let's you modify .jsx files without losing application / component state

see example here

Environment / Tools

Development requires an up-to-date chrome/safari/firefox browser. I am testing with chrome dev tools, so its recommended to use chrome for development.

Requires a jdk 8, no node/npm installation is required.

Development / Production mode

In development mode, http4k does not bundle single jsx files in order to ease debugging. Transpiled files are available in a (virtual) debug folder. E.g. chrome:

As one can see, jsx code is expanded in the transpiled files.

In production mode (same as usual) http4k just inlines and minifies everything, so the example reduces to a single request (index.html) with a size of 100kb.

(J)NPM

Kontraktor provides a pure java implementation of npm. Once an import is discovered inside a .jsx file, jnpm automatically installs required libraries and their dependencies in a 'node_modules' subdirectory on demand.

differences to npm:

  • only a flat 'node_modules' directory is used/searched. Cascaded/global 'node_module' directories are not supported.
  • jnpm does not read/write a 'package.json' file. Instead it reads 'jnpm.kson' where one can place version boundaries for specific npm modules (see example project). It's possible to use npm later on as jnpm's creates exactly the same folder structure as npm.
  • in order to "make clean", just delete your 'node_module' and press refresh on the browser, jnpm will automatically download and reinstall all missing packages then.
  • jnpm never updates node modules automatically (it sticks with the version found in local 'node_modules'). See 'make clean' above on how to update you local npm modules to newest versions.

Once the first request is done to a .jsx based webapp, jnpm will try to install missing libraries found in import statements. E.g. ìmport React, {Component} from 'react' will install react to configured node_modules dir if not already present.

Example: a http4k webserver config with jsx transpilation + http+websocket based API.

Http4K.Build("localhost", 8080)
        .resourcePath("/")
            .elements("./src/main/web/client","./src/main/web/lib","./src/main/web/node_modules")
            .transpile("jsx",
                new JSXIntrinsicTranspiler(DEVMODE)
                    .configureJNPM("./src/main/web/node_modules","./src/main/web/jnpm.kson")
                    .autoJNPM(true)
            )
            .allDev(DEVMODE)
            .buildResourcePath()
        .httpAPI("/api", app)
            .serType(SerializerType.JsonNoRef)
            .setSessionTimeout(10_000) // extra low to showcase session resurrection
            .buildHttpApi()
        .websocket("/ws",app)
            .serType(SerializerType.JsonNoRef)
            .sendSid(true)
            .buildWebsocket()
        .build();

Example: jnpm.kson config

JNPMConfig {
  versionMap:
    {
      react :  "~15.6.1"
      react-dom :  "~15.6.1"
      material-ui: "~0.19.0"
    }
  repo: "http://registry.npmjs.org/"
  transformFunction: "React.createElement"
}

Hot Reloading / Live Editing

In devmode, kontraktor supports a blazing fast live editing feature for react apps. A downside of this is, that breakpoints done from browser's debug tools won't work anymore. Use the debugger; statement (via live reloading) to workaround this (AKA: write 'debugger;' where your want to debug, then save for live-reload).

The following limits apply to kontraktor-http's hot reloading

  • only top level definitions can be live reloaded (classes, functions, objects)
  • Live Editing is enabled only for application files (code in node_modules directory is not instrumented).
  • top level object structures are overwritten, if you want to keep module-local application state, mark objects/functions by setting `statefulObject._kNoHMR=true'.
  • need to use 'debugger;' statement to set breakpoints
  • a module is re-executed on hot reload. If you do initial stuff (set-up network connections, init your store) in a module's init code, ensure this kind of stuff is not re-executed on hot-reload by testing if ( typeof _kHMR === 'undefined') { // code here is executed once, not on module-hot-reload}.
  • a new import statement does not require a full refresh if it has been loaded in your app before (So adding a function to a loaded module and importing it from another module works, creating a completely new .jsx file requries a full refresh).
  • use React 16's ErrorBoundaries in order to avoid "blank screen" in case you 'hot-reload' an error destroying your component tree (React 16 only). Simply add componentDidCatch(error, info) {console.log(error, info);} to your major container components.
  • static methods of classes are not hot reloaded currently

In order to enable hot-reload:

  • enable it on the transpiler .hmr(DEVMODE)
  • publish kontraktors devmode only filewatcher with .hmrServer(DEVMODE)
  • Important: In order to avoid losing state by redefining your main app with each reload, put your root render statement in a hot-reload detection condition like:
if ( typeof _kHMR === 'undefined' ) { // only load once, not when doing hot reloading
  global.app = <App/>;
  // required for hot reloading
  window._kreactapprender = ReactDOM.render(global.app,document.getElementById("root"));
}
  • set global window._kreactapprender to the result of ReactDOM.render (see above).
  • add import {KClient} from 'kontraktor-client statement to your index.jsx

Currying ('HOCs') fails in Hot Reload Mode

Unfortunately currently function scope is lost upon patching, with the following

function createTextEditor(width) {
  return (value,rowcfg,rowdata,props) => {
    return <Form><TextArea style={{width: width}} value={value}/></Form>;
  }
}

export const editText = createTextEditor(400);

you'll get an 'width is undefined' error. To workaround this, add

createTextEditor._kNoHMR = true;
editText._kNoHMR = true;

This way the code will execute without error, however live editing won't work on those functions.

Limits of ES6 Import emulation

Browser's do not yet implement ES6 import's and even then, without broad adaption of http 2.0 packaging and bundling will be required for web apps (to many sync http-get requests).

The intrinsic jsx transpiler only supports a subset (sufficient for frontend apps) of the official spec.

Namely:

  • The export keyword is ignored, instead kontraktor automatically exports all top level objects of a .jsx file.
  • default exports are supported
  • the * operator in imports is not supported

Example for import/export syntaxes understood by kontraktor-jsx

import { HashRouter, Route, Switch, Redirect } from 'react-router-dom'
import { AppActions,Store as AppStore } from "./store.jsx";
import React, {Component} from 'react';
export class AppStore extends EventEmitter { .. }
export default FeedsState;

In addition, the jsx parser does not provide too detailed error messages, so its recommended to use a jsx validating IDE for editing (e.g. intellij).

ES6

In development mode, kontraktor relies on es-6 browser support (e.g. chrome, firefox, ..), there are some exceptions:

  • object spread polyfill (really important for react) can be turned off (in case browsers implement it)
    in JSXParser.SHIM_OBJ_SPREAD. Note '...' is only polyfilled for objects not arrays or argument lists.
  • For all ES6 features beyond object spread and import/export you rely on the browser implementation (e.g. ...rest) in development mode, for production use clojure compiler integration to transpile down to es5.

The code for klibmap lookup and spread polyfill can be tweaked by overriding JSXIntrinsicTranspiler.getInitialShims().

Build / Production mode

When running in production mode, first request triggers bundling. The result is cached in memory, so changes to sources won't apply.

As this might be error prone (e.g. jnpm suddenly fetcing missing npm modules or so), its possible to generate a single bundle by setting a "distribution" directory in the 'resourcepath' builder.

   // (PRODMODE only: look (and create if not present) static build artefact for budled index.html [avoids rebundling on first request in prodmode]
   // Warning: you need to delete this file in order to force a rebuild then
   .productionBuildDir(new File("./dist") )

If a prebuilt bundle is present, kontraktor-http will deliver this one instead of building one (in production mode). If the bundle is not present, kontraktor-http will generate and save it.

Transpiling to ES5 in production

Add .jsPostProcessors(new ClojureJSPostProcessor()) // uses google clojure transpiler to ES5 (PRODMODE only) to resourcepath builder. By default kontraktor-http will just bundle and minify, so relies on ES6 support of browser then (fine with newer chrome/firefox/safari/edge).

Configuring Http4k

The following builder example configures:

  • a resourcepath pointing to the 'classpath' of your react/js frontend.
  • registers a transpiler for .jsx files
  • defines two communication endpoints to let you app talk to the java backend a) http long-poll b) websockets
  • sets up a 'distribution' directory to get a statically saved build
  • enables on-demand jnpm module installation (devmode)
  • enables hot-reload (attention: some tweaks to app code reqired, see above)
  • sets a session timeout for kontraktors built in actor-per client session handling
Http4K.Build("localhost", 8080)
    .resourcePath("/")
        .elements("./src/main/web/client","./src/main/web/lib","./src/main/web/node_modules")
        .transpile("jsx",
            new JSXIntrinsicTranspiler(DEVMODE)
                .configureJNPM("./src/main/web/node_modules","./src/main/web/jnpm.kson")
                .autoJNPM(true)
                .hmr(DEVMODE) // support required for hot reloading
        )
        .allDev(DEVMODE)
        .jsPostProcessors(new ClojureJSPostProcessor()) // uses google clojure transpiler to ES5 (PRODMODE only)
        // (PRODMODE only: look (and create if not present) static build artefact for budled index.html [avoids rebundling on first request in prodmode]
        // Warning: you need to delete this file in order to force a rebuild then
        .productionBuildDir(new File("./dist") )
        .buildResourcePath()
    .httpAPI("/api", app)
        .serType(SerializerType.JsonNoRef)
        .setSessionTimeout(10_000) // extra low to showcase session resurrection
        .buildHttpApi()
    .websocket("/ws",app)
        .serType(SerializerType.JsonNoRef)
        .sendSid(true)
        .buildWebsocket()
    .hmrServer(DEVMODE) // hot reloading file tracking server
    .build();

adding non-npm packages / libraries: klibmap

Recommendation: If one makes use of kontraktor's "java-npm" implementation (or directly uses npm), definition of "klibmap" is superfluous.

Kontraktor-jsx supports a global map of [library file name] => () => libraryObject in order to "fake" imported library objects.

For javascript libraries exporting objects to global window object, it might be required to construct a 'virtual' library object.

Example (see 'events' entry in klibmap):

<!doctype html>
<html lang="" style="height: 100%;">

<head>
...
</head>

<body>

  <div id="root" style="align-items: stretch; width: 100%;"></div>

  <!-- manual import as es6 imports are shimmed for jsx files only -->
  <script src="kontraktor-common.js"></script>
  <script src="kontraktor-client.js"></script>

  <script>
    // in order to make UMD builds work with 'import' we need a map of library filename to library Object,
    // as libs arent loaded yet, provide lambdas
    klibmap = {
      react: () => React,
      history: () => History,
      reactRouterDom: () => ReactRouterDOM,
      events: () => { return {  // events exposes global objects, so we need to construct a virtual lib object
        EventEmitter: EventEmitter,
      }}
    };
  </script>

  <script src="index.jsx"></script>
  <!-- take care: in dev mode index.jsx is loaded deferred (side effect of document.write -->
</body>

</html>

an import statement like import {Component} from 'react.js' then simply looks up klibmap['react']['Component']. Note lib name is taken without the .js extension. The polyfill still provides proper scope encapsulation: No definitions are leaked to the browsers window object,

Disclaimer: I am only testing under Linux, so there might be path related issues on wind or mac. Pls report.