I'm trying to create a custom form field that displays and edits string array as an editable ul list - the field creates a bullet for every string item in the array.
The issue is when I try to manage the field's state with a value and onChange props (like a regular form field) - the field duplicates the values that are being displayed and as a result, it's also set the value with the duplicates.
This is a piece of code that reproduces the issue:
import { useMemo, useState, type FormEvent } from 'react';
export default function List() {
const [value, setValue] = useState<string[]>(['first line', 'second line']);
const listItems = useMemo(
() => Array.from(Array.isArray(value) ? value : [value]),
[value]
);
function handleInput(event: FormEvent) {
const items = event.currentTarget.innerHTML
.split(/<li>(.*?)<\/li>/)
?.map((item: string) => item.replace(/<(.*?)>/g, ''))
?.filter(Boolean);
setValue(items);
}
return (
<ul
contentEditable
suppressContentEditableWarning
onInput={handleInput}
>
{listItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}
I use a local state to demonstrate the issue but in reality, the value and onChange will be injected as props
Some conclusions I've come to while playing with this:
- There is a react inner state that manages the
ulelement inner HTML perfectly. A temporary solution I use is to callonChangeon theul'sonBlurevent. - When I use
dangerouslySetInnerHTMLand set thelias strings - The cursor jumps to the beginning of the list for every stroke and I can't set the cursor position because I don't have a reference to the current edited line - but that's an issue or another question
Thanks

use react-contenteditable
The EditableListField component takes in two props: value and onChange. value is an array of strings that represent the list items, and onChange is a function that updates the list items. The EditableListField component maintains a state variable html that stores the HTML representation of the list. When the value prop changes, the useEffect hook updates html to reflect the new list items. The handleChange function is triggered when the user edits the list. It parses the updated HTML, extracts the list items, and calls the onChange prop with the new list items.
The App component maintains a state variable listItems that stores the current list items. It renders the EditableListField component and passes listItems and a setter function setListItems as props.
If you wanna play around with the code