Rendering a component declared inside the render function of another component does not include changes in the componentDidMount method in REACT 17

Viewed 74

I have come to an issue and I am having difficulties solving it. The array pageNumbers in component B is never populated. It depends on a prop that is only given value after the componentDidMount runs in class A. The Pagination component created in render method of class A does not reRender after it.

import React, { Component, useState, useEffec } from 'react'
import Pagination from './B';

class A extends Component {
constructor(props) {
        super(props)
        this.state = {
            weathers: [],           
        }
    }

    componentDidMount() {
        console.log("som v componentDidMOunt")
 return axios.post(`${URL}/retrieve/fromDb`} .then(
                response => {
                    this.setState({ weathers: response.data })

                }
 render() {
 let cmpnent = <Pagination totalItems = {this.state.weathers.length} />
        //totalItems is 0 in first render and length of weathers list in 2nd after 
        //componentDidMoun in Class A runs.
return cmpnent
}
}

import React, {Component} from 'react';

class B extends Component {
    constructor(props) {
        super(props)
        this.state = {
            
            pageNumbers :[],
        }
    }

componentDidMount(){
    this.fillPageNumber()
}
  
 fillPageNumber() { 
   this.state.pageNumbers = []
    for (let i = 1; i <= Math.ceil(this.props.totalItems / this.props.itemsPerPage); i++) {
        this.setState({pageNumbers : [this.state.pageNumbers, i]});
      }
    }
 render(){
      console.log("currentPage: " + this.props.currentPage)

  return (
    <nav>
      <ul className='pagination'>
        {this.state.pageNumbers.map(number => (
            console.log("number:" + number),
          <li key={number} className='page-item'>
              {number}
          </li>
        ))}
      </ul>
    </nav>
  );
        }
}
export default Pagination;
2 Answers

You are not doing correctly. try this. First collect all page numbers and then update state.

fillPageNumber() { 
    const pageNumbers = []
    for (let i = 1; i <= Math.ceil(this.props.totalItems / this.props.itemsPerPage); i++) {
        pageNumbers.push(i);
      }
    this.setState({pageNumbers})
}

Issues

You may want to review how state updates work in React. Using state correctly. You never set state directly (other than in constructor), so this (this.state.pageNumbers = []) is a no-go.

You are also calling setState within a loop and not updating from the previous state update, but instead are using the same this.state.pageNumbers array from the current render cycle. You also don't copy the previous state.

fillPageNumber() { 
  this.state.pageNumbers = [] // <-- mutates state, won't trigger rerender
  for (let i = 1; i <= Math.ceil(this.props.totalItems / this.props.itemsPerPage); i++) {
    this.setState({pageNumbers : [this.state.pageNumbers, i]}); // <-- fails to copy state correctly
  }
}

Solution

Simply create the new array first and update state once.

fillPageNumber() { 
  const pageNumbers = [];
  for (let i = 1; i <= Math.ceil(this.props.totalItems / this.props.itemsPerPage); i++) {
    pageNumbers.push(i);
  }
  this.setState({ pageNumbers });
}

Improved Solution

Since the page numbers are essentially derived "data" from props, they don't really belong in any component's state. You can actually compute the page numbers directly in the render function. It's also considered an anti-pattern to store props, or data derived from props, in state as this can lead to state/UI synchronicity issues. You really just need an array that is of length Math.ceil(totalItems / itemsPerPage), so just create an array with that length.

[...new Array(Math.ceil(totalItems / itemsPerPage)).keys()]

const itemsPerPage = 10;
const totalItems = 100;

const pageNumbers = [...new Array(Math.ceil(totalItems / itemsPerPage)).keys()];

console.log(JSON.stringify(pageNumbers));

This removes the need for fillPageNumber function and componentDidMount lifecycle method to initialize state. The correct page numbers will be computed and available on the first initial render. The constructor is also no longer necessary.

class B extends Component {
  render(){
    const { itemsPerPage, totalItems } = this.props;

    return (
      <nav>
        <ul className='pagination'>
          {[...new Array(Math.ceil(totalItems / itemsPerPage)).keys()].map(number => (
            <li key={number} className='page-item'>
              {number}
            </li>
          ))}
        </ul>
      </nav>
    );
  }
}

With such simplicity, you may as well convert this to a functional component.

const B = ({ itemsPerPage, totalItems }) => (
  <nav>
    <ul className='pagination'>
      {[...new Array(Math.ceil(totalItems / itemsPerPage)).keys()].map(number => (
        <li key={number} className='page-item'>
          {number}
        </li>
      ))}
    </ul>
  </nav>
);
Related