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.
We’re also using 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 doesn’t create a DOM node. Therefore, Fragments won’t affect styling and perform better. 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
anduseFormState
(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
anduseTransition
(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.