How do you handle debouncing input as to not have ...
# javascript
h
How do you handle debouncing input as to not have state updates on larger data? In smaller forms it's no problem. If it's just one
TextField
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.
Copy code
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.
a
@turansky ^^
h
I researched some more and worked on it and am currently testing this design:
Copy code
val (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?
t
Copy code
val planFlow = MutableStateFlow(displayPlan)
val stepFlow = MutableStateFlow(displaySteps)
You create new flows on every render as I see - it doesn't look like expected
h
I wanted to test if I can replace the flows with a js timeout function. The flows seem to work as expected though. About the list of elements. I am currently testing if the performance is better when I create a new FC that takes the step as props and does the whole immediate/debounced shebang internally and only calls the
onUpdate
with the debounced value.
Currently testing this:
Copy code
external 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
     [...]
}
Ok this works like a charm:
Copy code
external 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?
Ok it seems I can remove the
flow
stuff and just do:
Copy code
useEffect(displayStep, displayTipTap) {
    delay(800)
    props.onUpdate(displayStep.copy(tipTapJson = JSON.stringify(displayTipTap)))
}