19

React in Kotlin/JS, What I learned (long but useful read) - JavaScript - Kotlin...

 4 years ago
source link: https://discuss.kotlinlang.org/t/react-in-kotlin-js-what-i-learned-long-but-useful-read/16168
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

React in Kotlin/JS, What I learned (long but useful read)

Hi everyone!

Note: Hopefully I can post my entire project later, but as it’s a university project it would be considered fraud to publish it :confused:.

Why React?
Recently for University, we had to make a visualization tool for a dataset of our choosing. One of the recommendations was to use d3.js, because of the diversity in graphs. Now I don’t like JavaScript; it’s very unpredictable unless you’ve been working with it for years and the best way to debug is just console.logging everything you do. So when I found out Data2Viz 28 existed as d3 alternative for Kotlin, I decided to give it a shot and try to do this project in Kotlin/JS (I did choose for JS because showing the project on a website is still easier than running a Java program). Frameworks are not really necessary using Kotlin/JS but due to the current state of it, if you want a nice layout, you’re bound to reuse JS libraries. I worked with React (native) before and I liked the state management, so when I found that Jetbrains made their own kotlin-react wrapper 29, I thought it was worth a shot.

build.gradle.kts
Kotlin React tutorials like this 30 are great to get some info about how React works in Kotlin, however, they all still use create-react-app-kotlin-app as a starting point, which uses npm only, so Gradle-using libraries are off limits. Now, Jetbrains is rewriting these as we speak, but in my case, I had to find it all out myself using only this official tutorial 23 for setting up a JS project. .kts examples of build.gradle files are even harder to find, so it was a fun challenge.
Anyways, this was my final build.gradle.kts 53, (quick note: to use eap versions of kotlin, you need to add this 10 to your settings.gradle.kts). The project uses webpack automatically to enable hot reloading when saving a file. To do do this, you need to run the project like gradlew browserRun -t or setup the run configuration similarly.
Webpack hot reloading didn’t work flawlessly right away though. I described this in an earlier thread 11. The solution appeared to be adding ./webpack.config.d/webpack.config.js 19 to the root of my project. After this, it worked perfectly!

Layout
I am a fan of material design, not gonna lie. A React library that is oftentimes used for creating material UI’s is, well, Material-UI 25. Lucky for us, there are several Kotlin wrappers for this library. The first one I found was Muirwik 40. It worked great with kotlin-styled 27, so I decided to use it. It is not finished yet (like most things Kotlin/JS), so instead of importing it as a library, I was required to just download the files into a project folder so I could change some stuff when needed.
A simple component I created using the Card of Material-UI actually was a card that increased its shadow when the cursor hovers over it. It can be used just like a normal mCard. Check it out here 17! (It does use my React Prop and State Delegates 17, but it can easily be rebuilt without it).

React tips #1: Passing functions to child components
The age-old React rule of not defining arrow functions / lambdas inside attributes also applies to Kotlin. You might be tempted to write an onClick like this:

override fun RBuilder.render() {
    childComponent {
        attrs {
            onClick = { it: Event ->
                counter++
            }
        }
    }
}

While this avoids clutter, React doesn’t like it. This is because components (especially when they’re pure components) always refresh when their props / state change. According to Kotlin/JS
{ it: Event -> counter++ } != { it: Event -> counter++ }, so everytime the parent component updates, the child will update as well, even though nothing essentially changed.
So alright… like this then?

private fun doOnClick(it: Event) { counter++ }    
override fun RBuilder.render() {
    childComponent {
        attrs {
            onClick = ::doOnClick
        }
    }
}

Yeahh… no. Unfortunately Kotlin/JS works differently than Kotlin/JVM 12 in the sense that in Kotlin/JS ::doOnClick != ::doOnClick.
So, the way to go is like this:

private val doOnClick: (Event) -> Unit = { counter++ }
override fun RBuilder.render() {
    childComponent {
        attrs {
            onClick = doOnClick
        }
    }
}

Unfortunately this means in some cases you’ll need something like this, if you want to give an onClick extra information:

private val allDoOnClicks = hashMapOf<Item, (Event) -> Unit>()

private fun doOnClick(item: Item): (Event) -> Unit =
    allDoOnClicks.getOrPut(item) { // save each onClick function with item as key to prevent unnecessary reloads
        { it: Event ->
            // do something with this item
        }
    }

override fun RBuilder.render() {
    for (item in items)
        childComponent {
            attrs {
                onClick = doOnClick(item)
            }
        }
}

Another way this might work is using a more React-based solution is using useMemo 9. Fair warning, I haven’t fully tested it yet, but feel free to let me know whether it works better:

private fun doOnClick(item: Item): (Event) -> Unit = { it: Event ->
        // do something with this item
}
    

override fun RBuilder.render() {
    for (item in items)
        childComponent {
            attrs {
                onClick = useMemo({ doOnClick(item) }, arrayOf(item))
                // or maybe even:
                onClick = useMemo({{ it: Event -> /* do something with item */ }}, arrayOf(item))
            }
        }
}

In theory, this should only call doOnClick and thus regain a new (Event) -> Unit function for onClick if item changes, otherwise it takes it from memory.

React tips #2: Component class definition
This is a small tip, but it did cause me some trouble. When defining a class for an RComponent, do not name the props in the constructor props:

class ExampleComponent(prps: ExampleComponentProps) : RComponent<ExampleComponentProps, ExampleComponentState>(prps) {}

See, prps is not the same as this.props or just props when the component updates because of a changing attribute. If you only use this.props you’re fine as you’ll always get the newest version of props for this component. this.props and this.state should be seen as functions getProps and getState. If you keep that in mind, you’ll be fine.
Bonus tip: when using a component with properties / attributes, make sure you use the right init function, even if you’re not using props inside the init function:

override fun ExampleComponentsState.init(props: ExampleComponentProps) {
    counter = 0    
}

React tips #3: Kotlin React Prop and State Delegates
This is a couple of functions / classes I wrote to Kotlin-ify React state and props a bit more using delegates. It’s not for everyone as it requires some more writing in some parts but less in other parts. You can read my forum post here 18.

Data2Viz in React
Like I said in the intro, I used Data2Viz 28 in this project. D3.js and Data2Viz however need to bind to an HTML canvas element, which requires some ref-magic in React. If you ever want to use my solution for yourself, you can find it here 10. Unfortunately Data2Viz doesn’t allow the JS variant to use the SVG way of creating graphs/maps, only Canvases. Those don’t work well with cursors, only providing you with (x, y) coordinates. D3 devs have found a solution for this by using a hidden canvas and a lot of different colors (as used here 3), so that’s why my component function also has an optional hidden canvas you can run stuff on.

Porting over JS libraries
Adding a npm package is super easy using gradle. For example, you can just type implementation(npm("react-list")) in the dependencies and it will get imported. Getting it to work with Kotlin is a bit more difficult. I got to port over one library: react-list.
I took examples from kotlinlang.org 2 and their react tutorial 8.
I don’t know that the best way is to make it into a library or official wrapper or something, but you can find my version of react-list for Kotlin here 17 (This also includes some helper functions I borrowed from Muirwik to more easily create a styled component).

Multithreading is too hard for now
Because of my background in Android development and I was performing a large amount of calculations on a dataset (freezing the UI) I was looking into multithreading. As we have Coroutines in Kotlin/JS I assumed it just would work. I was very wrong. JavaScript is single threaded. All coroutines do is provide async functions. This means that the function can be executed at any time, in any order, but still on the main thread.
I found out about Web Workers for JavaScript. There unfortunately isn’t any (great working) Kotlin library which uses this, nor would this be an easy task I think. See, the main way to start a worker in javascript is like Worker("worker.js"). Kotlin, in my case, puts all the code in a single .js file, so this is a no-go. You can also use an in-line Worker in JS by passing plain JS as a String to a blob and creating a worker like that. Again, this is a problem in Kotlin, because Kotlin has to compile to JS first. Passing objects from and to workers from another thread is limited to a few native types and sending an object reduces it to just its attributes (breaking HashMaps etc). If you’re okay with these limits, you can try the Kworker 11 library, which is difficult to use, however it does work.
Libraries that make Workers easier to use in JS like comlink 3 are great, but almost impossible to port to Kotlin due to the hacks they use to make it work.
So yeah, someone with a lot of time and knowledge of both Kotlin and JavaScript needs to take a deep look into this as in 2020 a system that can’t even multithread properly shouldn’t exist.

Conclusion
I’m amazed Kotlin is able to be so diverse. While IMO it’s definitely better suited to work in the JVM, the JavaScript variant could be the typed refresh JavaScript needs. TypeScript is great, but it’s trying to make the meal that is JS only tastier by adding a lot of sauce. Kotlin just threw out the chef and is making the meal themselves in their kitchen (maybe I shouldn’t write this at dinner time). Now obviously, this is only my opinion and I know people that really like the way how JS/TS works, but for all those students (like me) that were taught Java as their first language, Kotlin makes a whole lot more sense.
With the promise of a single language to cover all, I think React shouldn’t be ignored by Kotlin. It is great to see Jetbrains creating kotlin-react wrappers, but I think we need a lot more people and an easy way to post wrappers to make it more mainstream.
I’m excited for Dukat 54 merging into the build process for Kotlin/JS soon, allowing you to import d.ts files from NPM and use them like it’s Kotlin. However I don’t know how this will work for libraries that are built in TypeScript and do not use type definitions, but we’ll see.

If you made it through all this you must also be very dedicated. Let me know what you think about React and / or Kotlin/JS and if you have any other tips for me or for others, don’t hesitate to post them below!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK