简体   繁体   中英

React component not re-rendering, although Object in props changes

I know, there are many, many similary questions.. ** duplicate alarm! **

But: I looked through all of them, I promise. I'm quite sure now, that this is another case, that could have to do with the props being an object (from what I've read here). But I couldn't solve the following, anyway:

class CsvListDropdown extends Component {
    constructor(props) {
        super(props);
        this.state = { sessions: props.sessions }
        this csvsInSession = this.csvsInSession.bind(this);
    }

    csvsInSession(sessions) {
        return (sessions
            .map(keys => Object.entries(keys)[2][1])
            .map((csv, i) => (
                <option value={csv} key={i}>{csv}</option>
            ))
        )
    }

    render() {
        const { isLoading } = this.props
        if (isLoading) { blablabla.. }
        else {
            return (
                ...
                <select value={this.props.sessions[0].currentCsv}>
                    {this.csvsInSession(this.state.sessions)}
                </select>
                ...
            )
        }
    }
}

export default withTracker(() => {
    const handle = Meteor.subscribe('sessions');
    return {
      isLoading: !handle.ready(),
      sessions: Sessions.find({}).fetch() 
    };
})(CsvListDropdown);

Now from the client I am writing another document into the Sessions collection, containing the .csv filename, while this new csv file is being uploaded to a remote server. console.log(this.props.sessions) gives me an array, which is up to date. But the component itself does not re-render.

What I also don't understand is: console.log(this.state.sessions) returns undefined . (note: state )

What I tried so far:

  • {this.csvsInSession(this.props.sessions)} (note: props )
  • Adding a withTracker / State / Props to the parent component and passing the sessions object from either state or props as params to the child component, that should re-render.

  • forceUpdate()

  • componentWillUpdate()

What may be important as well: The component should re-render about the same time another component also re-renders (which displays the contents of uploaded CSVs, that return from a microservice and get written into another collection). The latter does actually re-render.. But that dropdown does not.. argh!

this.state will only change if you call this.setState() , which you are not doing. You are initializing state with a value from props , but only in the constructor when the component is first instantiated. After that, even if props changes your component may re-render but what it displays won't change because state hasn't been updated.

In fact, there does not appear to be any reason whatsoever to store data in state in that component. It might as well be a functional presentational component:

function CsvListDropdown(props) {
    function csvsInSession(sessions) {
        return (sessions
            .map(keys => Object.entries(keys)[2][1])
            .map((csv, i) => (
                <option value={csv} key={i}>{csv}</option>
            ))
        )
    }

    const { isLoading } = props;
    if (isLoading) { blablabla.. }
    else {
        return (
            ...
            <select>
                {csvsInSession(props.sessions)}
            <select>
            ...
        )
    }
}

Generally all of your components should be stateless functional components unless they specifically need to store internal state for some reason.

Now I finally solved it, and it turns out that the component did actually update at any time, but I did not notice it, simply because the latest item in the array was quietly appended to the bottom of the dropdown list. This however I was not expecting, as I had published the collection with a descending sorting .

// server-side
Meteor.publish('sessions', function() {
    return Sessions.find({ userId: this.userId }, { sort: {createdAt: -1} });
});

Server-side seems to be the wrong place to sort. It simply does not have an effect. So sorted on the client side, when subscribing:

// client-side
export default withTracker(() => {
    const handle = Meteor.subscribe('sessions');
    return {
      isLoading: !handle.ready(),
      sessions: Sessions.find({}, { sort: {createdAt: -1} }).fetch() 
    };
})(App)

I had omitted an important detail from my question, that is how I set the value of the dropdown field:

<select value={this.props.sessions[0].currentCsv}>
    {this.csvsInSession(sessions)}
</select>

So lesson learned: If you think your react component does not re-render, always check if that's true, before assuming so.


As a side effect of debugging I restructered my components. Now the Meteor.subscribe() is within the parent component, that contains all the children, that have to handle the sessions object. And the sessions object gets passed down from the parent to the (grand)children as props. I think it's more readable and easier to maintain that way.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM