Wrong components rendered by Preact

前端 未结 1 1669
清酒与你
清酒与你 2020-12-13 17:36

I\'m using Preact (for all intents and purposes, React) to render a list of items, saved in a state array. Each item has a remove button next to it. My problem is: when the

相关标签:
1条回答
  • 2020-12-13 18:26

    This is a classic issue that is totally underserved by Preact's documentation, so I'd like to personally apologize for that! We're always looking for help writing better documentation if anyone is interested.

    What has happened here is that you're using the index of your Array as a key (in your map within render). This actually just emulates how a VDOM diff works by default - the keys are always 0-n where n is the array length, so removing any item simply drops the last key off the list.

    Explanation: Keys transcend renders

    In your example, imagine how the (Virtual) DOM will look on the initial render, and then after removing item "b" (index 3). Below, let's pretend your list is only 3 items long (['a', 'b', 'c']):

    Here's what the initial render produces:

    <div>
      <div className="title">Packages</div>
      <div className="packages">
        <div className="package" key={0}>
          <button>X</button>
          <Package tracking="a" />
        </div>
        <div className="package" key={1}>
          <button>X</button>
          <Package tracking="b" />
        </div>
        <div className="package" key={2}>
          <button>X</button>
          <Package tracking="c" />
        </div>
      </div>
    </div>
    

    Now when we click "X" on the second item in the list, "b" is passed to removePackage(), which sets state.packages to ['a', 'c']. That triggers our render, which produces the following (Virtual) DOM:

    <div>
      <div className="title">Packages</div>
      <div className="packages">
        <div className="package" key={0}>
          <button>X</button>
          <Package tracking="a" />
        </div>
        <div className="package" key={1}>
          <button>X</button>
          <Package tracking="c" />
        </div>
      </div>
    </div>
    

    Since the VDOM library only knows about the new structure you give it on each render (not how to change from the old structure to the new), what the keys have done is basically tell it that items 0 and 1 remained in-place - we know this is incorrect, because we wanted the item at index 1 to be removed.

    Remember: key takes precedence over the default child diff reordering semantics. In this example, because key is always just the 0-based array index, the last item (key=2) just gets dropped off because it's the one missing from the subsequent render.

    The Fix

    So, to fix your example - you should use something that identifies the item rather than its offset as your key. This can be the item itself (any value is acceptable as a key), or an .id property (preferred because it avoids scattering object references around which can prevent GC):

    let packages = this.state.packages.map((tracking, i) => {
      return (
                                      // ↙️ a better key fixes it :)
        <div className="package" key={tracking}>
          <button onClick={this.removePackage.bind(this, tracking)}>X</button>
          <Package tracking={tracking} />
        </div>
      );
    });
    

    Whew, that was a lot more long-winded that I had intended it to be.

    TL,DR: never use an array index (iteration index) as key. At best it's mimicking the default behavior (top-down child reordering), but more often it just pushes all diffing onto the last child.


    edit: @tommy recommended this excellent link to the eslint-plugin-react docs, which does a better job explaining it than I did above.

    0 讨论(0)
提交回复
热议问题