Skip to content

akryl-kt/akryl-core

Repository files navigation

Akryl

Kotlin wrapper around ReactJS.

With akryl, you can write simple and idiomatic Kotlin code that will be converted into React components. For example, you can rewrite this JavaScript code:

const App = ({name}) => {
    return <div>Hello, {name}!</div>;
}

in Kotlin using akryl:

fun App(name: String) = component {
    div(text = "Hello, $name!")
}

Install

There are two options to install Akryl:

  • Starter project - a convenient way for beginners.
  • Manual install - if you want full control over the project.

Starter Project

If you don't want to set up the project yourself, you can clone akryl-frontend-starter repository:

git clone https://github.com/akryl-kt/akryl-frontend-starter
cd akryl-frontend-starter
./run.sh
# open http://localhost:8080 when build completes

Manual Install

To add Akryl to an existing project, follow the steps below. This instruction assumes that you already have a Kotlin/JS project configured with Gradle.

Akryl library consists of several pieces:

In most cases, you need to install all of them.

  1. Add jcenter repository:
repositories {
    jcenter()
    ...
}
  1. Add these dependencies into your build.gradle file:
kotlin {
    sourceSets {
        main {
            dependencies {
                ...
                // kotlin dependencies
                implementation "io.akryl:akryl-core:0.+"
                implementation "io.akryl:akryl-dom:0.+"
                implementation "io.akryl:akryl-redux:0.+"

                // babel-plugin-akryl dependencies
                implementation npm("babel-loader", "8.0.6")
                implementation npm("@babel/core", "7.7.7")
                implementation npm("@babel/preset-env", "7.7.7")
                implementation npm("babel-plugin-akryl", "0.1.1")
                // react dependencies
                implementation npm("react", "16.12.0")
                implementation npm("react-dom", "16.12.0")
                implementation npm("redux", "4.0.5")
                implementation npm("react-redux", "7.1.3")
            }
        }
    }
}
  1. Add babel.js file into webpack.config.d directory to load babel-plugin-akryl:
config.module.rules.push({
  test: /\.m?js$/,
  exclude: /(node_modules|bower_components|packages_imported)/,
  use: {
    loader: 'babel-loader',
    options: {
      presets: ['@babel/preset-env'],
      plugins: [
        ['babel-plugin-akryl', {production: !config.devServer}],
      ],
    }
  }
});
  1. Add src/main/resources/index.html file:
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Akryl Frontend Starter</title>
    <link rel="stylesheet" href="index.css">
</head>
<body>
    <div id="app"></div>
    <script type="text/javascript" language="JavaScript" src="<jour-project-name>.js"></script>
</body>
</html>
  1. Add src/main/kotlin/App.kt file:
import io.akryl.component
import io.akryl.dom.html.Div
import react_dom.ReactDom
import kotlin.browser.document

fun app() = component {
  div(text = "Hello, World!")
}

fun main() {
  ReactDom.render(app(), document.getElementById("app"))
}
  1. Run the project:
./gradlew run --continuous

Documentation

akryl-core provides basic React features:

  • Components
  • Context
  • Hooks

To use JSX-like syntax you need to install akryl-dom library.

Components

With Akryl you can use only hooks API and functional components. To create a component you must define a function, that takes props as arguments and returns a virtual DOM element. It is important to wrap the function body into a component function call. It tells the babel-plugin-akryl to convert a simple Kotlin function into a React component.

Example:

fun Calc(a: Int, b: Int) = component {
    div(text = "Sum = ${a + b}")
}

The Calc is a React component, that accepts a and b in props and returns div element. Here is the equivalent component in JSX:

export const Calc = ({a, b}) => {
    return <div>Sum = {a + b}</div>;
};

and in TSX:

export interface CalcProps {
    a: number;
    b: number;
}

export const Calc = ({a, b} : CalcProps) => {
    return <div>Sum = {a + b}</div>;
};

In Akryl you don't need to wrap props into an interface or in a class - just pass them as function arguments.

You can call a component function from any place in code. There is no restriction to use a component only inside another component.

Example:

// Akryl
val element = Calc(a = 10, b = 20)
// JSX
const element = <Calc a={10} b={20} />;

If your component is pure, you can use memo instead of component function. It has the same effect as in React: it will prevent a component from re-render if its props are not changed.

Hooks

Akryl has common hooks from React:

All hooks have receiver argument of type ComponentScope, that prevents usage outside of a component at compile time.

// will compile
fun counter() = component {
    val (state, setState) = useState(0)
}

// will not compile: ComponentScope receiver is not provided
val (state, setState) = useState(0)

To create a custom hook, write a function that has the ComponentScope receiver parameter:

fun ComponentScope.useRenderCount(): Int {
    val ref = useRef(0)
    ref.current += 1
    return ref.current
}

JavaScript Interop

Akryl components can accept JS components as children and vice versa. There can be a component tree that contains an arbitrary mix of Akryl and JS components.

To use an existing React library, declare the library using @JsModule and write helper functions to use components in a convenient way. For example, let's create a wrapper for blueprintjs button:

// library declaration

@JsModule("@blueprintjs/core")
@JsNonModule
external object Blueprint {
    val Button: Component<dynamic>
}

// helper function for button

fun bpButton(
    onClick: (() -> Unit)? = undefined,
    children: List<ReactElement<*>> = emptyList()
) = React.createElement(
  type = Blueprint.Button,
  props = json(
    "onClick" to onClick
  ),
  children = *children.toTypedArray()
)

// usage in Akryl

fun app() = component {
    bpButton(
        onClick = { window.alert("Hello, World!") },
        children = listOf(text("Click me!"))
    )
}

Comparison with kotlin-react

There are two options to create a component in kotlin-react.

  1. Class API
interface GreetingProps : RProps {
    var name: String
}

class Greeting : RComponent<GreetingProps, RState>() {
    override fun RBuilder.render() {
        div {
            +"Hello, ${props.name}!"
        }
    }
}

fun RBuilder.greeting(name: String = "John") = child(Greeting::class) {
    attrs.name = name
}
  1. Functional API
interface GreetingProps : RProps {
    var name: String
}

val greeting = functionalComponent<GreetingProps> { props ->
    div {
        +"Hello, ${props.name}!"
    }
}

fun RBuilder.greeting(name: String = "John") = child(greeting) {
    attrs.name = name
}

Akryl has only a functional API.

fun greeting(name: String = "John") = component {
    div(text = "Hello, $name!")
}

Differences between kotlin-react and akryl:

  • In both options of kotlin-react, you need to create a props class, a component body, and a helper function. In akryl, you only need to create a single function.
  • The functional component of kotlin-react will be anonymous. It will not have a name in React DevTools, so it will be harder to debug.
  • attrs are not enforcing required props.