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
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: