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

Step-by-step guide how I implemented a Kotlin/JS app with Redux and Material UI

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:

You've successfully subscribed to AKJAW
Great! Now you have full access to all members content.
Error! Could not sign up. invalid link.
Welcome back! You've successfully signed in.
Error! Could not sign in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.