Stencil: Array update does not affect item reference within JSX events

Created on 12 Dec 2017  路  12Comments  路  Source: ionic-team/stencil

Stencil version:

 @stencil/[email protected]

I'm submitting a:

Current behavior:
I have a component with an array State() json; which I iterate over in my render function to create a list of input fields similar to this:

return (
  <section>
    {this.json
      ?
      this.json.map((exercise) =>
        <div>
          <td>{exercise.id}</td>
          <td><input type="text" value={exercise.name}
                      onInput={(ev) => exercise.name = (ev.target as HTMLInputElement).value}></input></td>
          <td><input type="text" value={exercise.description}
                      onInput={(ev) => exercise.description = (ev.target as HTMLInputElement).value}></input></td>
        </div>
      )
      : ""
    }
  </section>
);

The problem here is that when I update json somewhere with new data the onInput functions don't update the new array but seem to apply on the previous array.
The rerendering seems to be triggered though. I don't really know much about how stencil and jsx works so maybe I'm just making a stupid mistake or something.
I'm updating the array by assigning a completely new array with new data. And the new values are even shown in the UI just the onInput seems to be affected.

Expected behavior:
When updating the exercise name & description in the onInput event this should always update the exercise of the current json array.

Most helpful comment

Hi, I'm experiencing an issue similar to the one reported here.
I wrote a small sample component to reproduce the issue:

import { Component, State } from '@stencil/core'

@Component({ tag: 'my-todo-list' })
export class MyTodoList {
  @State() todos: string[] = []

  componentWillLoad() {
    this.todos = []
  }

  removeTodo(todo) {
    this.todos = this.todos.filter(x => x !== todo)
  }

  addTodo(event) {
    if (event.key === 'Enter') {
      const todo = event.target.value
      this.todos = this.todos.concat([todo])
      event.target.value = ''
    }
  }

  render() {
    return (
      <div>
        <ul>
          {this.todos.map(todo =>
            <li>
              {todo}
              <button onClick={() => this.removeTodo(todo)}>x</button>
            </li>
          )}
        </ul>
        <input onKeyPress={e => this.addTodo(e)} />
      </div>
    )
  }
}

Steps to reproduce:

  1. add some todos (enter the string in the input and then press Enter key to confirm)
  2. remove the entries from the first entered to the last one (using the x buttons)
  3. if you add a breakpoint in the removeTodo function, you'll see that the array reference is wrong.

I found a workaround, if you use the index parameter of the map function to reference the item, it seems to work:

{this.todos.map((_, index) =>
  <li>
    {this.todos[index]}
    <button onClick={() => this.removeTodo(this.todos[index])}>x</button>
  </li>
)}

Is this a bug or I'm doing something wrong?
Can you help me to clarify this behavior?
Thanks!

All 12 comments

I may be mistaken, but since its a 1-way bind and the array reference has not changed then it will not re-render. This is mentioned in the documentation

I'm changing the array reference by replacing it with a new array (just as the docs say).
Also the render function actually gets called after I change the array and the value attributes get updated properly and I see the changes reflected in the UI.
Only the onInput events still reference an old exercise instance.

@FelschR Maybe I am missing something but the onInput functions are just updating objects within the array. This will not prompt a rerender. Is there more code to your component that would help clarify things?

Hello all! As it has been a while before there was any activity on this issue, I am going to close this for now. Thanks!

Sorry for not responding,

@jthoms1
I am updating the json array somewhere else in the code. When that happens and the UI is re-rendered then the issue I describes is occuring.

When I first assign values to json everything is fine and I can change values via the inputs.

When I set a new array to json the rendering happens and then when I'm entering something in the input fields not even the values in the json object change but they change in one of the older json array that I was saving during debugging.

This is how I update the json array:

@Prop()
protected workbook: any;

@PropWillChange("workbook")
protected async workbookChangeHandler(newWorkbook: any) {
    this.parser = new ExerciseParser(newWorkbook);
    this.json = await this.parser.read(this.xlsSheet);
}

@State()
protected json: ExerciseBase[];

So, what I'm doing is setting workbook to a new value which triggers the workbookChangeHandler.

Hey! Hmmm are you able to post more code or a repo we could use to reproduce?

Here's the whole component where I have this issue.
This is all I can give you for now. Tomorrow I can create a repository with a simplified version to reproduce this issue.

import { Component, Prop, PropWillChange, State, Method } from '@stencil/core';
import { ExerciseParser } from "../../services/exercise-parser";
import { ExerciseBase } from "../../model/csharp-generated/Exercise";
import { BaseMasterData } from '../base-masterdata';
import { IExerciseCsv } from '../../model/csv/exercise';

@Component({
    tag: 'exercise-masterdata',
    styleUrl: 'exercise-masterdata.scss'
})
export class ExerciseMasterData extends BaseMasterData<ExerciseBase, IExerciseCsv, ExerciseParser> {

    protected xlsSheet = "Muskelbeteiligungen";
    protected name = "exercises";

    @Prop()
    protected workbook: any;

    @PropWillChange("workbook")
    protected async workbookChangeHandler(newWorkbook: any) {
        this.parser = new ExerciseParser(newWorkbook);
        this.json = await this.parser.read(this.xlsSheet);
    }

    @State()
    protected json: ExerciseBase[];

    public componentWillUpdate() {
        console.log('The component will update');
        console.log(this.json);
    }

    public componentDidUpdate() {
        console.log('The component did update');
        console.log(this.json);
    }

    public render() {
        console.log('Rendering...');
        console.log(this.json);
        return (
            <section class="muscle-masterdata">
                <button onClick={() => this.loadFromDatabase()}>Load from DB</button>
                <header>
                    <h1>Muscle Group Master data</h1>
                </header>
                {this.json
                    ? ([
                        <button onClick={() => this.downloadJson()}>Export JSON</button>,
                        <button onClick={() => this.writeToDatabase()}>Write to DB</button>
                    ])
                    : ""
                }
                <table>
                    <tr class="table-head">
                        <td>ID</td>
                        <td>Name</td>
                        <td>Description</td>
                    </tr>
                    {this.json
                        ?
                        this.json.map((exercise) =>
                            <tr>
                                <td>{exercise.id}</td>
                                <td><input type="text" value={exercise.name}
                                                       onInput={(ev) => exercise.name = (ev.target as HTMLInputElement).value}></input></td>
                                <td><input type="text" value={exercise.description}
                                                       onInput={(ev) => exercise.description = (ev.target as HTMLInputElement).value}></input></td>
                            </tr>
                        )
                        : ""
                    }
                </table>
            </section>
        );
    }

    @Method()
    public updateSheet() {
        // write changes to workbook
        this.parser.write(this.xlsSheet, this.json);
    }
}

Hi, I'm experiencing an issue similar to the one reported here.
I wrote a small sample component to reproduce the issue:

import { Component, State } from '@stencil/core'

@Component({ tag: 'my-todo-list' })
export class MyTodoList {
  @State() todos: string[] = []

  componentWillLoad() {
    this.todos = []
  }

  removeTodo(todo) {
    this.todos = this.todos.filter(x => x !== todo)
  }

  addTodo(event) {
    if (event.key === 'Enter') {
      const todo = event.target.value
      this.todos = this.todos.concat([todo])
      event.target.value = ''
    }
  }

  render() {
    return (
      <div>
        <ul>
          {this.todos.map(todo =>
            <li>
              {todo}
              <button onClick={() => this.removeTodo(todo)}>x</button>
            </li>
          )}
        </ul>
        <input onKeyPress={e => this.addTodo(e)} />
      </div>
    )
  }
}

Steps to reproduce:

  1. add some todos (enter the string in the input and then press Enter key to confirm)
  2. remove the entries from the first entered to the last one (using the x buttons)
  3. if you add a breakpoint in the removeTodo function, you'll see that the array reference is wrong.

I found a workaround, if you use the index parameter of the map function to reference the item, it seems to work:

{this.todos.map((_, index) =>
  <li>
    {this.todos[index]}
    <button onClick={() => this.removeTodo(this.todos[index])}>x</button>
  </li>
)}

Is this a bug or I'm doing something wrong?
Can you help me to clarify this behavior?
Thanks!

@fgandellini Thanks for creating the sample.
This sounds like the exact same issue that I'm experiencing.

Also interesting to see that using map with an index parameter works.

@jgw96 @jthoms1 can you work with this information?

I'm facing the same issue also. I have a solution similar to @fgandellini . I'm dealing with 2 nested .map() and the code is looking a bit ugly.

I'm very interested in a better workaround or fix.

I guess this is probably related to / has the same root cause as #478

I just wanted to note that this issue has been fixed for me after I tried the latest release (0.6.9).
Pretty sure that this was #478.

Was this page helpful?
0 / 5 - 0 ratings