SolidJS: input field loses focus when typing

Viewed 443

I have a newbie question on SolidJS. I have an array with objects, like a to-do list. I render this as a list with input fields to edit one of the properties in these objects. When typing in one of the input fields, the input directly loses focus though.

How can I prevent the inputs to lose focus when typing?

Here is a CodeSandbox example demonstrating the issue: https://codesandbox.io/s/6s8y2x?file=/src/main.tsx

Here is the source code demonstrating the issue:

import { render } from "solid-js/web";
import { createSignal, For } from 'solid-js'

function App() {
  const [todos, setTodos] = createSignal([
    { id: 1, text: 'cleanup' },
    { id: 2, text: 'groceries' },
  ])

  return (
    <div>
      <div>
        <h2>Todos</h2>
        <p>
          Problem: whilst typing in one of the input fields, they lose focus
        </p>
        <For each={todos()}>
          {(todo, index) => {
            console.log('render', index(), todo)
            return <div>
              <input
                value={todo.text}
                onInput={event => {
                  setTodos(todos => {
                    return replace(todos, index(), {
                      ...todo,
                      text: event.target.value
                    })
                  })
                }}
              />
            </div>
          }}
        </For>
        Data: {JSON.stringify(todos())}
      </div>
    </div>
  );
}

/*
 * Returns a cloned array where the item at the provided index is replaced
 */
function replace<T>(array: Array<T>, index: number, newItem: T) : Array<T> {
  const clone = array.slice(0)
  clone[index] = newItem
  return clone
}

render(() => <App />, document.getElementById("app")!);

UPDATE: I've worked out a CodeSandbox example with the problem and the three proposed solutions (based on two answers): https://codesandbox.io/s/solidjs-input-field-loses-focus-when-typing-itttzy?file=/src/App.tsx

3 Answers

<For> components keys items of the input array by the reference. When you are updating a todo item inside todos with replace, you are creating a brand new object. Solid then treats the new object as a completely unrelated item, and creates a fresh HTML element for it.

You can use createStore instead, and update only the single property of your todo object, without changing the reference to it.

const [todos, setTodos] = createStore([
   { id: 1, text: 'cleanup' },
   { id: 2, text: 'groceries' },
])
const updateTodo = (id, text) => {
   setTodos(o => o.id === id, "text", text)
}

Or use an alternative Control Flow component for mapping the input array, that takes an explicit key property: https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#Key

<Key each={todos()} by="id">
   ...
</Key>

While @thetarnav solutions work, I want to propose my own. I would solve it by using <Index>

import { render } from "solid-js/web";
import { createSignal, Index } from "solid-js";

/*
 * Returns a cloned array where the item at the provided index is replaced
 */
function replace<T>(array: Array<T>, index: number, newItem: T): Array<T> {
  const clone = array.slice(0);
  clone[index] = newItem;
  return clone;
}

function App() {
  const [todos, setTodos] = createSignal([
    { id: 1, text: "cleanup" },
    { id: 2, text: "groceries" }
  ]);

  return (
    <div>
      <div>
        <h2>Todos</h2>
        <p>
          Problem: whilst typing in one of the input fields, they lose focus
        </p>
        <Index each={todos()}>
          {(todo, index) => {
            console.log("render", index, todo());
            return (
              <div>
                <input
                  value={todo().text}
                  onInput={(event) => {
                    setTodos((todos) => {
                      return replace(todos, index, {
                        ...todo(),
                        text: event.target.value
                      });
                    });
                  }}
                />
              </div>
            );
          }}
        </Index>
        Dat: {JSON.stringify(todos())}
      </div>
    </div>
  );
}
render(() => <App />, document.getElementById("app")!);

As you can see, instead of the index being a function/signal, now the object is. This allows the framework to replace the value of the textbox inline. To remember how it works: For remembers your objects by reference. If your objects switch places then the same object can be reused. Index remembers your values by index. If the value at a certain index is changed then that is reflected in the signal.

This solution is not more or less correct than the other one proposed, but I feel this is more in line and closer to the core of Solid.

With For, whole element will be re-created when the item updates. You lose focus when you update the item because the element (input) with the focus gets destroyed, along with its parent (li), and a new element is created.

You have two options. You can either manually take focus when the new element is created or have a finer reactivity where element is kept while the property is updated. The indexArray provides the latter out of the box.

The indexArray keeps the element references while updating the item. The Index component uses indexArray under the hood.

function App() {
  const [todos, setTodos] = createSignal([
    { id: 1, text: "cleanup" },
    { id: 2, text: "groceries" }
  ]);

  return (
    <ul>
      {indexArray(todos, (todo, index) => (
        <li>
          <input
            value={todo().text}
            onInput={(event) => {
              const text = event.target.value;
              setTodos(todos().map((v, i) => i === index ? { ...v, text } : v))
            }}
          />
        </li>
      ))}
    </ul>
  );
}

Note: For component caches the items internally to avoid unnecessary re-renders. Unchanged items will be re-used but updated ones will be re-created.

Related