Alessander França Alessander França - 10 days ago 4
React JSX Question

React props are updating automatically

I am making a paginate component, and I'm having problems with the props passed to this paginate component. The elements props are updating automatically, so I cannot use componentWillReceiveProps to update my component.

Here is my parent component render and callback to Paginate's component:

class AlarmManagement extends React.Component{
constructor(props){
super(props)
this.state = {
alarms: undefined,
nameIsOrdered: false,
codeIsOrdered: true,
priorityIsOrdered: false,
alarmsPaginate: undefined,
}
this.orderByName = this.orderByName.bind(this)
this.orderByCode = this.orderByCode.bind(this)
this.orderByPriority = this.orderByPriority.bind(this)
this.getAlarms = this.getAlarms.bind(this)
this.getAlarmsPaginate = this.getAlarmsPaginate.bind(this)
}

componentWillMount(){
this.getAlarms()
}

getAlarms(){
$.ajax({
type: 'GET',
url: '/api/alarm-event-types/',
headers: { 'Authorization': "Token " + localStorage.token },
success: (alarms) => {
this.setState({
alarms: alarms.sort((a, b) => a.code - b.code)
})

},
error: (fail) => console.log(fail)
})
}

getAlarmsPaginate(page, amount) {
const newAlarms = this.state.alarms
this.setState({ alarmsPaginate: newAlarms.slice(page, amount) });

}

componentWillReceiveProps({emergency}){
if(emergency !== undefined && emergency !== null){
let updateList = [],
shouldUpdate = false
emergency.forEach((el, index) => {
if(el.update){
updateList.push(index)
}
if(el.update == "alarm"){
shouldUpdate = true
}
})
if(shouldUpdate){
this.getAlarms()
}
updateList.forEach((el) => {
if(this.props.clearList){
this.props.clearList(el)
}
})
}
}

orderByName(){
let alarms = this.state.alarms
if(this.state.nameIsOrdered === true){
this.setState({
alarms: alarms.sort((a, b) => {
let nameA = `${a.name.toUpperCase()}`,
nameB = `${b.name.toUpperCase()}`
if (nameA > nameB) {
return 1;
}
if (nameA < nameB) {
return -1;
}

return 0;
}),
nameIsOrdered: false,
codeIsOrdered: false,
priorityIsOrdered: false
})
}
else{
this.setState({
alarms: alarms.sort((a, b) => {
let nameA = `${a.name.toUpperCase()}`,
nameB = `${b.name.toUpperCase()}`
if (nameB > nameA) {
return 1;
}
if (nameB < nameA) {
return -1;
}

return 0;
}),
nameIsOrdered: true,
codeIsOrdered: false,
priorityIsOrdered: false
})
}
}

orderByCode(){
let alarms = this.state.alarms
if(this.state.codeIsOrdered === true){
this.setState({
alarms: alarms.sort((a, b) => b.code - a.code),
codeIsOrdered: false,
nameIsOrdered: false,
priorityIsOrdered: false
})
}
else{
this.setState({
alarms: alarms.sort((a, b) => a.code - b.code),
codeIsOrdered: true,
nameIsOrdered: false,
priorityIsOrdered: false
})
}
}

orderByPriority(){
let alarms = this.state.alarms
if(this.state.priorityIsOrdered === true){
this.setState({
alarms: alarms.sort((a, b) => b.priority - a.priority),
nameIsOrdered: false,
codeIsOrdered: false
})
}
else{
this.setState({
alarms: alarms.sort((a, b) => a.priority - b.priority),
nameIsOrdered: false,
codeIsOrdered: false
})
}
this.setState(prevState => ({
priorityIsOrdered: !prevState.priorityIsOrdered,
}))
}

render(){
const alarms = this.state.alarms,
alarmsPaginate = this.state.alarmsPaginate;
return(
this.props.user.groups == 1 ? (
<div className="contactto-middle-content">
<ManagementMenu/>
<div className="management-content">
<div className="list-management-subtitle">ALARMES</div>
<button type="button" className="btn btn--save-attend-or-add-someone btn--color-tecno" onClick={() => browserHistory.push('/app/alarms/form/add')}>
<div><span className="btn--bold">+ ADICIONAR</span> ALARME</div>
</button>
{alarms &&
<div className="list-table">
<Paginate outerClass="paginate-wrapper" filterElements={this.getAlarmsPaginate} maxElements={5} elements={alarms} />
<div className="list-table-header">
<div className="left list-table--200"><span className="icons-order clickable" onClick={this.orderByName}>Nome<Order width="15" height="10"/></span></div>
<div className="left list-table--200"><span className="icons-order clickable" onClick={this.orderByCode}>Código<Order width="15" height="10"/></span></div>
<div className="left list-table--200"><span className="icons-order clickable" onClick={this.orderByPriority}>Prioridade<Order width="15" height="10"/></span></div>
<div className="list-table-body-column--action--2icons"></div>
</div>

<div className="list-table-body scroll">
{alarmsPaginate && alarmsPaginate.map((alarm) =>
<AlarmRow key={alarm.code} alarm={alarm} getAlarms={this.getAlarms} channelId={this.props.channelId} iconDetail={this.refs["iconDetail"]}/>
)}
</div>

</div>
}

</div>
</div>
):
<div className="error">Página não pode ser acessada, pois você não é um administrador</div>
)
}
}

export default AlarmManagement


And here is my Paginate component:

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

this.state = {
pagesNumber: undefined,
elements: undefined,
elementsNumber: undefined,
startNumber: undefined,
endNumber: undefined,
pos: undefined,
}

this.nextList = this.nextList.bind(this)
this.previousList = this.previousList.bind(this)
}

previousList(){
if(this.state.pos > 1){
let pos = this.state.pos - 1
this.changePage(pos)
}

}

nextList(){
if(this.state.pos < this.state.pagesNumber){
let pos = this.state.pos + 1
this.changePage(pos)
}

}

changePage(pos){
const newStartNumber = pos === 1 ? 0 : this.props.maxElements * (pos - 1);
const newEndNumber = this.props.maxElements * pos > this.state.elementsNumber ? this.state.elementsNumber : this.props.maxElements * pos;
this.props.filterElements(newStartNumber, newEndNumber);
this.setState({
pos: pos,
startNumber: newStartNumber,
endNumber: newEndNumber,
});
}

componentWillReceiveProps(nextProps){
console.log(nextProps.elements != this.props.elements ? 'different' : 'equal')
}

componentWillMount(){
this.setState((prevState, props) => ({
pagesNumber: props.elements ? Math.ceil(props.elements.length / props.maxElements) : 0,
elements: props.elements,
elementsNumber: props.elements.length,
startNumber: 1,
endNumber: props.maxElements,
pos: 1,
}));
if(this.props.filterElements){
this.props.filterElements(0, this.props.maxElements)
}
}

render(){
const elementsNumber = this.state.elementsNumber,
startNumber = this.state.startNumber,
endNumber = this.state.endNumber;
return(
<div className={this.props.outerClass}>
{elementsNumber > this.props.maxElements &&
<div className="left contactto-100">
{elementsNumber && <span className="left">{`${startNumber === 0 ? 1 : startNumber}-${endNumber} de ${elementsNumber}`}</span>}
<span className="paginate-arrow paginate-arrow-left" onClick={this.previousList}></span>
<span className="paginate-arrow paginate-arrow-right" onClick={this.nextList}></span>
</div>
}
</div>
)
}
}

Paginate.defaultProps = {
outerClass: 'paginate-wrapper'
}

Paginate.propTypes = {
outerClass: React.PropTypes.string,
filterElements: React.PropTypes.func.isRequired,
maxElements: React.PropTypes.number.isRequired,
elements: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
}


The elements logged at componentWillReceive props are the same.

Code updated following the post advices, but:
componentWillReceiveProps(nextProps){
console.log(nextProps.elements != this.props.elements ? 'different' : 'equal') // still are equal
}

What is wrong?

Thanks in advance.

Answer

It is not very clear what your question is, or how your code is intended to function. However, you are doing some very inadvisable things within your code...

changePage(pos){
        this.state.startNumber = pos === 1 ? 0 : this.props.maxElements * (pos - 1);
        this.state.endNumber = this.props.maxElements * pos > this.state.elementsNumber ? this.state.elementsNumber : this.props.maxElements * pos;
        this.props.filterElements(this.state.elements, this.state.startNumber, this.state.endNumber);
        this.setState({ 
            pos: pos,
            startNumber: this.state.startNumber,
            endNumber: this.state.endNumber
        });
    }

Although you are correctly using setState() to update state at the end of the function, you are directly mutating state before this. You can remedy this by either using a temporary variable to hold the new state, or by doing the calculations within the setState call. E.g.

changePage(pos){
        const newStartNumber = pos === 1 ? 0 : this.props.maxElements * (pos - 1);
        const newEndNumber = this.props.maxElements * pos > this.state.elementsNumber ? this.state.elementsNumber : this.props.maxElements * pos;
        this.props.filterElements(this.state.elements, newStartNumber, newEndNumber);
        this.setState({
            pos: pos,
            startNumber: newStartNumber,
            endNumber: newEndNumber
        });
    }

The use of props within the constructor or componentWillReceiveProps() methods to set state is also something of an anti-pattern. In general in a React application we want to have a single source of truth - i.e. all data is the responsibility of one single component, and only one component. It is the responsibility of this component to store the data within its state, and distribute the data to other components via props.

When you do this...

constructor(props){
        super(props)

        this.state = {
            elements: this.props.elements,
            ...
        }
        ...
    }

The parent and child component are now both managing elements within their state. If we update state.elements in the child, these changes are not reflected in the parent. We have lost our single source of truth, and it becomes increasingly difficult to track the flow of data through our application.

In your specific case, it is the responsibility of the Parent component to maintain elements within its state. The Child component receives elements as props - it should not store them as state or directly update elements in any way. If an action on Child should require an update of elements, this needs to be done via a function passed to it from Parent as props.

The changePage function I used as an example above could then become...

changePage(pos){
        const newStartNumber = pos === 1 ? 0 : this.props.maxElements * (pos - 1);
        const newEndNumber = this.props.maxElements * pos > this.props.elements.length ? this.props.elements.length : this.props.maxElements * pos;
        this.props.filterElements(this.props.elements, newStartNumber, newEndNumber);
        this.setState({
            pos: pos,
            startNumber: newStartNumber,
            endNumber: newEndNumber
        });
    }

However, we can go even further - why do we need to pass this.props.elements to the filterElements function? The Parent component must already have a reference to elements; after all it gave it to us in the first place!

changePage(pos){
        const newStartNumber = pos === 1 ? 0 : this.props.maxElements * (pos - 1);
        const newEndNumber = this.props.maxElements * pos > this.props.elements.length ? this.props.elements.length : this.props.maxElements * pos;
        this.props.filterElements(newStartNumber, newEndNumber);
        this.setState({
            pos: pos,
            startNumber: newStartNumber,
            endNumber: newEndNumber
        });
    }

In your Parent component you would then change the function...

getAlarmsPaginate(alarms, page, amount) {
    this.state.alarmsPaginate = alarms.slice(page, amount)
    this.setState({ alarmsPaginate: this.state.alarmsPaginate })

}

into...

getAlarmsPaginate(page, amount) {
    const newAlarmsPaginate = this.state.alarms.slice(page, amount);
    this.setState({ alarmsPaginate: newAlarmsPaginate });
}

Note we are slicing this.state.alarms directly, rather than a function argument - also we are no longer mutating our state - but using the setState function exclusively.

There are several more instances where you use props and state inappropriately throughout your code - I would go through it all and ensure that you follow the guidelines I have set out above - or even better go and read the React Documentation. You may well find that your issue resolves when you follow these practices, but if not then post back here with your revised code and I will be glad to help further.

Edit/Sample Code

Parent Component:

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

    // our parent maintains an array of all elements in its state,
    // as well as the current page, and the number of items per page.
    // getSampleData can be seen in the fiddle - it just makes an array
    // of objects for us to render
    this.state = {
      allElements: getSampleData(),
      page: 1,
      numPerPage: 10
    };
  }


  // we pass this function as a prop to our Paginate component to allow
  // it to update the state of Parent
  setPage(pageNum) {
    this.setState({
        page: pageNum
    });
  }

  render() {   

    // get the appropriate elements from our own state   
    const firstItem = (this.state.page - 1) * this.state.numPerPage;
    const lastItem = this.state.page * this.state.numPerPage;
    const elementRender = 
      this.state.allElements
      .slice(firstItem, lastItem)
      .map(element => {
        return (
          <div key={element.itemNumber}>{element.itemName}</div>
        );
    });

    // numberOfElements, numPerPage and the setPage function from
    // Parent's state are passed
    // to the paginate component as props
    return (
        <div>
        <Paginate 
          numberOfElements={this.state.allElements.length} 
          numPerPage={this.state.numPerPage}
          setPage={this.setPage.bind(this)}
          />
        {elementRender}
      </div>
    );
  }
}

Paginate Component:

class Paginate extends React.Component {
    render() {
    const numberOfButtons = Math.ceil(this.props.numberOfElements / this.props.numPerPage);
    const buttons = [];

    // make a first page button
    buttons.push(<button key={0} onClick={() => this.props.setPage(1)}>First Page</button>);


    let i = 0;

    // add a button for each page we need
    while (i < numberOfButtons) {
        const page = ++i;
        buttons.push(
        <button 
            key={i} 
          onClick={() => this.props.setPage(page)}
        >
          Page {i}
        </button>
      );
    }

    // add a last page button
    buttons.push(<button key={i+1} onClick={() => this.props.setPage(i)}>Last Page</button>);
    return (
        <div>
        {buttons}
      </div>
    );
  }
}

JSFiddle