SolidJS does not support reactivity for arrays of objects

I've been scratching my head trying to figure out why this code isn't working as expected. I'm simply updating an object and expecting it to be refreshed in the DOM, but for some reason, that's not happening. The console log confirms that the object was updated, so why isn't it reflecting in 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")!);

Answer №1

In short, the reason why const td = [...todos()]; is used instead of directly modifying the array is because it creates a shallow copy that works well for certain cases but not when using <For>, which relies on the internal structure of the array. Therefore, it's necessary to create a copy of the object being modified instead of changing the .name attribute directly.

A way to solve this issue is by following a similar approach to what you have already implemented:

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);
}

The usage of createSignal in

const [todos, setTodos] = createSignal(getToDos(), { equals: false });
indicates that changes to todos will be tracked each time setTodos is called (docs).

Upon inspecting the source code of <For>, it becomes evident that it internally uses createMemo(), which depends on referential equality.

An alternative modern method involves utilizing 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);
}

Answer №2

The problem lies in the method you are using to update the element. Instead of directly manipulating the element like this:

const elem = td.find(t => t.id === id);
elem!.name = elem!.name + ' - Done';

You should be updating the array immutably. Consider doing something like this instead:

const td = todos().map(todo=>todo.id===id ? {...todo, name:todo.name+'done'} : todo)

A more concise way of achieving this is by updating the array directly in the callback function:

 setTodos(todos=>todos.map(todo=>todo.id===id ? {...todo, name:todo.name+'done'} : todo));

If you are working with TypeScript, consider using readonly types for objects and arrays to prevent unintended mutations.

I hope this explanation helps clarify things for you.

Answer №3

When Signals checks for a new value using the `===` operator, it will only call the effects if the value is actually set. It's important to note that objects are compared by reference, so if you mutate an object, Solid's runtime won't recognize the change as an update.

This condition is met in the following code snippet:

const td = [...todos()];

The `For` component also relies on this feature, as it takes each value and creates an array of signals to update individual DOM elements associated with each item. It uses object references as keys to cache DOM nodes, returning previously created elements or creating new ones when needed.

To better understand this concept, you can refer to:

Since mutating an array item causes `For` to return the cached DOM element, the UI remains unaffected:

const elem = td.find(t => t.id === id);
elem!.name = elem!.name + ' - Done';

To resolve this issue, it's recommended to create a new todo object instead:

const onTodoClick = (id: number) => {
  setTodos(prev => prev.map(item => {
    if (item.id !== id) return item;
    return { ...item, done: true };
  }));
}

In the above code snippet, I used the callback form of the setter function for clarity. The `done` property is added to mark an item as complete, allowing for easy customization when rendering items with different styles or HTML tags based on their completion status.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Using Typescript to import Eslint with the `import/named` method

My project utilizes Eslint with the following configurations: parser: @typescript-eslint/parser 1.4.2 plugin: @typescript-eslint/eslint-plugin 1.4.2 resolver: eslint-import-resolver-typescript 1.1.1 rule extends: airbnb-base and plugin:@typescript-eslint ...

Having difficulty converting string columns/data to numerical values when exporting an Ag-Grid table to Excel with getDataAsExcel function

I am having an issue with exporting data from my ag-grid table to excel using the API method getDataAsExcel. The problem arises because I have a column containing only string values, while all other columns are numeric and displayed in the ag-grid table a ...

Exploring the methods for monitoring multiple UDP ports on a single address in Node.js within a single process

I am currently working on developing a Node.js application to manage a small drone. The SDK provides the following instructions: To establish a connection between the Tello and a PC, Mac, or mobile device, use Wi-Fi. Sending Commands & Receiving Responses ...

Tips for picking out a particular item from a list of child elements

When I select the first parent's children array, it ends up selecting every other parent's children as well. This is due to index 0 remaining the same for all of them. How can I specify and highlight a specific child? Link: Check out the stackb ...

Merge the values of checkboxes into a single variable

How can I gather all the checkbox values from my list and combine them into a single variable? This is the structure of my HTML: <div class="card" *ngFor="let event of testcases" > <input class="form-check-input" ...

The static method in TypeScript is unable to locate the name "interface"

Is it possible to use an interface from a static method? I'm encountering an issue and could really use some help. I've been experimenting with TypeScript, testing out an interface: interface HelloWorldTS { name : string; } Here&ap ...

Utilizing AWS Websockets with lambda triggers to bypass incoming messages and instead resend the most recent message received

I am facing an issue when invoking a lambda that sends data to clients through the websocket API. Instead of sending the actual message or payload, it only sends the last received message. For example: Lambda 1 triggers Lambda 2 with the payload "test1" ...

Enhancing IntelliSense to recognize exports specified in package.json

I have a package.json file where I define various scripts to be exported using the exports field. "exports": { ".": { "default": "./dist/main.es.js", "require": "./dist/main.cjs.js", ...

Typescript: Potential null object error when defining a method

I recently encountered an error message stating "Object is possibly null" while working on the changePageSize method in book-store.component.html. It seems like I need to initialize the object within the class, but I'm not familiar with how to do that ...

The disappearing act of embedded Twitter timelines in Ionic 2

My code displays the timeline initially, but it disappears when I navigate away from the view. Can anyone help me troubleshoot this issue? Upon first visit to the view, the timeline is visible, but upon returning, it disappears. Below is the code snippet ...

Spotlight a newly generated element produced by the*ngFor directive within Angular 2

In my application, I have a collection of words that are displayed or hidden using *ngFor based on their 'hidden' property. You can view the example on Plunker. The issue arises when the word list becomes extensive, making it challenging to ide ...

Determine the type of embedded function by analyzing the callback

Are you struggling to create a function that takes an object and returns a nested function, which in turn accepts a callback and should return the same type of function? It seems like achieving this with the same type as the callback is posing quite a chal ...

The error message "The file 'environment.ts' is not located within the specified 'rootDir' directory" was encountered during the Angular Library build process

When attempting to access the environment variable in an Angular 9 library, I encountered an error during the library build process. Here is how it was implemented: import { EnvironmentViewModel } from 'projects/falcon-core/src/lib/view-models/envir ...

Ways to expand the DOM Node type to include additional attributes

I've been diving into typescript and transitioning my current code to use it. In the code snippet below, my goal is: Retrieve selection Get anchorNode from selection -> which is of type 'Node' If anchorNode has attributes, retrieve attr ...

Access values of keys in an array of objects using TypeScript during array initialization

In my TypeScript code, I am initializing an array of objects. I need to retrieve the id parameter of a specific object that I am initializing. vacancies: Array<Vacancy> = [{ id: 1, is_fav: this.favouritesService.favourites.find(fav = ...

Tips for accessing the StaticRouterContext in Typescript with react-router-dom

Currently, I am implementing SSR for my app specifically targeting robots. There is a possibility that the render of the <App/> component may lead to a route not being found. In order to handle this scenario, I need to identify when the render ends ...

Utilizing a navigation menu to display various Strapi Collection pages within a single Angular Component

I have set up a collection in Strapi called Pages and I am looking to display them in the same component using my Navigation Bar Component. However, I am unsure of how to achieve this. Currently, all the data from the Collection is being displayed like th ...

Organize by a collection of strings or a collection of enums

Here is a list of objects that I have: enum MealType { Breakfast, Lunch, Dinner } interface FoodItem { name: string, type: MealType[], } const foodItems: FoodItem[] = [ { name: 'Pizza', type: [MealType.Lunch, MealType.Dinner ...

Arranging information by utilizing arrays

I am working on a component called components.ts in my Angular project. My goal is to create a function that sorts an array based on counts, and then utilize the sorted data in my HTML to generate a chart. import { Component } from '@angular/core&apo ...

Custom typings for Next-Auth profile

I'm experiencing an issue with TypeScript and Next Auth type definitions. I followed the documentation guidelines to add my custom types to the NextAuth modules, specifically for the Profile interface in the next-auth.d.ts file. It successfully adds t ...