React in Kotlin

React has become a well-established framework to build modular applications. Most often this refers to web applications written in JavaScript or TypeScript. As of this year, I’ve been tasked to write some React components in Kotlin. Without prior knowledge of Kotlin there’s a lot that I’ve learned. This post shall help me and other TypeScript developers to use their existing React expertise with the Kotlin language.

React for Kotlin has changed a bit over the past years. Similarly to how React started with components using classes, there have been changes to the syntax in Kotlin. The following Kotlin examples are functional components using React Hooks. They have been written for Kotlin 1.9.21 and React 18.2.0.

The most basic component

Let’s just return some HTML to get started. In TypeScript, I’m going to use the tsx file ending. Other than ts, tsx implies the presence of React’s HTML-like syntax.

import React from 'react';

export function HelloWorld(): React.JSX.Element {
    return <h1>Hello world!</h1>;
}
import react.FC
import react.dom.html.ReactHTML

val HelloWorld = FC("HelloWorld") {
    ReactHTML.h1 {
        +"Hello World!"
    }
}

Next, we’re going to import an existing component. This example uses the Typography component from the Material UI framework MUI.

import React from 'react';
import { Typography } from '@mui/material';

export function OpeningHoursNotice(): React.JSX.Element {
    return (
        <Typography
            component="div"
            variant="h6"
            sx={{
                flexGrow: 1,
                overflowX: 'hidden',
                textOverflow: 'ellipsis',
                whiteSpace: 'nowrap',
            }}
        >The library closes at 10 PM.</Typography>
    );
}
import mui.material.Typography
import mui.material.styles.TypographyVariant
import mui.system.sx
import react.FC
import react.dom.html.ReactHTML
import web.cssom.Overflow
import web.cssom.TextOverflow
import web.cssom.WhiteSpace
import web.cssom.number

val OpeningHoursNotice = FC("OpeningHoursNotice") {
    Typography {
        component = ReactHTML.div
        variant = TypographyVariant.h6
        sx {
            flexGrow = number(1.0)
            overflow = Overflow.hidden
            textOverflow = TextOverflow.ellipsis
            whiteSpace = WhiteSpace.nowrap
        }
        +"The library closes at 10 PM."
    }
}

Props

Most React applications will use Props to hand values from parents to children. Props may be values, styles and function handlers. Let’s start with a simple, structured object.

import React from 'react';

export type Book = {
    id: number;
    title: string;
    author: string;
};

type BookItemProps = {
    book: Book;
};

export function BookItem(props: BookItemProps): React.JSX.Element {
    return (
        <li>{props.book.author} - {props.book.title}</li>
    );
}
import react.FC
import react.Props
import react.dom.html.ReactHTML

data class Book(
    val id: Int,
    val title: String,
    val author: String
)

external interface BookItemProps : Props {
    var book: Book
}

val BookItem = FC<BookItemProps>("BookItem") { props ->
    ReactHTML.li {
        +props.book.author
        +" - "
        +props.book.title
    }
}

The following example is a component which can receive props of various types.

This example still needs to be written. Come back at a later time.

Children props

This section still needs to be written. Come back at a later time.

Lists

Quite often, applications will need to display a list of information. Let’s have a look at how to render lists from common data structures.

Array

The most straight-forward data type for a list is an Array. We use the previously defined BookItem component to render the individual items.

import React from 'react';
import { Book, BookItem } from './BookItem';

const recentReads: Book[] = [
    {
        id: 1,
        title: 'Show Your Work!',
        author: 'Austin Kleon',
    },
    {
        id: 2,
        title: 'How to Be Everything',
        author: 'Emilie Wapnick',
    },
];

export function RecentReadsArray(): React.JSX.Element {
    return (
        <aside>
            <h2>Recent Reads</h2>
            <ol>
                {recentReads.map(recentRead => (
                    <BookItem
                        key={recentRead.id}
                        book={recentRead}
                    />
                ))}
            </ol>
        </aside>
    );
}
import react.FC
import react.dom.html.ReactHTML

private val recentReads = listOf(
    Book(1, "Show Your Work!", "Austin Kleon"),
    Book(2, "How to Be Everything", "Emilie Wapnick")
)

val RecentReadsArray = FC("RecentReadsArray") {
    ReactHTML.aside {
        ReactHTML.h2 {
            +"Recent Reads"
        }
        ReactHTML.ol {
            for (recentRead in recentReads) {
                BookItem {
                    key = recentRead.id.toString()
                    book = recentRead
                }
            }
        }
    }
}

Object

Sometimes we find ourselves working with lists stored in an Object. If an API provides us with an Object, this is how we render it as a list.

import React from 'react';
import { Book, BookItem } from './BookItem';

const recentReads: Record<string, Book> = {
    'fettnäpfchenführer-taiwan': {
        id: 3,
        title: 'Fettnäpfchenführer Taiwan',
        author: 'Deike Lautenschläger',
    },
};

export function RecentReadsObject(): React.JSX.Element {
    return (
        <aside>
            <h2>Recent Reads</h2>
            <ol>
                {[...Object.entries(recentReads)].map(([key, book]) => (
                    <BookItem
                        key={key}
                        book={book}
                    />
                ))}
            </ol>
        </aside>
    );
}
import react.FC
import react.dom.html.ReactHTML

private val recentReads: Map<String, Book> = mapOf(
    "fettnäpfchenführer-taiwan" to Book(4, "Fettnäpfchenführer Taiwan", "Deike Lautenschläger")
)

val RecentReadsObject = FC("RecentReadsObject") {
    ReactHTML.aside {
        ReactHTML.h2 {
            +"Recent Reads"
        }
        ReactHTML.ol {
            for ((bookId, recentRead) in recentReads) {
                BookItem {
                    key = bookId.toString()
                    book = recentRead
                }
            }
        }
    }
}

Map

Whenever untrusted user input needs to be used as a key of an Object, we should use Map instead. This prevents vulnerabilities and errors if a key is named “proto”. With security in mind, let’s dihave a look at the previous code solved with a Map.

Kotlin doesn’t seem to have the distinction between JavaScript’s Object and Map, so this example uses the Map type again.

import React from 'react';
import { Book, BookItem } from './BookItem';

const recentReads: Map<number, Book> = new Map([
    [4, {
        id: 4,
        title: 'Der letzte Außenposten',
        author: 'Anne Polifka',
    }],
]);

export function RecentReadsMap(): React.JSX.Element {
    return (
        <aside>
            <h2>Recent Reads</h2>
            <ol>
                {[...recentReads.entries()].map(([key, book]) => (
                    <BookItem
                        key={key}
                        book={book}
                    />
                ))}
            </ol>
        </aside>
    );
}
import react.FC
import react.dom.html.ReactHTML

private val recentReads: Map<Int, Book> = mapOf(
    4 to Book(4, "Der letzte Außenposten", "Anne Polifka")
)

val RecentReadsMap = FC("RecentReadsMap") {
    ReactHTML.aside {
        ReactHTML.h2 {
            +"Recent Reads"
        }
        ReactHTML.ol {
            for ((bookId, recentRead) in recentReads) {
                BookItem {
                    key = bookId.toString()
                    book = recentRead
                }
            }
        }
    }
}

Event handlers

Whenever I see React examples, they tell me how to write with a counter component. If you ever write an article about React, please don’t stop there. Also write about the difficult components. Show me how to use the various Hooks, forms, styling and other advanced methods which are needed to build a professional application! I promise that the counter is just the beginning of this chapter.

Note, we’re also adding a Fragment for the first time. Whenever you want to return multiple React nodes in one component, you can wrap them in a Fragment. Fragments are written as <Fragment></Fragment> or <></>. A Fragment won’t create a DOM node and therefore won’t affect styling while also having a better performance. It got introduced as a group of nodes because a React component can only return one node.

It’s an uncontrolled component which stores its own state using useState. For simplicity, the next example does not cache the handler function.

import React, { Fragment, useState } from 'react';

export function UnoptimizedCounter(): React.JSX.Element {
    const [count, setCount] = useState<number>(0);

    return (
        <Fragment>
            <p>The count is: {count}</p>
            <button
                type="button"
                //FIXME: Avoid expensive DOM changes with useCallback
                onClick={() => setCount(count + 1)}
            >Increase</button>
        </Fragment>
    );
}
import react.FC
import react.Fragment
import react.dom.html.ReactHTML
import react.useState
import web.html.ButtonType

val UnoptimizedCounter = FC("UnoptimizedCounter") {
    val (count, setCount) = useState(0)

    Fragment {
        ReactHTML.p {
            +"The count is: "
            +count.toString()
        }
        ReactHTML.button {
            type = ButtonType.button
            //FIXME: Avoid expensive DOM changes with useCallback
            onClick = { _: Any -> setCount(count + 1) }
            +"Increase"
        }
    }
}

Caching handlers with useCallback

If you’re using React 19 with a compiler, you may not need this as the developers are working on a fully automatic detection which would make useCallback and useMemo obsolete.

Changes to the browser’s DOM are relatively expensive. To improve the performance of a component, we can choose to cache handler function using the React Hook useCallback.

import React, { Fragment, useCallback, useState } from 'react';

export function Counter(): React.JSX.Element {
    const [count, setCount] = useState<number>(0);

    const handleIncrease = useCallback(() => {
        // This uses the callback variant of setState.
        setCount(value => value + 1);
    }, []);

    return (
        <Fragment>
            <p>The count is: {count}</p>
            <button
                type="button"
                onClick={handleIncrease}
            >Increase</button>
        </Fragment>
    );
}
import react.FC
import react.Fragment
import react.dom.events.MouseEvent
import react.dom.html.ReactHTML
import react.useCallback
import react.useState
import web.html.ButtonType
import web.html.HTMLButtonElement

val Counter = FC("Counter") {
    val (count, setCount) = useState(0)

    val handleIncrease = useCallback<(it: MouseEvent<HTMLButtonElement, *>) -> Unit> {
        // This uses the callback variant of setState.
        setCount { value -> value + 1 }
    }

    Fragment {
        ReactHTML.p {
            +"The count is: "
            +count.toString()
        }
        ReactHTML.button {
            type = ButtonType.button
            onClick = handleIncrease
            +"Increase"
        }
    }
}

useCallback with dependencies

If a function inside useCallback reads a variable from the component’s level, it must be added to the array of dependencies.

const handleBooking = useCallback(() => {
    bookThroughApi(bookId);
}, [bookId]);
val handleBooking = useCallback<(it: MouseEvent<HTMLButtonElement, *>) () -> Unit> {
    bookThroughApi { bookId }
}

The array of dependencies allows you to add as many values as needed.

const handleBooking = useCallback(() => {
    bookingApi.book(bookId);
}, [bookId, bookingApi]);
val handleBooking = useCallback<(it: MouseEvent<HTMLButtonElement, *>) (bookId, bookingApi) -> Unit> {
    bookingApi.book { bookId }
}

Form events

Let’s build a small form. For the best accessibility, let’s listen to the submit event on the form instead of click on the submit button. Did you know, you can submit a form by pressing Ctrl + Enter while any input is focused?

Note, the HTML button has the default type submit. Clicking it will emit a submit event on the surrounding form node. For clarity I explicitly set the type. Many UI frameworks for React define a Button component with the default type button. If you change the code to use your Button component, you don’t have to worry which type is being used.

import React, { useCallback, useState } from 'react';
import { sendBookOrder } from '../api.ts';

export function BookOrderForm(): React.JSX.Element {
    const [ title, setTitle ] = useState<string>('');

    //FIXME: Avoid expensive DOM changes by reading the input values from the event
    const handleSubmit: React.FormEventHandler<HTMLFormElement> = useCallback(event => {
        // We don't want the browser to handle this and possibly reload the page.
        event.preventDefault();

        sendBookOrder(title);
    }, [title]);

    const handleTitleChange: React.ChangeEventHandler<HTMLInputElement> = useCallback(
        event => setTitle(event.target.value),
        []
    );

    return (
        <form
            onSubmit={handleSubmit}
        >
            <h2>Order Service</h2>
            <label>
                Book title:
                <input
                    value={title}
                    onChange={handleTitleChange}
                />
            </label>
            <button type="submit">Order</button>
        </form>
    );
}
import react.FC
import react.dom.events.ChangeEventHandler
import react.dom.events.FormEventHandler
import react.dom.html.ReactHTML
import react.useCallback
import react.useState
import sendBookOrder
import web.html.ButtonType
import web.html.HTMLFormElement
import web.html.HTMLInputElement

val BookOrderForm = FC("BookOrderForm") { props ->
    val (title, setTitle) = useState("")

    val handleSubmit = useCallback<FormEventHandler<HTMLFormElement>> { event ->
        // We don't want the browser to handle this and possibly reload the page.
        event.preventDefault()

        sendBookOrder(title)
    }

    val handleTitleChange = useCallback<ChangeEventHandler<HTMLInputElement>> { event ->
        setTitle(event.target.value)
    }

    ReactHTML.form {
        onSubmit = handleSubmit
        ReactHTML.h2 {
            +"Order Service"
        }
        ReactHTML.label {
            +"Book title:"
            ReactHTML.input {
                value = title
                onChange = handleTitleChange
            }
        }
        ReactHTML.button {
            type = ButtonType.submit
            +"Order"
        }
    }
}

Select input

A select is a dropdown which allows only one option to be selected.

import React, { useCallback, useState } from 'react';

export function LibrarySelect(): React.JSX.Element {
    const [library, setLibrary] = useState<string>('ksml-main');

    const handleChange: React.ChangeEventHandler<HTMLSelectElement> = useCallback(event => {
        setLibrary(event.target.value);
    }, []);

    return (
        <label>
            Library:
            <select
                value={library}
                onChange={handleChange}
            >
                <optgroup label="Kaohsiung">
                    <option value="ksml-main">KSML Main Branch</option>
                    <option value="ksml-gushan">KSML Gushan Branch</option>
                </optgroup>
                <optgroup label="Vancouver">
                    <option value="vpl-central">VPL Central Library</option>
                    <option value="vpl-hastings">VPL Hastings Branch</option>
                </optgroup>
            </select>
        </label>
    );
}
import react.FC
import react.Fragment
import react.dom.events.ChangeEventHandler
import react.dom.html.ReactHTML
import react.useCallback
import react.useState
import web.html.HTMLSelectElement

val LibrarySelect = FC("LibrarySelect") {
    val (library, setLibrary) = useState("ksml-main")

    val handleChange: ChangeEventHandler<HTMLSelectElement> = useCallback { event ->
        setLibrary(event.target.value)
    }

    Fragment {
        ReactHTML.label {
            +"Library:"
            ReactHTML.select {
                value = library
                onChange = handleChange
                ReactHTML.optgroup {
                    label = "Kaohsiung"
                    ReactHTML.option {
                        value = "ksml-main"
                        +"KSML Main Branch"
                    }
                    ReactHTML.option {
                        value = "ksml-gushan"
                        +"KSML Gushan Branch"
                    }
                }
                ReactHTML.optgroup {
                    label = "Vancouver"
                    ReactHTML.option {
                        value = "vpl-central"
                        +"VPL Central Library"
                    }
                    ReactHTML.option {
                        value = "vpl-hastings"
                        +"VPL Hastings Branch"
                    }
                }
            }
        }
    }
}

useEffect

useEffect without dependencies

import React, { Fragment } from 'react';

export function KeyboardShortcuts(): React.JSX.Element {
    useEffect(() => {
        const handleKey = (event) => {
            if (event.code === 'h') {
                window.location.href = '/home';
            } else if (event.code === 'b') {
                window.location.href = '/books';
            }
        });
        
        document.addEventListener('keyup', handleKey);
        return () => {
            document.removeEventListener('keyup', handleKey);
        };
    }, []);

    return <Fragment></Fragment>;
}
import kotlinx.browser.window
import react.FC
import react.Fragment
import react.useEffect
import web.dom.document
import web.keyboard.KeyCode
import web.uievents.KeyboardEvent

val KeyboardShortcuts = FC("KeyboardShortcuts") {
    useEffect {
        val handleKey = { event: KeyboardEvent ->
            if (event.code == KeyCode("h")) {
                window.location.href = "/home"
            } else if (event.code == KeyCode("b")) {
                window.location.href = "/book"
            }
        }

        document.addEventListener(KeyboardEvent.KEY_UP, handleKey)
        cleanup {
            document.removeEventListener(KeyboardEvent.KEY_UP, handleKey)
        }
    }

    Fragment { }
}

useEffect with dependencies

Next we’re going to create an Carousel which updates its content every 10 seconds. After the 6th item, it will start again from the first item.

To learn how useEffect can be affected by events and for great keyboard accessibility, we pause the interval whenever the carousel is in focus.

import React, { useState } from 'react';

export function Carousel(): React.JSX.Element {
    const [carouselIndex, setCarouselIndex] = useState<number>(0);
    const [carouselFocused, setCarouselFocused] = useState<boolean>(false);

    useEffect(() => {
        // If the carousel is focused, the content stays the same.
        if (carouselFocused) {
            return;
        }
        const interval = setInterval(() => {
            setCarouselIndex(index => index < 5 ? index + 1 : 0);
        }, 10000);

        // This cleans up the interval, if the value of a
        // dependency changes or the component gets unmounted.
        return () => {
            clearInterval(interval);
        };
    }, [carouselFocused]);

    return (
        <div
            onBlur={useCallback(() => setCarouselFocused(false), [])}
            onFocus={useCallback(() => setCarouselFocused(true), [])}
        >
            <img
                alt=""
                src={`/imgs/carousel${carouselIndex}.png`}
            />
        </div>
    );
}
import kotlin.time.Duration.Companion.seconds
import react.FC
import react.dom.events.FocusEventHandler
import react.dom.html.ReactHTML
import react.useCallback
import react.useEffect
import react.useState
import web.html.HTMLDivElement
import web.timers.clearInterval
import web.timers.setInterval

val Carousel = FC("Carousel") {
    val (carouselIndex, setCarouselIndex) = useState(0)
    val (carouselFocused, setCarouselFocused) = useState(false)

    useEffect(carouselFocused) {
        // If the carousel is focused, the content stays the same.
        if (carouselFocused) {
            return@useEffect
        }
        val interval = setInterval(10.seconds) {
            setCarouselIndex { index ->
                if (index < 5) index + 1 else 0
            }
        }

        // This cleans up the interval, if the value of a
        // dependency changes or the component gets unmounted.
        cleanup {
            clearInterval(interval)
        }
    }

    ReactHTML.div {
        onBlur = useCallback<FocusEventHandler<HTMLDivElement>> { _ -> setCarouselFocused { false } }
        onFocus = useCallback<FocusEventHandler<HTMLDivElement>> { _ -> setCarouselFocused { true } }

        ReactHTML.img {
            alt = ""
            src = "/imgs/carousel$carouselIndex.png"
        }
    }
}

Data attributes

Data attributes allow us to specify custom, non-standard values on a DOM node. These are sometimes used for styling or recognising form inputs in JavaScript. Another common use case is to apply IDs to components for end-to-end testing. In the later case they will be used by a browser-based testing framework like Cypress of Selenium to find a specific component.

import React from 'react';

export function Outro(): React.JSX.Element {
    return (
        <p
            data-test-id="outro"
        >Thanks for reading!</p>
    );
}
import react.FC
import react.dom.html.ReactHTML

val Outro = FC("Outro") {
    ReactHTML.span {
        asDynamic()["test-id"] = "outro"
        +"Thanks for reading!"
    }
}

Styling

Realistically, I assume this section is more relevant than some of the previous sections. I left styling for last because it’s a different concern.

The Kotlin wrapper for React uses type-safe classes and enums to ensure valid CSS values at compile time. This is different from styling in HTML, CSS and TypeScript, which all use strings.

Styled components with Emotion

For repeated use of styles, new components can be defined with the help of the emotion framework.

import emotion.styled.styled
import mui.material.Typography
import web.cssom.TextOverflow
import web.cssom.Overflow
import web.cssom.WhiteSpace

private val NoOverflow = Typography.styled {
    overflowX = Overflow.hidden
    whiteSpace = WhiteSpace.nowrap
    textOverflow = TextOverflow.ellipsis
}

These emotion components can also use props for styling:

import emotion.styled.styled
import mui.material.LinearProgress
import web.cssom.Position
import web.cssom.Visibility
import web.cssom.px

val StyledLinearProgress = LinearProgress.styled { props ->
    visibility = if (props.hidden == true) Visibility.hidden else Visibility.visible
    position = Position.absolute
    bottom = (-2).px
    left = 0.px
    right = 0.px
}

And may use colours or other definitions from a theme:

import emotion.styled.styledWithTheme
import mui.material.AppBar
import mui.material.AppBarProps
import web.cssom.Global

val StyledAppBar = AppBar.styledWithTheme { _: AppBarProps, theme: Theme ->
    width = Globals.unset
    backgroundColor = theme.palette.primary.light
}

Ideas for more sections

  • Events handlers
    • textarea
    • Checkboxes and radio inputs
  • useMemo, useEffect and other Hooks
  • APIs and fetching data
  • Forms with action, useFormStatus and useFormState (new (experimental?) React features)
  • Error Boundary
  • Aria tags for custom accessibility
  • render props
  • Context

I never used it, but might be useful

  • Themes and Portals
  • useOptimistic and useTransition (new (experimental?) React features)

Contribute

This article is published under the CC BY-NC-SA 4.0 Deed license.

I wrote this to help myself and other web developers. Would you like to help too?

As you may have noticed, there are unfinished sections and ideas to cover more. Send me completed sections for me to add them to this article.

If you speak another language, consider translating the article. Post it anywhere, but please link to this article to give me credit. I would be delighted to host translations in German, Esperanto or Traditional Chinese on my blog.

The syntax highlighting of some code examples is broken. It would be amazing if someone could fix the lexers to improve this post and many other Hugo blogs.