Preact: DOM trees not reconciling (because different attribute order?)

Created on 27 Dec 2016  路  8Comments  路  Source: preactjs/preact

My app is a tree structure that expands/collapses, or a row can be highlighted when clicked on.
Let's the say the tree looks like this (just four rows)

image

...and I click on the top item. This sets that to 'current' and a render cycle is triggered. 95% of the time, the item will just be selected. But very rarely it will clone the whole tree!

image

At this point the app is broken. Clicking on the bottom tree will fire a click event, but select the item in the top version of the tree.

I'm guessing this is a reconciliation issue; if I look at the two DOM trees, I notice that the order of attributes is different in each.

image

It's not just the table, sometimes if I click on the top item the title of the site gets cloned

Preact: 7.1.0
Node: 7.1.0
Windows 10
Chrome 57

Any ideas? The site source is here if that helps.

bug feedback needed help wanted

Most helpful comment

Update: the rewritten Pure Functional Components support in Preact should fix this!

All 8 comments

wondering if this is a key issue? Might be worth trying it with this line removed. I'll try to look into this as soon as I can.

I have seen this same issue with stateless components. (Note axes below).

Note that I have react aliased in production build and do not experience this issue when using React in development.

screen shot 2017-01-05 at 11 36 23 am

import React from 'react';
import * as d3 from 'd3';
import { Motion, spring } from 'react-motion';

const XAxis = ({ xScale, yScale, label }) => {
  const xRange = xScale.range();
  const yRange = yScale.range();
  return (
    <Motion
      style={{
        x1: spring(xRange[0]),
        x2: spring(xRange[1]),
        y1: spring(yRange[0]),
        // y2: spring(yRange[1]),
      }}
    >
      {({ x1, x2, y1 }) => {
        // Baseline
        const baseline = (
          <line
            x1={x1}
            x2={x2}
            y1={y1}
            y2={y1}
            style={{ stroke: 'black' }}
          />
        );

        // Calculate tick values and generate line elements
        const tickValues = d3.ticks(xScale.invert(x1), xScale.invert(x2), 5);
        const ticks = tickValues.map((tickValue) => {
          return (
            <line
              x1={xScale(tickValue)}
              x2={xScale(tickValue)}
              y1={y1 + 10}
              y2={y1}
              style={{ stroke: 'black' }}
              key={tickValue}
            />
          );
        });

        // Tick labels
        const tickLabels = tickValues.map((tickValue) => {
          return (
            <text
              x={xScale(tickValue)}
              y={y1 + 25}
              textAnchor={'middle'}
              key={tickValue}
              style={{ userSelect: 'none', pointerEvents: 'none' }}
            >
              {tickValue}
            </text>
          );
        });

        // Axis Label
        const axisLabel = (
          <text
            x={(x2 + x1) / 2}
            y={y1 + 40}
            textAnchor={'middle'}
            style={{ userSelect: 'none', pointerEvents: 'none' }}
          >
            {label}
          </text>
        );

        return (
          <g className={'axis axis--x'} >
            {baseline}
            {ticks}
            {tickLabels}
            {axisLabel}
          </g>
        );
      }}
    </Motion>
  );
};

XAxis.propTypes = {
  xScale: React.PropTypes.func,
  yScale: React.PropTypes.func,
  label: React.PropTypes.string,
};

export default XAxis;

@dyst5422 what version of preact? The values you're supplying for key seem odd - they will trigger a recycle on every diff.

I'm using version 7.1.0 of preact.

I rearranged some of my rendering and it cleared up the problem (changed keys about a bit too).

My understanding is that it should still be garbage collecting those elements which dont exist on a rerender (those whose keys dont match the previous set). So while I may have resolved the particular issue I was having, it seems to still have some issues in that regard. Mind you, I know very little of the internals of how all that works.

I'll include the new code which resolved this in case it is of any help.

import React from 'react';
import * as d3 from 'd3';
import { Motion, TransitionMotion, spring } from 'react-motion';

const NUM_TICKS = 5;

const XAxis = ({ xScale, yScale, label }) => {
  const xRange = xScale.range();
  const yRange = yScale.range();
  const x1 = xRange[0];
  const x2 = xRange[1];
  const y1 = yRange[0];
  const tickValues = d3.ticks(xScale.invert(x1), xScale.invert(x2), NUM_TICKS);

  const styles = tickValues.map((tickValue, i) => {
    return ({
      key: (i + 1).toString(),
      data: tickValue,
      style: {
        xVal: spring(xScale(tickValue)),
      },
    });
  });

  return (
    <g className={'axis axis--x'} >
      <Motion
        style={{
          springx1: spring(x1),
          springx2: spring(x2),
          springy1: spring(y1),
          // y2: spring(yRange[1]),
        }}
      >{({ springx1, springx2, springy1 }) => {
        return (
          <g className={'react-motion-transition-group'}>
            <line
              x1={springx1}
              x2={springx2}
              y1={springy1}
              y2={springy1}
              style={{ stroke: 'black' }}
            />
            <text
              x={(springx2 + springx1) / 2}
              y={springy1 + 40}
              textAnchor={'middle'}
              style={{ userSelect: 'none', pointerEvents: 'none' }}
            >
              {label}
            </text>
          </g>
        );
      }}
      </Motion>
      <TransitionMotion
        styles={styles}
      >{(interpolatedStyles) => {
        return (
          <g className={'react-motion-transition-group'}>
            {interpolatedStyles.map(({ key, data, style }) => {
              return (
                <g className={'tick'} key={key}>
                  <line
                    x1={style.xVal}
                    x2={style.xVal}
                    y1={y1 + 10}
                    y2={y1}
                    style={{ stroke: 'black' }}
                    key={-key}
                  />
                  <text
                    x={style.xVal}
                    y={y1 + 25}
                    textAnchor={'middle'}
                    key={key}
                    style={{ userSelect: 'none', pointerEvents: 'none' }}
                  >
                    {data}
                  </text>
                </g>
              );
            })}
          </g>
        );
      }}
      </TransitionMotion>
    </g>
  );
};

XAxis.propTypes = {
  xScale: React.PropTypes.func,
  yScale: React.PropTypes.func,
  label: React.PropTypes.string,
};

export default XAxis;

@dyst5422 hmm - interesting, so it seemed like Motion being a child of XAxis was causing a problem. That gives me somewhere to start!

Update: the rewritten Pure Functional Components support in Preact should fix this!

Fixed in 8.

Nice one!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kay-is picture kay-is  路  3Comments

jescalan picture jescalan  路  3Comments

Zashy picture Zashy  路  3Comments

KnisterPeter picture KnisterPeter  路  3Comments

SabirAmeen picture SabirAmeen  路  3Comments