typescriptsolid-js

Array of objects isn't reactive in SolidJS


I am unable to see what's wrong with this code, where I'm simply trying to update the object and get it refreshed in DOM, which doesn't happen. The console log does say that the object was updated, but why is this isn't updating my DOM?

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

function ToDo() {
    const getToDos = () => {
        return [{
            id: 1, name: 'one'
        }, {
            id: 2, name: 'two'
        }]
    }

    const onTodoClick = (id: number) => {
        const td = [...todos()];
        const elem = td.find(t => t.id === id);
        elem!.name = elem!.name + ' - Done';
        console.log(td);
        console.log(todos());
        setTodos(() => td);
    }

    const [todos, setTodos] = createSignal(getToDos());

    return (
        <div>
            <For each={todos()} >
                {(todo) => <p onClick={() => onTodoClick(todo.id)}>{todo.name}</p>}
            </For>
        </div>
    );
}

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


Solution

  • tl;dr, it's because const td = [...todos()]; is just a shallow copy of todos(), which does work when creating something directly dependent on that array, but <For> depends on the internals of the array, so you need to make a copy of the object you're modifying instead of directly changing the .name attribute.

    One way, similar to how you currently have your, that you can solve this is like so:

    const onTodoClick = (id: number) => {
        const td = [...todos()];
        const elemIndex = td.findIndex(t => t.id === id);
        td[elemIndex] = { ...td[elemIndex], name: td[elemIndex].name + ' - Done' }
        console.log(td);
        console.log(todos());
        setTodos(() => td);
    }
    

    You can tell that it's not directly dependent on todos() itself because if you change the createSignal call to be const [todos, setTodos] = createSignal(getToDos(), { equals: false }); it will always count it as the todos changing whenever you call setTodos (docs).

    If you check the source code of <For>, you can see that it, internally, uses createMemo(), which uses referential equality.

    A more modern way you can do this is using Array.prototype.with.

    const onTodoClick = (id: number) => {
        const elemIndex = todos().findIndex(t => t.id === id);
        const td = todos().with(elemIndex, {
          ...todos()[elemIndex],
          name: todos()[elemIndex].name + ' - Done'
        })
        console.log(td);
        console.log(todos());
        setTodos(() => td);
    }