React needs a root node to render to.
Use the createRoot(htmlElement)
function to set the given htmlElement
as a Root
element,
and call Root.render(reactNode)
to inject the corresponding ReactNode
as the root:
fun main() {
createRoot(document.getElementById("root")).render(App.create())
}
private val App = FC<Props> {
+"Hello, world!"
}
In this example, we made sure a <div id="root">
is present in our HTML file, and we use that <div>
as root.
We'll explain the other parts of this code in the next sections.
As you might know, the simplest way to define a React component in JavaScript is to write a function,
like this Welcome
component:
import React from 'react';
export function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
In Kotlin, we use the FC
function to define a component (where FC stands for "functional component").
So the equivalent code looks like this:
import react.*
import react.dom.html.ReactHTML.h1
external interface WelcomeProps : Props {
var name: String
}
private val Welcome = FC<WelcomeProps>("Welcome") { props ->
h1 {
+"Hello, ${props.name}"
}
}
FC
is a generic function that requires us to specify the type of props for our component.
This is why we specify this type between chevrons in the call: FC<WelcomeProps>
.
When writing React code in JavaScript, the type annotations for props (via PropTypes
) are optional, but in Kotlin
they are not. This is why we need an explicit interface to define the type of the props for our component.
If we don't need any custom props, we can also use the provided basic Props
type without defining a custom interface.
We also specify the name of the component as argument to the FC
function. This is actually optional too.
If we don't provide a name, the component tree in React dev tools will look slightly less nice.
As you can see, you can use a DSL inside the body of a component: ChildrenBuilder
lets you construct your component's
markup using type-safe builders.
All the standard HTML elements are available in the react.dom.html.ReactHTML
object and can be used in this DSL, like
the h1
in the example above.
This means that, similar to JSX in JavaScript, you can intertwine HTML-like tags with Kotlin logic, string interpolation,
loops, etc.
As we've seen, if you don't need any special props in your component, you can use FC<Props>
.
private val SimpleComponent = FC<Props>("SimpleComponent") {
// ...
}
Class components (which allow state and lifecycle handlers) are discouraged now, and shouldn't be used in new code. Please define your components as functions instead, and use hooks to store the state or run effects.
You can use a component in mainly 2 ways.
The most common is to use the component as part of the DSL in contexts where a ChildrenBuilder
is available,
usually nested inside other components or HTML elements.
Just call the component like a function with a lambda parameter, so it looks like a block:
div {
Welcome {
name = "Kotlin"
}
}
When used like this, the component is automatically appended as a child of the parent component.
Inside the block of your component, you can set values for its props, or nest other components.
Sometimes, you need to create a standalone "instance" of a component without attaching it as a child of the parent component. This is the case when defining the root component for instance. This may also be necessary if a library requires you to pass a component as a property of another component.
To do so, simply call .create()
on your component (you don't need a ChildrenBuilder
in this case):
val myComponentInstance = Welcome.create {
name = "Kotlin"
}
If a single component contains a lot of code, you can use ChildrenBuilder
extension functions to group and structure
code that belongs together.
val myComplexComponent = FC<Props> {
myHeader("complex")
myFooter("!")
}
fun ChildrenBuilder.myHeader(adjective: String) {
h1 {
+"Hello, $adjective Header!"
}
// . . . more complex code
}
fun ChildrenBuilder.myFooter(punctuation: String) {
h1 {
+"Hello, Footer $punctuation"
}
// . . . more complex code
}
Note that this is not the same as creating new React components – for the React renderer and reconciler, this is a single component.
React 16.8 introduced Hooks as a novel way of using state and other React features without writing a class.
You can use the useState
Hook when you want to keep track of state without the need for a class component. Consider
the following implementation of a counter inside a FC
:
val counter = FC<Props> {
val (count, setCount) = useState(0)
button {
onClick = { setCount(count + 1) }
+count.toString()
}
}
Calling useState
returns a pair (that is directly destructured): a reference to your state (here, count
of
type Int
), and a function that can be used to set the state (here, setCount
of type RSetState<Int>
).
Note that unlike properties in a React class component, the setCount
function can be called without having to use
a setState
lambda.
It is also possible to use the State Hook via Kotlin's delegation syntax, which can simplify the code even more:
val counter = FC<Props> {
var count by useState(0)
button {
onClick = { count += 1 }
+count.toString()
}
}
In this example, assignments to the count
variable are automatically wrapped with the appopriate hook calls under the
hood.
To learn more about the State Hook, check out the official React documentation.
The Effect Hook can be used to perform side effects without the need for a class component, such as querying an API or
establishing a connection.
Consider the following implementation of a FC
which fetch
es a random fact and displays it in a h3
tag:
val randomFact = FC<Props> {
val (randomFact, setRandomFact) = useState<String?>(null)
useEffect(*emptyArray()) { // or useEffectOnce
window.fetch("http://numbersapi.com/42").then {
it.text().then { fact -> setRandomFact(fact) }
}
}
h3 { +(randomFact ?: "Fetching...") }
}
Note that in this example, useEffect
gets passed a list of (empty) dependencies. If *emptyArray()
is
omitted, useEffect
will be called after each invocation of setRandomFact
, resulting in an endless loop.
We might want to set up a subscription to some external data source. In that case, it is important to clean up so that we don't introduce a memory leak. The following example illustrates how to write effects with cleanup:
// Same example in JavaScript: https://codesandbox.io/s/jvvkoo8pq3?file=/src/index.js
data class Hit(val objectID: String, val url: String, val title: String)
data class Data(val hit: List<Hit>)
val searchResults = FC<Props> {
var data by useState(Data(emptyList()))
var query by useState("react")
useEffect(listOf(query)) {
var ignore = false
val job = MainScope().launch {
window.fetch("https://hn.algolia.com/api/v1/search?query=${query}").then {
if (!ignore)
it.text().then { fetched -> data = JSON.parse(fetched) }
}
}
cleanup {
job.cancel()
ignore = true
}
}
input {
value = query
onChange = {
query = (it.target as HTMLInputElement).value
}
}
ul {
for (item in data.hit) {
li {
key = item.objectID
a {
href = item.url
+item.title
}
}
}
}
}
To learn more about the Effect Hook, check out the official React documentation.
Building your own Hooks lets you extract component logic into reusable functions.
A custom Hook is a Kotlin function whose name starts with "use" and that may call other Hooks:
fun useCards(): List<String> {
val (cardsInDeck, setCardsInDeck) = useState(emptyList<String>())
useEffectOnce {
val job = MainScope().launch {
val results = List(52) { i -> i.toString() }
setCardsInDeck(results)
}
cleanup {
job.cancel()
}
}
return cardsInDeck
}
// now we can use this hook in any component!
val randomFact = FC<Props> {
val cardsInDeck = useCards()
h3 {
for (card in cardsInDeck) {
p {
+card
}
}
}
}
To learn more about the Custom Hook, check out the official React documentation.
There is no built-in capability for writing inline styles in a type-safe manner.
However, it can be done by adding a dependency on
kotlin-emotion.
Note that kotlin-styled-next
is not compatible with the kotlin-react
module (only the kotlin-react-legacy
).
kotlin-emotion
allows you to style built-in HTML elements and custom components if their props type extends
PropsWithClassName
instead of just Props
.
To do so, call the css { ... }
function inside your component, and set type-safe CSS properties as you see fit:
div {
css {
backgroundColor = NamedColor.blue
width = 20.px
height = 20.px
borderRadius = 50.pct
}
}
Note that this css { ... }
helper is actually a shorthand for assigning the className
property.
This is important to understand because it has several consequences:
- it is only available in components that have the
className
property (fromPropsWithClassName
) - calling
css { ... }
multiple times will override previous styles - the last block takes precedence - mixing a
className
assignment withcss
blocks also has the same consequence - you can "transfer" the CSS properties of your component (passed by the caller) to a child of your component by simply transferring
the
className
property
For example, if you want to create a stylable component (so callers can use css {...}
) and you want to transfer all the CSS
properties to a child in your component, you actually just have to transfer the className
property:
private val Circle = FC<PropsWithClassName>("Circle") { props ->
div {
className = props.className
+"Hello"
}
}
// later in a ChildrenBuilder context
Circle {
css {
width = 20.px
}
}
You can also combine the existing properties with new CSS properties by using an overload of the css
function that takes a
class name as a first argument:
private val Circle = FC<PropsWithClassName>("Circle") { props ->
div {
css(props.className) { // using props.className respects the CSS passed by the caller
// here we can add more CSS
backgroundColor = NamedColor.red
}
+"Hello"
}
}