React Component's state renders strangely with

2020-03-06 03:00发布

问题:

I have an array, I have 2 components(child and parent). I iterate through array within parent component, I render child components, I give them props with data from array.

Child components have their props translated to state and then have increment and decrement that state.

Parent component can add new item into array and re-render. BUT. If i unshift() new item in front of the array, i get last item from array added to the screen instead of new one to the front.

QUESTION: Why it renders good with .push() and bad with .unshift(). Also everything is ok with concat and [newItem, ...oldArray], but bad with same things when i add items in front of the array? Also how to properly .unshift() new items(comments, counters, images, posts, eg anything) into state, so they render first?

PS: Anything i do (concat, slice, ...array, unshift, react's immutability helper) doesn't seem to work properly. Mobx and Redux didn't help.

PS: This also occurs with Mithril, Inferno and Aurelia.

import React from 'react'
import {render} from 'react-dom'
var Component = React.Component

var data = [0, 12, -10, 1, 0, 1]

class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            counter: data
        }
        this.addCounter = this.addCounter.bind(this)
    }
    addCounter(e){
        let newArr = [1, ...this.state.counter]
        this.setState({
            counter: newArr
        })
    }
    render() {
        if(this.state.counter){
            return (
                <div>
                    <button onClick={this.addCounter}>add counter</button>
                    {this.state.counter.map(e=>{
                        return(
                            <Counter count={e}/>
                        )
                    })}
                </div>
            )
        } else {
            return(
                <div>loading...</div>
            )
        }
    }
}

class Counter extends Component {
    constructor(props) {
        super(props)
        this.state = {
            count: this.props.count
        }
        this.increment = this.increment.bind(this)
        this.decrement = this.decrement.bind(this)
    }
    increment(e){
        this.setState({count: this.state.count + 1})
    }
    decrement(e){
        this.setState({count: this.state.count - 1})
    }
    render() {
        return (
            <span>
                <b onClick={this.increment}>+</b>
                <i>{this.state.count}</i>
                <b onClick={this.decrement}>-</b>
            </span>
        )
    }
}

render(<App/>, document.getElementById('root'))

回答1:

The main problem isn't the way you are prepending items to your array, it's that you are not providing a key when rendering the child component.

What happens when you render the initial array is that the child component gets instantiated once per item in your array. However, React has no way of mapping the values in your array to those instances.

Let's call the first instance A. When you prepend to your list and render again, the first child instance (in the array resulting from your this.state.counter.map) will still be instance A, just with the prop e set to a new value. You can verify this by for example logging this.props.e in your child's render method. After prepending the new item, the first logged value should correspond to the prepended value.

Since your child component is stateful, and does not do anything to handle componentWillReceiveProps, having the e prop changed will not do anything to change each instance's previous state.

The reason why it works when you append is because the already existing instances will still map 1-to-1 with the items in your counter array, and the new item will be rendered as a new instance of Counter.

You would have the same problem if you were to rearrange the order of the items in counter, for example. The resulting child instances would not change order.

So, the solution is to provide a unique key to Counter, for each item. Since your items do not have an intrinsic identity, my suggestion would be to put a

let currentId = 0

above your App component, and have each item in your counter array be an object of {value, id: currentId++}, then pass id as the key to Counter.