Home » Javascript » React changing order of components does not trigger a re-render

React changing order of components does not trigger a re-render

Posted by: admin November 1, 2017 Leave a comment

Questions:

I’m designing a system that shows when my Heroku apps have been updated. The problem I had was the parent component (Herokus) decided the order, but I wanted them to sort based on when they had been updated starting with the most recent. I was able to achieve this, by passing the dates from the Heroku component back to the parent Herokus component. I tried adding a console.log(this.state.apps) which shows the sorted order, but that is not reflected by the rendered component. My guess is react is not seeing the changed order of the components as a reason to update the view – but to be honest I have no clue. Any help is appreciated. Files are below. Sorry, my code is a mess, I rewrote it three times trying to fix this issue.

Herokus Component:

import React from 'react';
import Heroku from './Heroku';

export default class Herokus extends React.Component {
  constructor(props) {
    super(props);

    var apps = [
      'example-heroku-app'
    ]

    this.state = {
      apps: apps.map((h) => {
        return {
          app: h,
          updatedAt: new Date()
        };
      })
    }
  }

  sortApps() {
    var sorted = this.state.apps.sort((a, b) => {
      return a.updatedAt < b.updatedAt;
    });
    this.setState({
      apps: sorted
    });
  }

  updateParrent(data){
    var apps = this.state.apps;
    apps.find(x => x.app === data.app).updatedAt = data.updatedAt;
    this.setState({
      apps: apps
    });
    this.sortApps();
    this.forceUpdate();
  }

  render() {
    var s = this;

    return (
      <div>
        {s.state.apps.map((app, i) => {
          return (
            <Heroku app={app.app} compact key={`heroku-${i}`} updateParrent={s.updateParrent.bind(s)}/>
          );
        })}
      </div>
    );
  }
}

Heroku Component:

import React from 'react';

export default class Ping extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
        app: props.app,
      updatedAt: new Date()
    };
  }

  componentDidMount() {
    var s = this;
    axios.get(`/api/heroku/app/${s.props.app}`, {
        timeout: 20000,
    })
    .then(res => {
        var data = res.data;
      s.setState({
        app: data.app.name,
        updatedAt: new Date(data.app['updated_at'])
      });
      s.props.updateParrent(s.state);

      setInterval(() => {
        var updatedAt = new Date(s.state.updatedAt);
        s.setState({
          updatedAt: updatedAt
        });
      }, 1000);
    });
  }

  howLongAgo(date) {
    var ms = new Date() - date;
    var seconds = ms / 1000;
    var n = seconds;
    var suffix = '';

    // less than 1 minute
    if(seconds < 60){
      n = seconds;
      suffix = 'second' + (n > 1.5 ? 's' : '');
    }
    // less than 1 hour
    else if(seconds < 3600) {
      n = seconds / 60
      suffix = 'minute' + (n > 1.5 ? 's' : '');
    }
    // less than 1 day
    else if(seconds < 86400) {
      n = seconds / 3600;
      suffix = 'hour' + (n > 1.5 ? 's' : '');
    }
    // less than 1 week
    else if(seconds < 604800) {
      n = seconds / 86400;
      suffix = 'day' + (n > 1.5 ? 's' : '');
    }
    // more than 1 week
    else {
      n = seconds / 604800;
      suffix = 'week' + (n > 1.5 ? 's' : '');
    }

    return `${Math.round(n)} ${suffix} ago`;
  }

  render() {
    var s = this.state;
    var self = this;
    var output;

    // COMPACT VIEW
    if(this.props.compact){
      output = (
        <div className={'heroku heroku-compact'}>
            <h3>{s.app}</h3>
            <p>{self.howLongAgo(s.updatedAt)}</p>
            <div className='clearfix'></div>
        </div>
      )

    // FULL SIZE VIEW
    } else{
      output = ()
    }

    return output;
  }
}
Answers:

The problem seems to be that the order changes, but since the keys are based off the i index variable, the recorded list still has the same keys in the same order. React therefore does not see a difference.

Example:

Unsorted apps a, b, and c

<div key="1"> //app b
<div key="2"> //app a
<div key="3"> //app c

Sorted apps

<div key="1"> //app a
<div key="2"> //app b
<div key="3"> //app c

The apps are now in the right order, but the parent component Herokus still seems them with keys 1, 2, and 3 in the same order.

The solution:
Change the key to something unique to the individual Heroku components, but something that will remain unchanged unlike updatedAt. In this case the most logical choice is the app name.

This can be implemented in the Herokus component’s render function like so:

export default class Herokus extends React.Component {
  render() {
    var s = this;

    return (
      <div>
        {s.state.apps.map((app, i) => {
          return (
            <Heroku app={app.app} compact key={`heroku-${app.app}`} updateParrent={s.updateParrent.bind(s)}/>
          );
        })}
      </div>
    );
  }
}