Kotlin/JS React, Redux with thunk and Material UI example app/template

Preface

After struggling for a couple of days with setting up a Kotlin React app in my Kotlin multiplatform project. I thought I would share my experience and provide a set-up that worked for me. I have created a repository Kotlin/JS React example which can be used as a starting point or reference when creating React applications in Kotlin.

Kotlin/JS with React is still not that well documented unfortunately, most of the information I used in this project comes from open source projects or discussions. This is why I thought that creating such an example app can help some people set up their projects faster.

In this blog post I will go more in-depth about the app and its inner-workings. A brief overview can be found in the repository README.

The app

A simple app that displays a list of tasks which can be added and removed.

Dependencies

For this project I used Gradle Kotlin DSL, but Groovy can be used as well. Make sure to check Muirwik compatibility with the Kotlin wrapper versions. The current 0.5.1 is compatible with the pre.104-kotlin version of wrappers.

plugins {
    id("org.jetbrains.kotlin.js") version "1.3.72"
}

repositories {
    mavenCentral()
    maven("https://kotlin.bintray.com/kotlin-js-wrappers/")
    maven("https://dl.bintray.com/cfraser/muirwik")
    jcenter()
}

dependencies {
    implementation(kotlin("stdlib-js"))

    implementation("org.jetbrains.kotlinx:kotlinx-html-js:0.7.1")
    implementation("org.jetbrains:kotlin-react:16.13.1-pre.104-kotlin-1.3.72")
    implementation("org.jetbrains:kotlin-react-dom:16.13.1-pre.104-kotlin-1.3.72")
    implementation("org.jetbrains:kotlin-react-redux:5.0.7-pre.104-kotlin-1.3.72")
    implementation(npm("react", "16.13.1"))
    implementation(npm("react-dom", "16.13.1"))

    implementation("org.jetbrains:kotlin-redux:4.0.0-pre.104-kotlin-1.3.72")

    implementation("org.jetbrains:kotlin-styled:1.0.0-pre.104-kotlin-1.3.72")
    implementation("org.jetbrains:kotlin-css-js:1.0.0-pre.104-kotlin-1.3.72")
    implementation("com.ccfraser.muirwik:muirwik-components:0.5.1")
    implementation(npm("styled-components"))
    implementation(npm("inline-style-prefixer"))
    implementation(npm("@material-ui/core", "^4.9.14"))

    testImplementation(kotlin("test-js"))
}

kotlin.target.browser { }

React

Container class components

I treat containers as Redux entry points into React, they probably should also contain the React state of the components, but I like to use state hooks for that. I wasn’t able to get rConnect working with functional components unfortunately.

interface TaskListConnectedProps : RProps {
    var isLoading: Boolean
    var tasks: Array<Task>
    var fetchTasks: () -> Unit
    var addTask: (task: Task) -> Unit
    var removeTask: (task: Task) -> Unit
}

private interface StateProps : RProps {
    var isLoading: Boolean
    var tasks: Array<Task>
}

private interface DispatchProps : RProps {
    var fetchTasks: () -> Unit
    var addTask: (task: Task) -> Unit
    var removeTask: (task: Task) -> Unit
}

private class TaskListContainer(props: TaskListConnectedProps) 
    : RComponent<TaskListConnectedProps, RState>(props) {

    override fun componentDidMount() {
        props.fetchTasks()
    }

    override fun RBuilder.render() {
        child(taskList) {
            attrs.isLoading = props.isLoading
            attrs.tasks = props.tasks
            attrs.addTask = props.addTask
            attrs.removeTask = props.removeTask
        }
    }
}

val taskListContainer: RClass<RProps> =
    rConnect<AppState, RAction, WrapperAction, RProps, StateProps, DispatchProps, TaskListConnectedProps>(
        { state, _ ->
            tasks = state.taskListState.tasks
            isLoading = state.taskListState.isLoading
        },
        { dispatch, _ ->
            fetchTasks = { dispatch(TaskListSlice.fetchTasks()) }
            addTask = { task -> dispatch(TaskListSlice.AddTask(task)) }
            removeTask = { task -> dispatch(TaskListSlice.RemoveTask(task)) }
        }
    )(TaskListContainer::class.js.unsafeCast<RClass<TaskListConnectedProps>>())

Functional components

They are the UI representation of the component with optional local react state provided by hooks. All components prefixed with m like mList, mButton are provided by Muirwik.

interface TaskListProps : RProps {
    var isLoading: Boolean
    var tasks: Array<Task>
    var addTask: (task: Task) -> Unit
    var removeTask: (task: Task) -> Unit
}

private object TaskListClasses : StyleSheet("TaskList", isStatic = true) {
    val loading by css {
        minHeight = LinearDimension("200px")
        display = Display.flex
        justifyContent = JustifyContent.spaceAround
        alignItems = Align.center
    }
    val actions by css {
        display = Display.flex
        justifyContent = JustifyContent.spaceAround
        alignItems = Align.center

        children {
            firstChild {
                margin = "5px 10px 0 0"
            }
        }
    }
    val task by css {
        margin = "10px 0"
    }
}

val taskList = functionalComponent<TaskListProps> { props ->
    val (taskName, setTaskName) = useState("")
    val (isTaskNameInvalid, setIsTaskNameInvalid) = useState(false)

    styledDiv {
        if (props.isLoading) {
            css(loading)
            mCircularProgress { }
        } else {
            styledDiv {
                css(TaskListClasses.actions)
                mTextField(
                    label = "Task",
                    variant = MFormControlVariant.outlined,
                    value = taskName,
                    error = isTaskNameInvalid,
                    fullWidth = true,
                    onChange = {
                        setIsTaskNameInvalid(false)
                        setTaskName(it.targetInputValue)
                    }
                )
                mButton(
                    caption = "Add",
                    variant = MButtonVariant.outlined,
                    onClick = {
                        if(taskName.isBlank()){
                            setIsTaskNameInvalid(true)
                        } else {
                            props.addTask(Task(taskName))
                            setTaskName("")
                        }
                    }
                )
            }
            mList {
                props.tasks.forEach { task ->
                    mCard {
                        css(TaskListClasses.task)
                        mListItem(button = true, onClick = { props.removeTask(task) }) {
                            + task.name
                        }
                    }
                }
            }
        }
    }
}

Redux

The state for the whole app, every reducer state will have their own property in this class.

data class AppState(
    val taskListState: TaskListSlice.State = TaskListSlice.State()
)

I like the idea which is used in Redux Toolkit where a single feature resides in a single slice. This slice contains all the information needed for the Redux reducer.

object TaskListSlice {
    data class State(
        val tasks: Array<Task> = emptyArray(),
        val isLoading: Boolean = true
    )

    private val fetchTaskThunk = FetchTaskThunk()

    fun fetchTasks(): RThunk = fetchTaskThunk

    data class SetIsLoading(val isLoading: Boolean): RAction

    data class SetTasks(val tasks: Array<Task>): RAction

    data class AddTask(val task: Task): RAction

    data class RemoveTask(val task: Task): RAction

    fun reducer(state: State = State(), action: RAction): State {
        return when (action) {
            is SetIsLoading -> state.copy(isLoading = action.isLoading)
            is SetTasks -> state.copy(tasks = action.tasks)
            is AddTask -> state.copy(tasks = state.tasks + action.task)
            is RemoveTask -> state.copy(tasks = state.tasks.filterNot { task -> task == action.task }.toTypedArray())
            else -> state
        }
    }
}

Reducers are combined using this function

fun combinedReducers() = combineReducersInferred(
    mapOf(
        AppState::taskListState to TaskListSlice::reducer
    )
)

// Credit https://github.com/JetBrains/kotlin-wrappers/blob/master/kotlin-redux/README.md
fun <S, A, R> combineReducersInferred(reducers: Map<KProperty1<S, R>, Reducer<*, A>>): Reducer<S, A> {
    return combineReducers(reducers.mapKeys { it.key.name })
}

And the store is created with the createStore function, note that the last argument of the compose function enables Redux dev tools.

val myStore = createStore<AppState, RAction, dynamic>(
    combinedReducers(),
    AppState(),
    compose(
        rThunk(),
        rEnhancer(),
        js("if(window.__REDUX_DEVTOOLS_EXTENSION__ )window.__REDUX_DEVTOOLS_EXTENSION__ ();else(function(f){return f;});")
    )
)

Redux thunk

This is the setup that I use for thunks

interface RThunk : RAction {
    operator fun invoke(
        dispatch: (RAction) -> WrapperAction,
        getState: () -> AppState
    ): WrapperAction
}

// Credit https://github.com/AltmanEA/KotlinExamples
fun rThunk() =
    applyMiddleware<AppState, RAction, WrapperAction, RAction, WrapperAction>(
        { store ->
            { next ->
                { action ->
                    if (action is RThunk)
                        action(store::dispatch, store::getState)
                    else
                        next(action)
                }
            }
        }
    )

val nullAction = js {}.unsafeCast<WrapperAction>()

Here’s one dummy thunk defined in this example app, after a timeout it dispatches a list of tasks to the reducer.

class FetchTaskThunk: RThunk {
    override fun invoke(dispatch: (RAction) -> WrapperAction, getState: () -> AppState): WrapperAction {
        dispatch(TaskListSlice.SetIsLoading(true))
        window.setTimeout({
            val tasks = listOf(
                Task("Kotlin"),
                Task("is"),
                Task("awesome")
            )
            dispatch(TaskListSlice.SetTasks(tasks.toTypedArray()))
            dispatch(TaskListSlice.SetIsLoading(false))
        }, 2000)

        return nullAction
    }
}

Webpack

To create a custom webpack config, there has to be a directory named webpack.config.d in the project root. In this directory any file with the extension config.js will be treated as the webpack config. Here’s the config of the example app:

config.devServer = Object.assign(
    {},
    config.devServer || {},
    {
        open: false,
        // port: 8080
    }
)

Note that I only override the fields that I need, and the rest is used from the default config. Overriding the whole config can and probably will break some things.

My experience and some tips

  • Use the —continuous argument for the gradle run command. It doesn’t work perfectly, but it is better than nothing. Usually the second page refresh contains the desired changes.
  • The error When accessing module declarations from UMD, they must be marked by both @JsModule and @JsNonModule is usually caused by the fact the @JsModule doesn't also have the @JsNonModule annotation. This can be fixed by creating a wrapper for the imports. Just like ReduxImportsWrapper.kt in this project.
  • If you’re using coroutines, the scope can be passed into the thunk as a constructor parameter, thanks to this you can cancel it if there is a need.
  • The Muirwik demo app has a great showcase of the available components and how to use them.

Here’s a list of resources that helped me immensely: