I have a bit of a performance issue and would like...
# react
h
I have a bit of a performance issue and would like to ask what the best practices are. How can I update an element of a list, without triggering a rerender for all list elements? So in my code there is something along the lines of this (pseudo/untested):
Copy code
data class Profile(
    val name: String,
    val entries: List<ProfileEntries>
)

val testView = FC<Props> {
    val profile by fetchProfilesOverApi()

    profile.entries.forEach { entry ->
        Typography { +entry.title}
        TipTapEditor {
            content = entry.content
            onUpdate = { uContent ->
                entry.copy(content = uContent).let { uEntry ->
                    profile = profile.filter { it.id != uEntry.id }.plus(uEntry)
                }
            }
        }
    }
}
The problem is now even with a debounce in
onUpdate
it still rerenders all list elements, even the ones who haven't changed. Is there a more "react" way of doing this? A custom
Component
that has it's own state or something? Or a pattern where the changes are not applied to the fetched profiles, but are merged with the edited entries afterwards?
t
You have 2 memoization problems 1. Missed memoization for
onUpdate
handler 2.
TipTapEditor
- is it external component? Does it use
memo
?
h
TipTap is from here: https://tiptap.dev/docs/editor/getting-started/install/react How would memoization for update handler in general look like? I currently have
useMemo
for static content like column definitions and such.
t
memo
is for components
h
Okay, I read up on memo and will try to incorporate it.
t
Looks like
TipTapEditor
wrapping is required
Also you change entry position in list

https://youtu.be/fGxKOmCuH5w

h
Yes I already wrapped TipTap. Ohh thank you for that vid. It didn't occur to me so search for a video. I did a quick google search but it didn't come up. Thanks
I'm sorry for the tutoring session but using my example above I already had it like this:
Copy code
val testView = FC<Props> {
    val profile by fetchProfilesOverApi()

    profile.entries.forEach { entry ->
        Box {
            println("BOX Updated")
            key = entry.id
            Typography { +entry.title}
            TipTapEditor {
                content = entry.content
                onUpdate = { uContent ->
                    entry.copy(content = uContent).let { uEntry ->
                        profile = profile.filter { it.id != uEntry.id }.plus(uEntry)
                    }
                }
            }
        }
    }
}
But it still updates everything the amount of times that entries has elements. Edit: I think the multi updates are a different issue. (debounce)
Also a weird behaviour:
Copy code
val testView = FC<Props> {
    val profile by fetchProfilesOverApi() // profile.entries.size = 15
    val testState by useState(false) // not used anywhere

    useEffect(profile) { // When commented out "BOX Updated" is only printed 15 not 30 times.
        println("USE EFFECT")
        testState = true
    }

    profile.entries.forEach { entry ->
        Box {
            println("BOX Updated") // printed 2 x 15 times.
            key = entry.id // Int doesn't change for updated entries.
            TextField {
                value = entry.title
                onUpdate = { uTitle ->
                    entry.copy(title = uTitle).let { uEntry ->
                        profile = profile.filter { it.id != uEntry.id }.plus(uEntry)
                    }
                }
            }
        }
    }
}
This updates two times the whole list such that BOX Updated is printed 30 times when the
useEffect(profile) {...}
is present. If not it just updates the whole list once (15 prints). But it should only be 1 print since Box has the
entry.id
as key, no? The 15 updates might have to do with reference equality, I will test it with a mutableMap.
Okay, MutableMap is a bust, I forgot you have to set the state not just change the content.
@turansky Hey there, I hope you had a good weekend. I tested the behavior a little more, but even with a small example it doesn't seem to behave like I want to. For example this here:
Copy code
val PlaygroundView = FC<Props> { props ->
    var importantList by useState<Map<String, String>>(
        (0..10).associate { it.toString() to it.toString() }
    )
    var unimportantBoolean by useState(false)

    Typography {
        +unimportantBoolean.toString()
    }

    Grid {
        container = true
        importantList.forEach { (id, value) ->
            Grid {
                key = id
                item = true
                xs = 12
                Button {
                    key = id
                    variant = ButtonVariant.contained
                    onClick = {
                        importantList = importantList.minus(id)
                    }
                    +value
                }
            }
        }
    }
}
Behaves like the gif in the attachement. I also tried with the
id = key
inside the
Button
but the behaviour was the same.
In the DOM it seems to be working though:
I am reading up more on this, it seems I misunderstood the re-render cycle somewhat. React (often times) re-renders the whole Component on any state change and I either have to use memoization more aggressively or rethink my Component structure to avoid costly updates. Sorry to have bothered you.
blob no problem 1
I watched/read some on
memo
(and
useMemo
), but I can't get this example to work. How would I memoize this, so that on editing a textfield the others don't recalculate? I just cannot seem to figure it out no matter what I try:
Copy code
val PlaygroundView = FCIT<Props> { props ->
    var importantList by useState<Map<String, String>>(
        (0..10).associate { it.toString() to it.toString() }
    )
    var unimportantBoolean by useState(false)

    Typography { +unimportantBoolean.toString() }

    Grid {
        container = true
        importantList.forEach { (id, text) ->
            Grid {
                key = id
                item = true
                xs = 12
                Button {
                    variant = ButtonVariant.contained
                    onClick = { importantList = importantList.minus(id) }
                    +id
                }
                TextField {
                    value = text
                    onChange = { event ->
                        importantList = importantList.plus(id to event.target.asDynamic().value)
                    }
                }
            }
        }
    }
}
This also doesn't work:
Copy code
external interface PlaygroundViewProps : Props {
    var idKey: String
    var text: String
    var onChange: (FormEvent<HTMLDivElement>) -> Unit
}

val PGListItem = FC<PlaygroundViewProps> { props ->
    Grid {
        key = props.idKey
        item = true
        xs = 12
        Button {
            variant = ButtonVariant.contained
            +props.idKey
        }
        TextField {
            value = props.text
            onChange = props.onChange
        }
    }
}

val MemoPGListItem = memo(PGListItem)

val PlaygroundView = FCIT<Props> { props ->
    var importantList by useState<Map<String, String>>(
        (0..10).associate { it.toString() to it.toString() }
    )
    var unimportantBoolean by useState(false)

    Typography { +unimportantBoolean.toString() }

    Grid {
        container = true
        importantList.forEach { (id, text) ->
            MemoPGListItem {
                idKey = id
                this.text = text
                onChange = { event ->
                    importantList = importantList.plus(id to (event.target as HTMLInputElement).value)
                }
            }
        }
    }
}
I wonder, is the
.forEach
the culprit since it creates a new Component when iterating or something?
t
It's because you create new
onChange
handler every time
h
Yeah I just found a video that explains that the onChange is created new every time so that the equality check fails and I need to use
useCallback
. Currently trying it out will report back in a second 😄
👌 1
Ah sorry I had an Impromptu meeting. This is working perfectly now:
Copy code
external interface PGListItemProps : Props {
    var idKey: String
    var text: String
    var onChange: (idKey: String, event: FormEvent<HTMLDivElement>) -> Unit
}

val PGListItem = FC<PGListItemProps> { props ->
    Grid {
        key = props.idKey
        item = true
        xs = 12
        TextField {
            value = props.text
            onChange = {
                props.onChange(props.idKey, it)
            }
        }
    }
}

// This initialization must happen outside of the Component, otherwise it would be un- and re-mounted every time.
val MemoPGListItem = memo(PGListItem)

val PlaygroundView = FC<Props> { props ->
    // We cannot use `by` here because the `useCallback` needs to remain a pure function that doesn't depend on volatile objects.
    var (importantList, setImportantList) = useState<Map<String, String>>(
        (0..10).associate { it.toString() to it.toString() }
    )
    var unimportantBoolean by useState(false)
    
    // `useCallback` prevents the `PGListItemProps` from being detected as changed due to new anonymous `onChange` functions.
    val handleSubmit = useCallback { idKey: String, event: FormEvent<HTMLDivElement>  ->
        setImportantList { prev ->
            prev.plus(idKey to (event.target as HTMLInputElement).value)
        }
    }

    Typography { +unimportantBoolean.toString() }

    Divider

    Grid {
        container = true
        importantList.map { (id, text) ->
            MemoPGListItem {
                idKey = id
                this.text = text
                onChange = handleSubmit
            }
        }
    }
}
Maybe this helps someone who finds this via google or something.
t
onChange: (id, value) -> Unit
?
h
You mean in
PGListItemProps
?
t
Yes
h
I don't know what you mean, should I remove the callback from the props?
t
You can calculate new value inside
PGListItem
h
Ah yes, you mean instead of handing over the event, draw out the value first and only pass the value yes.
👌 1
I was already thinking about wrapping it up a little to save boilerplate code. The setup is a little big for something that was just
list.forEach { showStuff(it) }
before.
Anyway, thank you for always helping .
How would you go about it when the value for the callback does not come from within the component?
Copy code
external interface PGListItemProps : Props {
    var mapId: String
    var text: String
    var onChange: (String, FormEvent<HTMLDivElement>) -> Unit
}

val PGListItem = FC<PGListItemProps> { props ->
    Grid {
        item = true
        xs = 10
        TextField {
            value = props.text
            onChange = {
                props.onChange(props.mapId, it)
            }
        }
    }
}

external interface PGButtonProps : Props {
    var onClick: () -> Unit
}

val PGButton = FC<PGButtonProps> { props ->
    Grid {
        item = true
        xs = 2
        Button {
            variant = ButtonVariant.contained
            onClick = { props.onClick() }

            +"Delete"
        }
    }
}

// This initialization must happen outside of the Component, otherwise it would be un- and re-mounted every time.
val MemoPGListItem = memo(PGListItem)
val MemoPGButton = memo(PGButton)

val ListInputExample = FCIT<Props> { props ->
    // We cannot use `by` here because the `useCallback` needs to remain a pure function that only uses other functions and not objects.
    var (importantList, setImportantList) = useState<Map<String, String>>(
        (0..500).associate { it.toString() to it.toString() }
    )
    var unimportantBoolean by useState(false)

    // `useCallback` prevents the `PGListItemProps` from being detected as changed due to new anonymous `onChange` functions.
    val handleSubmit = useCallback { idKey: String, event: FormEvent<HTMLDivElement>  ->
        setImportantList { prev ->
            prev.plus(idKey to (event.target as HTMLInputElement).value)
        }
    }

    val handleClick = useCallback { idKey: String ->
        setImportantList { prev ->
            prev.minus(idKey)
        }
    }
// (X) Crash because amount of hooks changes when an entry is deleted    
//    fun handleClick(idKey: String) = useCallback {
//        setImportantList { prev ->
//            prev.minus(idKey)
//        }
//    }

    Box {
        sx { padding = spacing(2) }
        Paper {
            variant = PaperVariant.outlined
            Typography { +unimportantBoolean.toString() }
            Divider
            Grid {
                container = true
                importantList.map { (id, text) ->
                    MemoPGButton {
                        key = "button-$id"
                        //onClick = { handleClick(id) } // (X) anonymous lambda causes equality check to fail. Poor performance
                        //onClick = handleClick(id) // Not possible because of the wrong signature.
                    }
                    MemoPGListItem {
                        key = "text-$id"
                        mapId = id
                        this.text = text
                        onChange = handleSubmit
                    }
                }
            }
        }
    }
}
t
I don't understand question unfortunately
h
It's mostly about the
PGButton
in this example, in the
PGListItem
you can see that I put the
mapId
into the props, so the callback of
PGListItem
can send it back. But what if I cannot put the
mapId
(I should have named it mapKey) into the props of the component? In the example both click listeners are commented out, one won't compile and the other is not performant because the anonymous lambda does not pass equality checks between rerenders. just doing
fun handleClick(string) = useCallback { }
instead of
val handleClick = useCallback { string -> }
also does not work, it also fails the equality check.
Hey I'm sorry to pick this up again, maybe this is more concise:
Copy code
val MemoButton = memo(Button)

val TestView = FC<Props> { props ->
    val (someMap, setSomeMap) = useState(
        (0..500).associate { it.toString() to it.toString() }
    )
    val handleClick = useCallback { event: MouseEvent<HTMLButtonElement, *>  -> 
        println("I'm stable, but do nothing.")
    }
    fun funHandleClick(mapKey: String) = useCallback { event: MouseEvent<HTMLButtonElement, *>  ->
        println("I remove the key, but change the amount of rendered hooks which crashes View")
        setSomeMap { prev -> prev.remove(mapKey) }
    }
    val workingHandleClick = useCallback { mapKey: String ->
        println("I'm stable and remove the key and don't change the amount of rendered hooks")
        setSomeMap { prev -> prev.remove(mapKey) }
    }

    someMap
        .entries
        .forEach { (mapKey, text) ->
            MemoButton {
                key = mapKey
                onClick = handleClick // Works and is stable, but doesn't delete key from map.
                onClick = funHandleClick(mapKey) // Removes the key, but creashes because the amount of (useCallback) hooks changes.
                onClick = { workingHandleClick(mapKey) } // Removes the key and doesn't crash, but breaks memoization, so bad performance.
            }
        }
    }
}
Also this just breaks the render process without any Error messages in console:
Copy code
val MemoButton = memo(Button)
val TestView = FC<Props> { props ->
    val (someMap, setSomeMap) = useState(
        (0..500).associate { it.toString() to it.toString() }
    )
    val handleOriginalClick = useCallback { event: MouseEvent<HTMLButtonElement, *>  -> 
        println("Click")
    }
    val handleClick = useCallback { mapKey: String ->
        setSomeMap { prev -> prev.remove(mapKey) }
        handleOriginalClick 
    }
    someMap
        .entries
        .forEach { (mapKey, text) ->
            MemoButton {
                key = mapKey
                onClick = handleClick(mapKey) // Compiles, but aborts render without any console output
            }
        }
    }
}