Hildebrandt Tobias
04/24/2025, 11:00 AMdata 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?turansky
04/24/2025, 11:15 AMonUpdate
handler
2. TipTapEditor
- is it external component? Does it use memo
?Hildebrandt Tobias
04/24/2025, 11:20 AMuseMemo
for static content like column definitions and such.turansky
04/24/2025, 11:20 AMmemo
is for componentsHildebrandt Tobias
04/24/2025, 11:27 AMturansky
04/24/2025, 11:28 AMTipTapEditor
wrapping is requiredturansky
04/24/2025, 11:31 AMHildebrandt Tobias
04/24/2025, 11:33 AMHildebrandt Tobias
04/24/2025, 11:54 AMval 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)Hildebrandt Tobias
04/24/2025, 12:50 PMval 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.Hildebrandt Tobias
04/24/2025, 6:02 PMHildebrandt Tobias
04/28/2025, 8:25 AMval 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.Hildebrandt Tobias
04/28/2025, 8:41 AMHildebrandt Tobias
04/28/2025, 8:52 AMHildebrandt Tobias
04/28/2025, 10:38 AMmemo
(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:
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)
}
}
}
}
}
}
Hildebrandt Tobias
04/28/2025, 11:03 AMexternal 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?turansky
04/28/2025, 11:21 AMonChange
handler every timeHildebrandt Tobias
04/28/2025, 11:22 AMuseCallback
.
Currently trying it out will report back in a second 😄Hildebrandt Tobias
04/28/2025, 12:15 PMexternal 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.turansky
04/28/2025, 12:41 PMonChange: (id, value) -> Unit
?Hildebrandt Tobias
04/28/2025, 12:43 PMPGListItemProps
?turansky
04/28/2025, 12:44 PMHildebrandt Tobias
04/28/2025, 12:45 PMturansky
04/28/2025, 12:46 PMPGListItem
Hildebrandt Tobias
04/28/2025, 12:47 PMHildebrandt Tobias
04/28/2025, 12:49 PMlist.forEach { showStuff(it) }
before.Hildebrandt Tobias
04/28/2025, 12:54 PMHildebrandt Tobias
05/08/2025, 2:27 PMexternal 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
}
}
}
}
}
}
turansky
05/08/2025, 2:33 PMHildebrandt Tobias
05/08/2025, 4:27 PMPGButton
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.Hildebrandt Tobias
05/12/2025, 10:33 AMval 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.
}
}
}
}
Hildebrandt Tobias
05/12/2025, 11:36 AMval 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
}
}
}
}
Hildebrandt Tobias
05/12/2025, 12:28 PM