📜 ⬆️ ⬇️

Kotlin + React vs Javasript + React

The idea to transfer the front to any js framework appeared at the same time as the ability to write React in Kotlin. And I decided to try. The main problem: few materials and examples (I will try to correct this situation). But I have full-fledged typing, fearless refactoring, all the features of Kotlin, and most importantly, the general code for the Beck on JVM and the front on Javascript.

In this article we will write a page on Javasript + React in parallel with its counterpart on Kotlin + React. To make the comparison fair, I added typing to Javasript.



Add typing to Javascript was not so easy. If I needed gradle, npm and webpack for Kotlin, then for Javascript I needed npm, webpack, flow and babel with the react, flow, es2015 and stage-2 presets. At the same time, the flow is somehow on the side, and it must be launched separately and separately to be friends with the IDE. If we take out the brackets and the like, then for direct writing the code, on the one hand, Kotlin + React remains, and on the other, Javascript + React + babel + Flow + ES5 | ES6 | ES7.
')
For our example, we will make a page with a list of cars and the ability to filter by brand and color. We can mark and color possible for filtering from Beck once at the first boot. Selected filters are stored in the query. Machines are displayed in the plate. My project is not about cars, but the overall structure is generally similar to what I regularly work with.

The result looks like this (I won’t be a designer):



I will not describe the configuration of this whole devil machine here, this is a topic for a separate article (for now, you can smoke the sources from this one).

Loading data from the back


First you need to load brands and available colors from the back.
javascript
kotlin
class Home extends React.Component <ContextRouter, State>{ state = { loaded: false, //(1) color: queryAsMap( this.props.location.search )["color"], brand: queryAsMap( this.props.location.search )["brand"], brands: [], //(2) colors: [] //(2) }; async componentDidMount() { this.setState({ //(3) brands: await ( //(4) await fetch('/api/brands') ).json(), colors: await ( //(4) await fetch('/api/colors') ).json() }); } } type State = { color?: string, //(5) brand?: string, //(5) loaded: boolean, //(1) brands: Array<string>, //(2) colors: Array<string> //(2) }; export default Home; 

 class Home( props: RouteResultProps<*> ) : RComponent <RouteResultProps<*>, State> (props) { init { state = State( color = queryAsMap( props.location.search )["color"], brand = queryAsMap( props.location.search )["brand"] ) } override fun componentDidMount() { launch { updateState { //(3) brands = fetchJson( //(4) "/api/brands", StringSerializer.list ) colors = fetchJson( //(4) "/api/colors", StringSerializer.list ) } } } } class State( var color: String?, //(5) var brand: String? //(5) ) : RState { var loaded: Boolean = false //(1) lateinit var brands: List<String> //(2) lateinit var colors: List<String> //(2) } private val serializer: JSON = JSON() suspend fun <T> fetchJson( //(4) url: String, kSerializer: KSerializer<T> ): T { val json = window.fetch(url) .await().text().await() return serializer.parse( kSerializer, json ) } 


Looks very similar. But there are differences:

  1. Default values ​​can be written in the same place where the type is declared. This makes it easier to maintain the integrity of the code.
  2. lateinit allows you not to set the default value at all for what will be loaded later. When compiling, such a variable is counted as NotNull, but every time it is accessed, it is checked that it has been filled in and a human readable error is issued. This will be especially true for a more complex object than an array. I know, the same could be achieved with the help of flow, but it is so cumbersome that I did not try.
  3. kotlin-react out of the box gives the setState function, but it does not combine with corutines, because it is not inline. I had to copy and put inline.
  4. Actually, korutiny . This is a replacement for async / await and much more. For example, through them is made yield . Interestingly, only the word suspend is added to the syntax, all the rest is just code. Therefore, more freedom to use. And a little tighter control at the compilation level. So, you cannot override componentDidMount with the suspend modifier, which is logical: componentDidMount is a synchronous method. But you can insert an asynchronous launch { } block anywhere in the code. You can explicitly take an asynchronous function in a parameter or a class field (just below an example from my project).
  5. In javascript less control is nullable. So in the resulting state, you can change the nullability of the brand, color and loaded fields and everything will be collected. In the Kotlin version there will be justified compilation errors.

A parallel trip to the Bek with the help of Corutin
 suspend fun parallel(vararg tasks: suspend () -> Unit) { tasks.map { async { it.invoke() } //  task,    . async {}  -  promise }.forEach { it.await() } // ,   } override fun componentDidMount() { launch { updateState { parallel({ halls = hallExchanger.all() }, { instructors = instructorExchanger.active() }, { groups = fetchGroups() }) } } } 


Now we will load the machines from the back using filters from query
Js:

  async loadCars() { let url = `/api/cars?brand=${this.state.brand || ""}&color=${this.state.color || ""}`; this.setState({ cars: await (await fetch(url)).json(), loaded: true }); } 

Kotlin:

  private suspend fun loadCars() { val url = "/api/cars?brand=${state.brand.orEmpty()}&color=${state.color.orEmpty()}" updateState { cars = fetchJson(url, Car::class.serializer().list) //(*) loaded = true } } 

I want to draw attention to Car::class.serializer().list . The fact is that jetBrains has written a library for serialization / deserialization, which works equally on JVM and JS. First, there are fewer problems and code in case there is a back-up on the JVM. Secondly, the validity of the incoming json is checked during deserialization, and not at some time during the call, so that when changing the version of the backup, and when integrating the principle, the problems will be faster.

We draw a cap with filters


We write a stateless component to display two drop-down lists. In the case of Kotlin, this will be just a function, in the case of js, a separate component that will be generated by the react loader during assembly.

javascript
kotlin
 type HomeHeaderProps = { brands: Array<string>, brand?: string, onBrandChange: (string) => void, colors: Array<string>, color?: string, onColorChange: (string) => void } const HomeHeader = ({ brands, brand, onBrandChange, colors, color, onColorChange }: HomeHeaderProps) => ( <div> Brand: <Dropdown value={brand} onChange={e => onBrandChange(e.value) } options={withDefault("all", brands.map(value => ({ label: value, value: value })))} /> Color: <Dropdown value={color} onChange={e => onColorChange(e.value) } options={withDefault("all", colors.map(value => ({ label: value, value: value })))} /> </div> ); function withDefault( label, options ) { options.unshift({ label: label, value: null }); return options; } 

 private fun RBuilder.homeHeader( brands: List<String>, brand: String?, onBrandChange: (String?) -> Unit, colors: List<String>, color: String?, onColorChange: (String?) -> Unit ) { +"Brand:" dropdown( value = brand, onChange = onBrandChange, options = brands.map { SelectItem( label = it, value = it ) } withDefault "all" ) {} +"Color:" dropdown( value = color, onChange = onColorChange, options = colors.map { SelectItem( label = it, value = it ) } withDefault "all" ) {} } infix fun <T : Any> List<SelectItem<T>>.withDefault( label: String ) = listOf( SelectItem( label = label, value = null ) ) + this 


The first thing that catches your eye is HomeHeaderProps in the JS part, we are forced to declare the incoming parameters separately. Inconvenient.

The syntax of Dropdown has changed a bit. I use primereact here , naturally, I had to write a kotlin wrapper. On the one hand, this is extra work (thank God, there is ts2kt ), but on the other hand, it is an opportunity to make api more convenient in some places.

Well, a little bit of syntax sugar when creating items for a dropdown. })))} in the js version looks interesting, but it does not matter. But straightening a sequence of words is much nicer: “we will transform colors into items and add` all` by default ”, instead of“ add ʻall` to the goals converted to items ”. This seems like a small bonus, but when you have several such coups in a row ...

Save filters in query


Now, when selecting filters by brand and color, you need to change the state, load the machines from the server and change the URL.

javascript
kotlin
 render() { if (!this.state.loaded) return null; return ( <HomeHeader brands={this.state.brands} brand={this.state.brand} onBrandChange={brand => this.navigateToChanged({brand})} colors={this.state.colors} color={this.state.color} onColorChange={color => this.navigateToChanged({color})} /> ); } navigateToChanged({ brand = this.state.brand, color = this.state.color }: Object) { //(*) this.props.history.push( `?brand=${brand || ""}` + `&color=${color || ""}`); this.setState({ brand, color }); this.loadCars() } 

  override fun RBuilder.render() { if (!state.loaded) return homeHeader( brands = state.brands, brand = state.brand, onBrandChange = { navigateToChanged(brand = it) }, colors = state.colors, color = state.color, onColorChange = { navigateToChanged(color = it) } ) } private fun navigateToChanged( brand: String? = state.brand, color: String? = state.color ) { props.history.push( "?brand=${brand.orEmpty()}" + "&color=${color.orEmpty()}") updateState { this.brand = brand this.color = color } launch { loadCars() } } 


And here again the problem with the default values ​​of the parameters. For some reason, flow did not allow me to have typing at the same time, the destructor and default value taken from the state. Maybe just a bug. But, if it did happen, you would have to declare a type outside the class, i.e. generally the screen is higher or lower.

Draw a table


The last thing left to do is to write a stateless component to draw the table with the machines.

javascript
kotlin
 const HomeContent = (props: { cars: Array<Car> }) => ( <DataTable value={props.cars}> <Column header="Brand" body={rowData => rowData["brand"] }/> <Column header="Color" body={rowData => <span style={{ color: rowData['color'] }}> {rowData['color']} </span> }/> <Column header="Year" body={rowData => rowData["year"]} /> </DataTable> ); 

 private fun RBuilder.homeContent( cars: List<Car> ) { datatable(cars) { column(header = "Brand") { +it.brand } column(header = "Color") { span { attrs.style = js { color = it.color } +it.color } } column(header = "Year") { +"${it.year}" } } } 


Here you can see how I straightened api primefaces, and how to set the style in kotlin-react. This is the usual json, as in the js version. In my project I did a wrapper that looks the same, but with strict typing, as far as possible in the case of html styles.

Conclusion


Getting involved in a new technology is risky. It is not enough guides, there is nothing on stack overflow, there is not enough of some basic things. But in the case of Kotlin, my expenses paid off.

While I was preparing this article, I learned a bunch of new things about modern Javascript: flow, babel, async / await, jsx templates. I wonder how quickly this knowledge will become obsolete? And all this is not necessary if you use Kotlin. In this case, you need to know very little about React, because most of the problems are easily solved with the help of language.

What do you think about replacing the entire zoo with one language with a large set of buns into the bargain?

For those interested source .

PS: There are plans to write articles about configs, integration with JVM and dsl that forms react-dom and plain html at the same time.

Already written articles about Kotlin:

Kotlin aftertaste, part 1
Kotlin aftertaste, part 2
Aftertaste from Kotlin, part 3. Korutiny - we divide the processor time

Source: https://habr.com/ru/post/418553/


All Articles