如何避免在 React 的 shouldComponentUpdate() 中使用 setState?

How to avoid using setState in shouldComponentUpdate() in React?

我为论坛视图创建了一个 React 组件。

源码可见:https://github.com/WayneHo25/YeePlus/blob/forum-feature/frontend/src/views/ForumPage/Sections/SectionPills.jsx

几个不同论坛的顶部有一些标签。

当我们点击标签时,state forumID 用于检查点击了哪个标签以及应该显示哪个论坛。

state discussions用于存放论坛的内容。

state isLoading用于标记请求何时发送到服务器。

import React from 'react'
// nodejs library that concatenates classes
import classNames from 'classnames'
// @material-ui/core components
import withStyles from '@material-ui/core/styles/withStyles'
import Tooltip from '@material-ui/core/Tooltip'
import Tab from '@material-ui/core/Tab'
import Tabs from '@material-ui/core/Tabs'
import CircularProgress from '@material-ui/core/CircularProgress'
// @material-ui/icons
import Reply from '@material-ui/icons/Reply'
// core components
import GridContainer from 'components/Grid/GridContainer.jsx'
import GridItem from 'components/Grid/GridItem.jsx'
import Button from 'components/CustomButtons/Button.jsx'
import Media from 'components/Media/Media.jsx'

import { getDiscussionsByForumID } from 'util/APIUtils'

import profile4 from 'assets/img/faces/card-profile4-square.jpg'

import sectionPillsStyle from 'assets/jss/material-kit-pro-react/views/blogPostsSections/sectionPillsStyle.jsx'

class SectionPills extends React.Component {
  constructor (props) {
    super(props)
    this.state = {
      discussions: [],
      isLoading: false,
      active: 0,
      forumID: 1
    }
    this.changeForum = this.changeForum.bind(this);
  }

  handleChange = (event, active) => {
    this.setState({ active });
  };

  changeForum(fid) {
    this.setState({ forumID: fid });
  }

  loadDiscussionList () {
    let promise

    promise = getDiscussionsByForumID(this.state.forumID)

    if (!promise) {
      return
    }

    this.setState({
      isLoading: true
    })

    promise
      .then(response => {
        const discussions = this.state.discussions.slice()

        this.setState({
          discussions: discussions.concat(response),
          isLoading: false
        })
      }).catch(error => {
        this.setState({
          isLoading: false
        })
      })
  }

  componentDidMount () {
    this.loadDiscussionList()
  }

  shouldComponentUpdate (nextState) {
    if (nextState.forumID != this.state.forumID) {
      this.loadDiscussionList()
    }
  }

  render () {
    const { classes } = this.props
    const flexContainerClasses = classNames({
      [classes.flexContainer]: true,
      [classes.horizontalDisplay]: false
    })
    const pillsClasses = classNames({
      [classes.pills]: true,
      [classes.horizontalPills]: false
    })
    const tabButtons = (
      <Tabs
        classes={{
          root: classes.root,
          fixed: classes.fixed,
          flexContainer: flexContainerClasses,
          indicator: classes.displayNone
        }}
        value={this.state.active}
        onChange={this.handleChange}
        centered={true}
      >
        <Tab
          key={1}
          label='YeePlus'
          classes={{
            root: pillsClasses,
            labelContainer: classes.labelContainer,
            label: classes.label,
            selected: classes.primary
          }}
          onClick={() => this.changeForum(1)}
        />
        <Tab
          key={2}
          label='Yeelight'
          classes={{
            root: pillsClasses,
            labelContainer: classes.labelContainer,
            label: classes.label,
            selected: classes.primary
          }}
          onClick={() => this.changeForum(2)}
        />
        <Tab
          key={3}
          label='Feedback'
          classes={{
            root: pillsClasses,
            labelContainer: classes.labelContainer,
            label: classes.label,
            selected: classes.primary
          }}
          onClick={() => this.changeForum(3)}
        />
      </Tabs>
    )
    const discussionList = []
    this.state.discussions.forEach((discussion, discussionIndex) => {
      discussionList.push(
        <Media
          key={discussion.id}
          avatar={profile4}
          title={
            <span>
              {discussion.title} <small>· 7 minutes ago</small>
            </span>
          }
          body={
            <p className={classes.color555}>
              {discussion.title}
            </p>
          }
          footer={
            <div>
              <Tooltip
                id='tooltip-tina'
                title='Reply to discussion'
                placement='top'
                classes={{ tooltip: classes.tooltip }}
              >
                <Button
                  color='primary'
                  simple
                  className={classes.footerButtons}
                >
                  <Reply className={classes.footerIcons} /> Reply
                </Button>
              </Tooltip>
            </div>
          }
        />)
    })
    const ColorCircularProgress = withStyles({
      root: {
        color: '#9c27b0'
      }
    })(CircularProgress)
    return (
      <div className={classes.section}>
        <GridContainer justify='center'>
          <GridItem xs={12} sm={10} md={8}>
            {tabButtons}
            <div className={classes.tabSpace} />
            {discussionList}
            <div className={classes.textCenter} >
            {
              this.state.isLoading
                ? <ColorCircularProgress /> : null
            }
            </div>
          </GridItem>
        </GridContainer>
      </div>
    )
  }
}

export default withStyles(sectionPillsStyle)(SectionPills)

错误信息:

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.

 50 |   return
  51 | }
  52 | 
> 53 | this.setState({
     | ^  54 |   isLoading: true
  55 | })
  56 | 

我想保留 state isLoading,但找不到同时保留它们的解决方案。

您无法在 componentShouldUpdate 中设置状态。相反,在发布更新论坛 ID 的 setState 时,还会启动为该新论坛加载讨论的过程。由于这是一个异步过程,因此当它完成时,您需要检查用户是否同时单击了另一个论坛。大致:

constructor(props) {
  // ...
  this.loadId = 0;
  // ...
}

changeForum(fid) {
  this.setState({ forumID: fid, discussions: [] }); // *** Note clearing discussions!
  this.loadDiscussionList(fid);
}

loadDiscussionList(fid = this.state.forumID) {
  // *** To handle out-of-sequence completions and/or the user changing forums
  // while loading is happening, use a unique load identifier (more below)
  let loadId = ++this.loadId;
  let promise = getDiscussionsByForumID(fid);
  if (!promise) { // ??? Normally a method returning a promise always does so
    return;
  }

  this.setState({
    isLoading: true
  });

  promise
    .then(response => {
      const discussions = this.state.discussions.slice();

      this.setState(({forumID}) => {
        // Note the check, *within* the `setState` callback:
        // 1. We're still on the same forum, and
        // 2. There hasn't been a more recent call to `loadDiscussionList`
        if (forumID === fid && this.loadId === loadId) {
          return {
            discussions: discussions.concat(response),
            loading: false
          };
        }
      });
    })
    .catch(error => {
      // *** Usually you report an error
      // *** Same check as above
      this.setState(({forumID}) => {
        if (forumID === fid && this.loadId === loadId) {
          this.setState({isLoading: false});
        }
      });
    });
}

您还需要调整(或移除)您的 shouldComponentUpdate。它至少需要检查 forumIDisLoadingdiscussions,可能还有其他的。