1

I have this component class (and some related methods):

const sortData = (name, data) => {
    return data.sort((a, b) => {return b[name] - a[name]});
};

class LeaderTable extends React.Component {
    renderByRecent() {
        let data = sortData('recent', this.props.list);
        ReactDOM.render(
            <LeaderTable list={data}/>,
            document.getElementById('board'))
    }
    render() {
        return (
            <table className="table table-bordered">
                <thead>
                    <tr>
                        <td><strong>#</strong></td>
                        <td><strong>Camper Name</strong></td>
                        <td id="recent" onClick={() => this.renderByRecent()} className="text-center">Points in past 30 days</td>
                        <td id="alltime" onClick={() => this.render()} className="text-center">All time points</td>
                    </tr>
                </thead>
                <tbody>
                    {this.props.list.map(function(item, index) {
                        let url = "https://www.freecodecamp.com/" + item.username;
                        return (
                            <tr key={index}>
                                <td>{index}</td>
                                <td>
                                    <a href={url}>
                                        <img src={item.img} className="logo"/> {item.username}
                                    </a>
                                </td>
                                <td className="text-center">{item.recent}</td>
                                <td className="text-center">{item.alltime}</td>
                            </tr>
                        );
                    })}
                </tbody>
            </table>
        );
    }
}

Now first render happens when page loads. It is simply called in javascript file like this:

ReactDOM.render(
    <LeaderTable list={campersData}/>,
    document.getElementById('board'))

This one is working fine.

What I need now is to rerender same component, but with different data (actually just with different order of that data).

If you look into renderByRecent method, here I "rerender" by passing same data sorted in different way (and is is called using onClick on td with id='"recent". But I do not know if this is really a good pattern to rerender.

Also I want to rerender back original data if td with id="alltime" is clicked. This part seems to not work (It does call render method every time I press respective td, but nothing changes). I guess I can't recal render method and hope to rerender it?

What kind of pattern is usually done with react if you have some similar case like this?

Update

Posted my original code on codepen, for easier investigation: https://codepen.io/andriusl/pen/YxWXzg

Andrius
  • 19,658
  • 37
  • 143
  • 243

4 Answers4

2

is really a good pattern to re-render?

I think no, if you want to render same component with different data manage that by using state variable don't use ReactDOM.render again.

Either use a state variable that will save the key name by which you want to sort the data then during ui creating check that and sort the data, Or you can store the props data in state variable and modify that data.

Issue with syntax onClick={() => this.render()}:

As per DOC:

The problem with this syntax is that a different callback is created each time the component renders, so better to bind the method in the constructor.

Issue with this.render():

Calling render method is not a good idea, always do setState react will automatically re-render the component.

You can write the code like this:

const sortData = (name, data) => {
    return data.sort((a, b) =>  b[name] - a[name]);
};

class LeaderTable extends React.Component {
    constructor(){
        super();
        this.state = {
            sortBy: ''
        }
    }

    _renderList(){
        let data = this.props.list.slice(0);

        if(this.state.sortBy){
            data = sortData(this.state.sortBy, data);
        }

        return data.map(function(item, index) {
            let url = "https://www.freecodecamp.com/" + item.username;
            return (
                <tr key={index}>
                    <td>{index}</td>
                    <td>
                        <a href={url}>
                            <img src={item.img} className="logo"/> {item.username}
                        </a>
                    </td>
                    <td className="text-center">{item.recent}</td>
                    <td className="text-center">{item.alltime}</td>
                </tr>
            );
        });
    }

    renderByRecent() {
        this.setState({
            sortBy: 'recent'
        });
    }

    renderOriginalList(){
       this.setState({
            sortBy: ''
       });
    }

    render() {
        return (
            <table className="table table-bordered">
                <thead>
                    <tr>
                        <td><strong>#</strong></td>
                        <td><strong>Camper Name</strong></td>
                        <td id="recent" onClick={() => this.renderByRecent()} className="text-center">Points in past 30 days</td>
                        <td id="alltime" onClick={() => this.renderOriginalList()} className="text-center">All time points</td>
                    </tr>
                </thead>
                <tbody>
                    {this._renderList()}
                </tbody>
            </table>
        );
    }
}
Mayank Shukla
  • 100,735
  • 18
  • 158
  • 142
  • please provide the reason for downvote also, i will correct my answer :) – Mayank Shukla Aug 01 '17 at 16:41
  • Yes, I was writing it. `Issue with syntax onClick={() => this.render()}` - Binding is the least that is wrong with that syntax. Performance wise - if you don't have really large scale application, you won't notice it by a long shot. Main issue with that code is that calling `this.render` in react is poor way of rerendering component - mainly because state and props will not be updated. If you really need to rerender component, you will use `this.forceUpdate()` Link on SO - https://stackoverflow.com/questions/30626030/can-you-force-a-react-component-to-rerender-without-calling-setstate – Oliver Rydzi Aug 01 '17 at 16:41
  • `this.sortData` can't be used, because `sortData` is outside method. So it should be just `sortData`. Also `alltime` part is not working, because you are using same `this.render` like me, which seems to not do anything useful. – Andrius Aug 01 '17 at 16:41
  • @Andrius that was a small mistake, removed `this` from answer, sorry i didn't check that mart let me know about use of `this.render` will tell you the solution of that also :) – Mayank Shukla Aug 01 '17 at 16:44
  • Furthermore, `this.forceUpdate()` makes sure that you have latest state & props. Update your answer accordingly, and I will remove downvote. – Oliver Rydzi Aug 01 '17 at 16:45
  • @OliverRydzi i don't need to give a answer for all you reasons, but i will suggest you to do more research about `forceUpdate` and `binding` :) – Mayank Shukla Aug 01 '17 at 16:48
  • @MayankShukla so you are saying that `this.render()` is better approach than `this.forceUpdate()`? I am avoiding using both, as I wrote above, but I would like you to link me article where they justify using `this.render()`, just curious. As about binding, I prefer to write functions with ES7 binding, i.e. `renderByRecent = () => {...}`, therefore there is no need to use binding. And in this application scale, there is absolutely nothing wrong with arrow syntax with onClick, since as I said there is almost no performance benefit.There is totally different problem with that code. – Oliver Rydzi Aug 01 '17 at 16:53
  • @MayankShukla well, with `this.render()` I was just experimenting. The use of that `onClick` (when `alltime` `td` is clicked) is to update list back, in a previous state. In other words sortby `alltime` points. – Andrius Aug 01 '17 at 16:53
  • 1
    @Andrius simply you need to call a funciton in which revert the state value back to `''` (blank) whenever we do setstate it will re-render the component with updated data. it will work, check the updated answer :) – Mayank Shukla Aug 01 '17 at 16:55
  • Note - you removed `this.render` what I pointed out to be main flaw with this code. When you will discourage using it, I will make that upvote ;) – Oliver Rydzi Aug 01 '17 at 16:57
  • @MayankShukla thanks. It does work with `alltime` now. I get it now, that all you need to do is change state and then react will do the job:). – Andrius Aug 01 '17 at 16:58
  • @OliverRydzi i missed that part initially i just wrote the code to sort the data and re-render the component again :) – Mayank Shukla Aug 01 '17 at 16:59
  • Thing is that you highlighted it above the code, saying main issue is binding. Even though he calls freaking `this.render` there. Otherwise good answer. – Oliver Rydzi Aug 01 '17 at 17:00
1

You should only have one main render method. Mount the React component to the DOM outside of your component and let React manage controlling the DOM updates as the component state changes.

corrected pin codepen

class LeaderTable extends React.Component {
    constructor() {
        super();

        this.state = {
            sort: false
        };

        this.renderByRecent = this.renderByRecent.bind(this); // bind this context to method
    }

    renderByRecent() {
        let data = this.props.list.slice();

        if (this.state.sort) {
            data.sort((a, b) => {
                return b.recent - a.recent;
            });
        }



        return data.map(function(item, index) {
            let url = "https://www.freecodecamp.com/" + item.username;
            return (
                <tr key={index}>
                    <td>
                        {index}
                    </td>
                    <td>
                        <a href={url}>
                            <img src={item.img} className="logo" />{" "}
                            {item.username}
                        </a>
                    </td>
                    <td className="text-center">
                        {item.recent}
                    </td>
                    <td className="text-center">
                        {item.alltime}
                    </td>
                </tr>
            );
        });
    }

    render() {
        return (
            <table className="table table-bordered">
                <thead>
                    <tr>
                        <td>
                            <strong>#</strong>
                        </td>
                        <td>
                            <strong>Camper Name</strong>
                        </td>
                        <td
                            id="recent"
                            onClick={() => this.setState({ sort: true })}
                            className="text-center"
                        >
                            Points in past 30 days
                        </td>
                        <td
                            id="alltime"
                            onClick={() => this.setState({ sort: false })}
                            className="text-center"
                        >
                            All time points
                        </td>
                    </tr>
                </thead>
                <tbody>
                    {this.renderByRecent()}
                </tbody>
            </table>
        );
    }
}

ReactDOM.render(<LeaderTable list={data} />, document.getElementById("board"));
canaan seaton
  • 6,688
  • 4
  • 17
  • 25
  • Tried this example, but it seems result is the same as of mine. For some reason it does not render back when I press on `td='alltime'`. I mean nothing happens. Maybe you know why? – Andrius Aug 01 '17 at 16:37
  • you can `console.log(data, this.state.sort)` in the render post method and make sure that the state is being updated on the respective click events as well as verifying that the data is being properly, I concur with some of the other posts that you should consider making use of the `Array.prototype.sort` method to do your sorting vs rolling your own function -> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort – canaan seaton Aug 01 '17 at 16:41
  • Hm, I do see `sort` to be `false` if I press `alltime` td when `render` is called. But it still does not go back in previous state (I mean to the one when it loads). – Andrius Aug 01 '17 at 16:45
  • Are you mocking this up in Codepen for freecodecamp? If so, can you send me a link to it so i can play with it? – canaan seaton Aug 01 '17 at 16:47
  • Yeah, I do. I actually just added link in a question. Look at the `Update` section. – Andrius Aug 01 '17 at 16:50
  • https://codepen.io/cmseaton42/pen/dzXoGr?editors=0010, see the corrected pin, is this what you are looking for? – canaan seaton Aug 01 '17 at 17:06
1

I have a few suggestions, first of all .sort() works in-place meaning that you are mutating the data you are trying to sort. For this reason I prefer to do a .slice() first then .sort(). This will return a new array sorted according to any sorting function you pass in, not mutating data is a good functional programming practice. Regarding a good approach, it depends on how you manage your state, but in general the need to force an update (from my experience) indicates you should reconsider your logic, I would only force an update as last resort.

If you normally manage your state inside the component I would save the data into a state variable, then have different methods that sort that data according to my needs. To give you a brief example:

...
constructor(props) {
  super(props);
  this.state = { listData: this.props.list };

  this.sortBy = this.sortBy.bind(this);
}

sortBy(field) {
  const { listData } = this.state;
  const sortedList = listData
    .slice()
    .sort((a,b) => b[field] - a[field]); 

  this.setState({ listData: sortedList });
}


render() {
  return (
    <div>
      {this.state.listData.map(listItem => <MyComponent ...listItem />)}
      <button onClick={() => this.sortBy('recent')}>Sort by recent</button>
      <button onClick={() => this.sortBy('alltime)}>Sort by all time</button>
    </div>
  )
}
...

EDIT

Although you already accepted an answer, check out this implementation, which I find easier to read and maintain. It also provides a more reusable sorting approach.

G4bri3l
  • 4,996
  • 4
  • 31
  • 53
0

React will automatically re-render if the props coming into the component have changed. So the sorting should be happening on the higher level component that is passing props to this one. You should also not be mutating the props like that.

You could also have this component manage the list in its own state like so:

const sortData = (name, data) => {
    return data.sort((a, b) => {return b[name] - a[name]});
};

class LeaderTable extends React.Component {
    constructor(props) {
        super(props)
        this.state={
            list: [{ name: 'Jerry'}, {name: 'Tabitha'}]
        }
    }
    renderByRecent() {
        // spread the array so it doesn't mutate state
        this.setState({
           list: sortData('name', [...this.state.list])
        })
    }
    render() {
        return (
            <table className="table table-bordered">
                <thead>
                    <tr>
                        <td><strong>#</strong></td>
                        <td><strong>Camper Name</strong></td>
                        <td id="recent" onClick={() => this.renderByRecent()} className="text-center">Points in past 30 days</td>
                        <td id="alltime" onClick={() => this.render()} className="text-center">All time points</td>
                    </tr>
                </thead>
                <tbody>
                    {this.state.list.map(function(item, index) {
                        let url = "https://www.freecodecamp.com/" + item.username;
                        return (
                            <tr key={index}>
                                <td>{index}</td>
                                <td>
                                    <a href={url}>
                                        <img src={item.img} className="logo"/> {item.username}
                                    </a>
                                </td>
                                <td className="text-center">{item.recent}</td>
                                <td className="text-center">{item.alltime}</td>
                            </tr>
                        );
                    })}
                </tbody>
            </table>
        );
    }
}
tgreen
  • 1,720
  • 1
  • 16
  • 28