Hildebrandt Tobias
03/12/2025, 8:32 AMTextField
and a few CheckBox
I can just update the state with onUpdate/onChange
. But with larger ones it becomes laggy.
But if I use the structure below for larger/more complex forms the code gets long/big really fast.
For example a single TextField
for a title, there I theoretically need 3 variables with 2 states.
var loadedTitle = useLoader.unsafeCast<String>()
val editedTitle by useState("")
val immediateTitle by useState("")
Then onChange
needs to update immediateTitle
directly and editedTitle
after a debounce.
The loadedTitle
is to reset the form if need be.
Now that is fine for some form elements, or if I can cover all of them with a single DTO,
but is this really the best practice or is there a better way?
Edit: Also I can't use the larger DTO for the immediate
state, then I'd just have the same problem es before, so I need separate immediate states
for each title, (tiptap) editor content, checkbox, image, etc. to have them react snappy even if the underlying DTO List has already grown big.Artem Kobzar
03/12/2025, 10:22 AMHildebrandt Tobias
03/12/2025, 10:23 AMval (loadedPlan, loadedComponents) = useLoaderData().unsafeCast<Pair<ProductionPlanDto?, Array<ComponentDto>?>>()
var initialPlan by useState(loadedPlan ?: ProductionPlanDto.new(companyId))
var editedPlan by useState(initialPlan)
var displayPlan by useState(initialPlan.toDisplayPlan())
var displaySteps by useState(initialPlan.stepList.associateBy { it.id })
val planFlow = MutableStateFlow(displayPlan)
val stepFlow = MutableStateFlow(displaySteps)
useEffectWithCleanup(displayPlan) {
planFlow.value = displayPlan
MainScope().launch { planFlow
.debounce(800)
.collect {
editedPlan = editedPlan.copy(
title = it.title,
version = it.version,
description = it.description,
)
}
}.let {
onCleanup { it.cancel() }
}
}
I hope it's clear despite the missing context of what it means exactly.
I decided to name the states that hold the immediate changes display
and the debounced ones edited
(DisplayPlan data class is a subset of ProductionPlanDto with only title, version and description)
Is this an acceptable approach or is there an even better way?
On a first test, it at least seems to do what I want.
Do the cheap changes immediately, and the costly ones debounced.
What is still a Problem.
The stepList
or rather the displaySteps
state is still a growing list.
So when I edit one of the steps the whole list is updated and rerenders the whole displaySteps.entries.forEach
.
But of course I cannot declare a state for each entry of the list, since it's dynamicaly changed.
This isn't just a KotlinJS/React problem I'd wager, how do seasoned JS Frontend devs handle this?turansky
03/12/2025, 1:10 PMval planFlow = MutableStateFlow(displayPlan)
val stepFlow = MutableStateFlow(displaySteps)
You create new flows on every render as I see - it doesn't look like expectedHildebrandt Tobias
03/12/2025, 1:12 PMonUpdate
with the debounced value.Hildebrandt Tobias
03/12/2025, 1:16 PMexternal interface ProductionTipTapProps : Props {
val scrollRef: RefObject<dynamic>
val step: ProductionStepDto
val onUpdate: (ProductionStepDto) -> Unit
}
val ProductionTipTap = FCIT<ProductionTipTapProps> { props ->
val displayStep by useState(props.step)
val stepFlow = MutableStateFlow(displayStep)
useEffectWithCleanup(displayStep) {
stepFlow.value = displayStep
MainScope().launch { stepFlow
.debounce(800) // 800ms delay
.collect {
props.onUpdate(it)
}
}.let {
onCleanup { it.cancel() }
}
}
Paper {
key = props.step.id.value.toString()
variant = PaperVariant.outlined
ref = props.scrollRef
[...]
}
Hildebrandt Tobias
03/12/2025, 1:34 PMexternal interface ProductionTipTapProps : Props {
var step: ProductionStepDto
var onUpdate: (ProductionStepDto) -> Unit
}
@OptIn(FlowPreview::class)
val ProductionTipTap = FCIT<ProductionTipTapProps> { props ->
var displayStep by useState(props.step)
var displayTipTap by useState<dynamic>(JSON.parse(props.step.tipTapJson))
val stepFlow = MutableStateFlow(displayStep)
useEffectWithCleanup(displayStep, displayTipTap) {
stepFlow.value = displayStep
MainScope().launch { stepFlow
.debounce(800) // 800ms delay
.collect {
props.onUpdate(displayStep.copy(tipTapJson = JSON.stringify(displayTipTap)))
}
}.let {
onCleanup { it.cancel() }
}
}
TipTapEditor {
extensions = editorExtensions
content = displayTipTap //JSON.parse(step.tipTapJson)
onUpdate = { update ->
displayTipTap = update.editor.getJSON()
}
}
}
So each list element has it's own little debounce.
I hope this is at least close to what the best practice is?Hildebrandt Tobias
03/12/2025, 1:41 PMflow
stuff and just do:
useEffect(displayStep, displayTipTap) {
delay(800)
props.onUpdate(displayStep.copy(tipTapJson = JSON.stringify(displayTipTap)))
}