Skip to content

Keys causing re-render/reorder when adding to/removing from array of elements #1069

@lewischa

Description

@lewischa

I've done quite a lot of searching and can't seem to find an answer to this specific problem I've encountered. I would greatly appreciate any insight!

Before I get into it, let me note that I am using unique, non-index-based key attributes in conjunction with shouldComponentUpdate.

My Particular Use Case

I am building a grid component for my employer that requires "pagination". Basically, I have a set of data that must be rendered as a grid, but it must be configurable such that only a certain number of "pages" are rendered at one time. As the user navigates through the grid (using arrow keys), new grid cells will be rendered on one side and removed (not rendered) from the other. More specifically, as the user moves toward the end of the grid, new "pages" will be rendered at the end and the same number of "pages" will be removed from the beginning. The opposite occurs when moving toward the beginning. This ensures that the number of rendered "pages" is always the same.

The Problem

This is somewhat similar to #338 I believe, but what I've found is that when adding a page to the end (which also removes a page from the beginning) the entire set of divs (cells) flash with chrome's purple highlight color. What's interesting is this does not happen when going the opposite way, i.e. when adding a page to the beginning (which removes a page from the end), only the first few divs flash as expected. What's also interesting is that shouldComponentUpdate is doing its job properly and apparently only rendering the cells that absolutely must be rendered.

A short, non-confidential example

Expected Behavior Example

A coworker spun up a simple example to illustrate the problem. First, here's what we'd expect. This is with adding 1 element to one side and removing 1 element from the other:
rerender-expected

Here is the code for this example:

import './style';
import { Component } from 'preact';


export default class App extends Component {
    constructor() {
        super();

        this.state = {
            data: [1, 2, 3, 4, 5],
            direction: 1
        };

        document.onkeydown = (e) => {
            e = e || window.event;

            switch (e.keyCode) {
                case 38: // up
                    this.setState({
                        direction: -1
                    });
                    break;
                case 40: // down
                    this.setState({
                        direction: 1
                    });
                    break;
            }
        };

        setInterval(() => {
            let newData = [...this.state.data];

            if (this.state.direction === 1) {
                newData.shift();
                newData.push(newData[newData.length - 1] + 1);
                // let lastElem = this.state.data[this.state.data.length - 1];
                // const sliced = this.state.data.slice(2);
                // sliced.push(++lastElem);
                // sliced.push(++lastElem);
                // newData = sliced;
            } else {
                let numToInsert = newData[0] - 1;

                if (numToInsert > 0) {
                    newData.pop(); // remove from tail
                    newData.unshift(numToInsert); // insert at head
                    // const sliced = this.state.data.slice(0, this.state.data.length - 2);
                    // sliced.unshift(numToInsert--);
                    // sliced.unshift(numToInsert);
                    // newData = sliced;
                }
            }

            this.setState({
                data: newData
            });
        }, 2000);
    }


    render() {
        const styles = {
            width: '100px',
            height: '50px',
            border: '1px solid #000',
            textAlign: 'center',
            lineHeight: '50px'
        };

        const BoxArray = this.state.data.map(obj => {
            return <div style={styles} key={obj}>{obj}</div>;
        });

        const directionStr = (this.state.direction === -1) ? 'UP' : 'DOWN';

        return (
            <div>
		{BoxArray}

                <div style="margin-top:20px">
                    Direction: {directionStr}
                </div>
            </div>
        );
    }
}
Real Use Case Behavior

And here is the same example, but with removing 2 elements from one side and adding 2 to the other. This is the behavior I'm seeing in my real use case:
rerender2
Here is the modified code for this example:

import './style';
import { Component } from 'preact';


export default class App extends Component {
    constructor() {
        super();

        this.state = {
            data: [1, 2, 3, 4, 5],
            direction: 1
        };

        document.onkeydown = (e) => {
            e = e || window.event;

            switch (e.keyCode) {
                case 38: // up
                    this.setState({
                        direction: -1
                    });
                    break;
                case 40: // down
                    this.setState({
                        direction: 1
                    });
                    break;
            }
        };

        setInterval(() => {
            let newData = [...this.state.data];
            // let newData;

            if (this.state.direction === 1) {
                // newData.shift();
                // newData.push(newData[newData.length - 1] + 1);
                let lastElem = this.state.data[this.state.data.length - 1];
                const sliced = this.state.data.slice(2);
                sliced.push(++lastElem);
                sliced.push(++lastElem);
                newData = sliced;
            } else {
                let numToInsert = newData[0] - 1;

                if (numToInsert > 1) {
                    // newData.pop(); // remove from tail
                    // newData.unshift(numToInsert); // insert at head
                    const sliced = this.state.data.slice(0, this.state.data.length - 2);
                    sliced.unshift(numToInsert--);
                    sliced.unshift(numToInsert);
                    newData = sliced;
                }
            }

            this.setState({
                data: newData
            });
        }, 2000);
    }


    render() {
        const styles = {
            width: '100px',
            height: '50px',
            border: '1px solid #000',
            textAlign: 'center',
            lineHeight: '50px'
        };

        const BoxArray = this.state.data.map(obj => {
            return <div style={styles} key={obj}>{obj}</div>;
        });

        const directionStr = (this.state.direction === -1) ? 'UP' : 'DOWN';

	return (
            <div>
                {BoxArray}

                <div style="margin-top:20px">
                    Direction: {directionStr}
                </div>
	    </div>
	);
    }
}

Why would adding/removing more than one element cause a re-render/reorder and only when adding to the end and removing from the beginning? My coworker and I are quite stumped at this point. I'd be happy to answer any questions if this explanation is not clear in any way. Thank you in advance!

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions