Skip to content

[10.0.0-alpha.4] DOM stability dependent on order of parent/child hook calls #1524

@natevw

Description

@natevw

I have a parent component that tracks an array, rendering a child component that renders and allows edits to the array:

const Parent = () => {
  let [items, setItems] = useState([]);
  return html`<${Child} items=${items} setItems=${setItems} />`;
}

In this child component, the existing items are editable and additionally there is a "pending item" input for adding a new item to the array:

const Child = ({items,setItems}) => {
  let [pendingId, setPendingId] = useState(null);
  if (!pendingId) {
    setPendingId(pendingId = Math.random().toFixed(20).slice(2));
  }
  
  return html`<div class="item-editor">
    ${items.map((item,idx) => html`<input key=${item._id}
      value=${item.val}
      oninput=${evt => {
        let val = evt.target.value,
            _items = [...items];
        _items.splice(idx, 1, {...item,val});
        setItems(_items);
      }}
    />`)}
    
    <input key=${pendingId} placeholder="type to add an item"
      oninput=${evt => {
        let val = evt.target.value;
            _items = [...items];
        _items.push({_id:pendingId, val});
        setItems(_items);
        setPendingId(null);
      }}
    />
  </div>`;
};

The problem is this: when using the code as written above, the key=${pendingId} input gets completely replaced in the DOM as soon as the user enters one letter.

Because that "pending" identifier is passed along into the new item my expectation was that, on the next render, the focused input should remain in the DOM and get reconciled with the lastmost of the newly rendered key=${item._id} inputs.

But instead, the input ends up getting removed from the DOM and the user's focus is lost!

Workaround

If I take the two setters that together add the pending item into the array and reset the pending id:

setItems(_items);
setPendingId(null);

And simply re-order the calls so that the child's pendingId state gets set before the parent's items state like so:

setPendingId(null);
setItems(_items);

Then the input is reconciled as expected, i.e. the user's focus is retained when the new input is inserted after the one they are using!

To reproduce

This code is available ready-to-run (plus some additional logging) at https://codesandbox.io/s/74jll4q4v1.

  1. Leave the "Set child state before parent state." configuration unchecked.
  2. Start typing in the placeholder text input

Expected results:

Despite a new placeholder getting inserted, the keyboard focus should be retained, and you should be able to keep typing. (And, if you check the "set child state before…" box it works this way!)

Actual results:

When the new placeholder is inserted, keyboard focus is completely lost. Debug logs indicate that the original placeholder input has been removed from the DOM, and both of the inputs now in the DOM are brand new.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions