Search code examples
reactjstypescripttimeoutsetstate

React/TypeScript `Error: Maximum update depth exceeded.` when trying to redirect on timeout


I have a project that I'm trying to get to redirect from page 1 to 2 etc. dynamically. This has worked for me previously, but recently I'm getting this error:

Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.

After seeing this message this morning, and multiple SO pages saying NOT to call setState in render, I have moved my setTimeout call into componentDidMount.

So far I've tried - calling a function that changes this.props.pageWillChange property, then in render I return a object based on that condition - returning a object pending condition set in an inline if statement in render - turning pageWillChange into a local prop, rather than one that is inherited by the class (I quite like this option, as the state of this will be the same for every new version of this component)

Many more things, but these felt like they would work. Anyone able to help?

import React, { Component } from "react"
import axios from "axios"
import { GridList, GridListTile } from "@material-ui/core"

import "../assets/scss/tile.scss"
import Request from "../config.json"
import DataTile from "./DiagnosticDataTile"
import { IDiagnosticResultData } from "../interfaces/IDiagnosticResultData"
import { Redirect } from "react-router"

interface IProps {
  category: string
  redirect: string
}

interface IPageState {
  result: IDiagnosticResultData[]
  pageWillChange: boolean
}

class Dashboard extends Component<IProps, IPageState> {
  _isMounted = false
  changeTimeInMinutes = 0.25
  willRedirect: NodeJS.Timeout

  constructor(props: Readonly<IProps>, state: IPageState) {
    super(props)
    this.state = state
    console.log(window.location)
  }

  componentDidMount(): void {
    this._isMounted = true
    this.ChangePageAfter(this.changeTimeInMinutes)

    axios
      .get(Request.url)
      .then(response => {
        if (this._isMounted) {
          this.setState({ result: response.data })
        }
      })
      .catch(error => {
        console.log(error)
      })
  }

  componentWillUnmount(): void {
    this._isMounted = false
    clearTimeout(this.willRedirect)
  }

  ChangePageAfter(minutes: number): void {
    setTimeout(() => {
      this.setState({ pageWillChange: true })
    }, minutes * 60000)
  }

  render() {
    var data = this.state.result
    //this waits for the state to be loaded
    if (!data) {
      return null
    }

    data = data.filter(x => x.categories.includes(this.props.category))

    return (
      <GridList
        cols={this.NoOfColumns(data)}
        cellHeight={this.GetCellHeight(data)}
        className="tileList"
      >
        {this.state.pageWillChange ? <Redirect to={this.props.redirect} /> : null}
        {data.map((tileObj, i) => (
          <GridListTile
            key={i}
            className="tile"
          >
            <DataTile data={tileObj} />
          </GridListTile>
        ))}
      </GridList>
    )
  }
}

export default Dashboard

(very new with React and TypeScript, and my first SO post woo!)


Solution

  • Try the code below, also couple of points:

    • No need for _isMounted field. Code in 'componentDidMount' always runs after it's mounted.
    • No need to set state in constructor. Actually there is no need for constructor anymore.
    • I can't see much point of clearTimeout in componentWillUnmount mount. It's never asigned to timeout.

    About routing. U can use 'withRouter' high order function to change route programmatically in changePageAfter method.

    Hope this helps!

    import axios from "axios"
    import { GridList, GridListTile } from "@material-ui/core"
    
    import "../assets/scss/tile.scss"
    import Request from "../config.json"
    import DataTile from "./DiagnosticDataTile"
    import { IDiagnosticResultData } from "../interfaces/IDiagnosticResultData"
    import { Redirect, RouteComponentProp } from "react-router"
    
    interface PropsPassed {
      category: string
      redirect: string
    }
    
    type Props = PropsPassed & RouteComponentProp
    
    interface IPageState {
      result: IDiagnosticResultData[]
      pageWillChange: boolean
    }
    
    class Dashboard extends Component<Props, IPageState> {
      changeTimeInMinutes = 0.25
      willRedirect: NodeJS.Timeout
    
      componentDidMount(): void {
        this.ChangePageAfter(this.changeTimeInMinutes)
    
        axios
          .get(Request.url)
          .then(response => {   
              this.setState({ result: response.data })
          })
          .catch(error => {
            console.log(error)
          })
      }
    
      changePageAfter(minutes: number): void {
        setTimeout(() => {
          this.props.history.push({
            pathname: '/somepage',
      });
        }, minutes * 60000)
      }
    
      render() {
        var data = this.state.result
        //this waits for the state to be loaded
        if (!data) {
          return null
        }
    
        data = data.filter(x => x.categories.includes(this.props.category))
    
        return (
          <GridList
            cols={this.NoOfColumns(data)}
            cellHeight={this.GetCellHeight(data)}
            className="tileList"
          >
            {data.map((tileObj, i) => (
              <GridListTile
                key={i}
                className="tile"
              >
                <DataTile data={tileObj} />
              </GridListTile>
            ))}
          </GridList>
        )
      }
    }
    
    export default withRouter(Dashboard)