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 a full-fledged typing, fearless refactoring, all the features of Kotlin, and most importantly, the general code for JVM backing and Javascript fronts.

    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 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 side, 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’m not going to describe the configuration of this devil machine here, this is a topic for a separate article (as long as 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
    classHomeextendsReact.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)
    };
    exportdefault Home;
    

    classHome(
      props: RouteResultProps<*>
    ) : RComponent
    <RouteResultProps<*>, State>
    (props) {
      init {
        state = State(
          color = queryAsMap(
            props.location.search
          )["color"],
          brand = queryAsMap(
            props.location.search
          )["brand"]
        )
      }
      overridefuncomponentDidMount()
      {
        launch {
          updateState { //(3)
            brands = fetchJson( //(4)"/api/brands",
              StringSerializer.list
            )
            colors = fetchJson( //(4)"/api/colors",
              StringSerializer.list
            )
          }
        }
      }
    }
    classState(
      var color: String?, //(5)var brand: String? //(5)
    ) : RState {
    var loaded: Boolean = false//(1)lateinitvar brands: List<String> //(2)lateinitvar colors: List<String> //(2)
    }
    privateval serializer: JSON = JSON()
    suspendfun<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 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 out 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 doesn’t 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, 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 a suspendmodifier, which is logical: componentDidMount is a synchronous method. But you can insert an asynchronous unit anywhere in the code launch { }. You can explicitly accept 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 Korutin
    suspendfunparallel(vararg tasks: suspend () -> Unit) {
        tasks.map {
            async { it.invoke() } //запускаем каждый task, но не ждем ответа. async {} возвращает что-то вроде promise
        }.forEach { it.await() } //все запустили, теперь ждем
    }
    overridefuncomponentDidMount() {
        launch {
            updateState {
                parallel({
                    halls = hallExchanger.all()
                }, {
                    instructors = instructorExchanger.active()
                }, {
                    groups = fetchGroups()
                })
            }
        }
    }
    


    Now we will load the machines from the back using the 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:

    privatesuspendfunloadCars() {
        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 pay 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 in 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 simply be 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;
    }
    

    privatefun 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"
      ) {}
    }
    infixfun<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 js version it looks interesting, but it does not matter. But straightening a sequence of words is much more pleasant: “we 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, it is necessary to change the state, load the machines from the back and change the URL.

    javascript
    kotlin
    
      render() {
        if (!this.state.loaded)
          returnnull;
        return (
          <HomeHeaderbrands={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()
      }
    

    overridefun
        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) }
        )
      }
      privatefunnavigateToChanged(
        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 still happened, 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 a table with 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>
    );
    

    privatefun 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 are not enough some basic things. But in the case of Kotlin, my costs 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.

    And 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, which forms react-dom and plain html at the same time.

    Already written articles about Kotlin:

    Aftertaste from Kotlin, part 1
    Aftertaste from Kotlin, part 2
    Aftertaste from Kotlin, part 3. Korutins - divide processor time

    Also popular now: