【问题标题】:React Native setState(...): Cannot update during an existing state transitionReact Native setState(...):在现有状态转换期间无法更新
【发布时间】:2017-02-09 05:55:00
【问题描述】:

我目前正在开发一个 React Native 应用程序,该应用程序的屏幕带有一个自定义滑动器组件,允许用户滑动浏览一组照片。最初我调用了一个 API 调用来加载 10 张照片,当当前照片索引接近存储它们的数组的末尾时,用户可以滑动加载另外 10 张。

由于我正在进行分页,因此我想跟踪用户所在的页面。例如,如果索引在 0-9 之间,则用户在第一页,10-19 为第二页,以此类推

我已经能够成功跟踪用户所在的页面,但是当我在状态内更新它时会生成一个警告,这让我认为有更好的方法来处理这个问题。

Warning: setState(...): Cannot update during an existing state transition
(such as within `render` or another component's constructor). Render
methods should be a pure function of props and state; constructor side
effects are an anti-pattern, but can be moved to `componentWillMount`.

这是我的屏幕实现:

'use strict'

import React, { Component } from 'react'
import { Text, View, Image, Dimensions, Platform } from 'react-native'
import { StackNavigator } from 'react-navigation'
import Swiper from 'react-native-swiper'
import styles from './styles/ImageScreenStyle'

const { width, height } = Dimensions.get('window')

class ImageScreen extends Component {
  constructor(props) {
    super(props)
    this.state = {
      page: this.props.navigation.state.params.page,
      key: this.props.navigation.state.params.key,
      items: this.props.navigation.state.params.array,
    }
    this._fetchNextPage = this._fetchNextPage.bind(this)
    this._renderNewItems = this._renderNewItems.bind(this)
    this._renderNewPage = this._renderNewPage.bind(this)
  }

  // Update the parent state to push in new items
  _renderNewItems(index, items) {
    let oldItems = this.state.items
    let newItems = oldItems.concat(items)
    this.setState({ items: newItems, key: index })
  }

  // This generates a warning but still works?
  _renderNewPage(page) {
    let newPage = this.state.page
    newPage.current = page
    this.setState({ page: newPage })
  }

  render() {
    return (
      <Swiper
        showsButtons
        loop = { false }
        index = { this.state.key }
        renderPagination = { this._renderPagination }
        renderNewItems = { this._renderNewItems }
        renderNewPage = { this._renderNewPage }
        fetchNextPage = { this._fetchNextPage }
        page = { this.state.page }>
        { this.state.items.map((item, key) => {
          return (
            <View key = { key } style = { styles.slide }>
              <Image
                style = {{ width, height }}
                resizeMode = 'contain'
                source = {{ uri: item.photo.images[1].url }}
              />
            </View>
          )
        })}
      </Swiper>
    )
  }

  _renderPagination(index, total, context) {
    const photoPage = Math.floor(index / 10) + 1
    const currentPage = this.page.current

    // Update the current page user is on
    if (photoPage !== currentPage) {
      return this.renderNewPage(photoPage)
    }

    // Add more photos when index is greater or equal than second last item
    if (index >= (total - 3)) {
      this.fetchNextPage().then((data) => {
        // Here is where we will update the state
        const photos = data.photos

        let items = Array.apply(null, Array(photos.length)).map((v, i) => {
          return { id: i, photo: photos[i] }
        })

        // Pass in the index because we want to retain our location
        return this.renderNewItems(index, items)
      })
    }
  }

  _fetchNextPage() {
    return new Promise((resolve, reject) => {
      const currentPage = this.state.page.current
      const nextPage = currentPage + 1
      const totalPages = this.state.page.total

      if (nextPage < totalPages) {
        const PAGE_URL = '&page=' + nextPage

        fetch(COLLECTION_URL + PAGE_URL + CONSUMER_KEY)
        .then((response) => {
          return response.json()
        })
        .then((data) => {
          return resolve(data)
        })
        .catch((error) => {
          return reject(error)
        })
      }
    })
  }
}

export default ImageScreen

分页在一个函数中处理,我使用 _renderNewItems 和 _renderNewPage 方法来处理新照片和页面索引的状态。

更新

我已经更改了我的代码以反映提供的答案,但我没有任何运气得到抑制警告。我认为绑定并更改为componentWillMount() 方法会有所帮助。这是我目前的立场:

class ImageScreen extends Component {
  constructor(props) {
    super(props)
    this.state = {
      page: '',
      key: '',
      items: []
    }
    this._fetchNextPage = this._fetchNextPage.bind(this)
    this._renderNewItems = this._renderNewItems.bind(this)
    this._renderNewPage = this._renderNewPage.bind(this)
  }

  componentWillMount() {
    this.setState({
      page: this.props.navigation.state.params.page,
      key: this.props.navigation.state.params.key,
      items: this.props.navigation.state.params.array
    })
  }

  render() {
    return (
      <Swiper
        showsButtons
        loop = { false }
        index = { this.state.key }
        renderPagination = { this._renderPagination.bind(this) }
        renderNewItems = { this._renderNewItems.bind(this) }
        renderNewPage = { this._renderNewPage.bind(this) }
        fetchNextPage = { this._fetchNextPage.bind(this) }>
        { this.state.items.map((item, key) => {
          return (
            <View key = { key } style = { styles.slide }>
              <Image
                style = {{ width, height }}
                resizeMode = 'contain'
                source = {{ uri: item.photo.images[1].url }}
              />
            </View>
          )
        })}
      </Swiper>
    )
  }

  _renderPagination(index, total, context) {
    const photoPage = Math.floor(index / 10) + 1
    const statePage = this.state.page.current

    if (photoPage !== statePage) {
      return this._renderNewPage(photoPage)
    }


    if (index >= (total - 3)) {
      this._fetchNextPage().then((data) => {
        const photos = data.photos

        let items = Array.apply(null, Array(photos.length)).map((v, i) => {
          return { id: i, photo: photos[i] }
        })

        return this._renderNewItems(index, items)
      })
    }
  }

  _renderNewItems(index, items) {
    let oldItems = this.state.items
    let newItems = oldItems.concat(items)
    this.setState({ items: newItems, key: index })
  }

  // TO-DO: Fix the warning this generates
  _renderNewPage(page) {
    let newPage = this.state.page
    newPage.current = page
    this.setState({ page: newPage })
  }

  _fetchNextPage() {
    return new Promise((resolve, reject) => {
      const currentPage = this.state.page.current
      const nextPage = currentPage + 1
      const totalPages = this.state.page.total

      if (nextPage < totalPages) {
        const PAGE_URL = '&page=' + nextPage

        fetch(COLLECTION_URL + PAGE_URL + CONSUMER_KEY)
        .then((response) => {
          return response.json()
        })
        .then((data) => {
          return resolve(data)
        })
        .catch((error) => {
          return reject(error)
        })
      }
    })
  }
}

export default ImageScreen

更新 2

解决了这个问题。正如 Felix 在 cmets 中指出的那样,renderPagination 方法经常重新渲染,所以我使用了 Swiper(来自 react-native-swiper)的 onMomentumScrollEnd 属性来更新页面信息。对于任何可能需要它的人,这是我的代码:

class ImageScreen extends Component {
  constructor(props) {
    super(props)
    this.state = {
      page: '',
      key: '',
      items: []
    }
  }

  componentWillMount() {
    this.setState({
      page: this.props.navigation.state.params.page,
      key: this.props.navigation.state.params.key,
      items: this.props.navigation.state.params.array
    })
  }

  render() {
    return (
      <Swiper
        showsButtons
        loop = { false }
        index = { this.state.key }
        onMomentumScrollEnd = { this._onMomentumScrollEnd.bind(this) }
        renderPagination = { this._renderPagination.bind(this) }
        renderNewItems = { this._renderNewItems.bind(this) }
        fetchNextPage = { this._fetchNextPage.bind(this) }>
        { this.state.items.map((item, key) => {
          return (
            <View key = { key } style = { styles.slide }>
              <Image
                style = {{ width, height }}
                resizeMode = 'contain'
                source = {{ uri: item.photo.images[1].url }}
              />
            </View>
          )
        })}
      </Swiper>
    )
  }

  _renderNewItems(index, items) {
    let oldItems = this.state.items
    let newItems = oldItems.concat(items)
    this.setState({ items: newItems, key: index })
  }

  _renderPagination(index, total, context) {
    if (index >= (total - 3)) {
      this._fetchNextPage().then((data) => {
        const photos = data.photos

        let items = Array.apply(null, Array(photos.length)).map((v, i) => {
          return { id: i, photo: photos[i] }
        })

        return this._renderNewItems(index, items)
      })
    }
  }

  _fetchNextPage() {
    return new Promise((resolve, reject) => {
      const currentPage = this.state.page.current
      const nextPage = currentPage + 1
      const totalPages = this.state.page.total

      if (nextPage < totalPages) {
        const PAGE_URL = '&page=' + nextPage

        fetch(COLLECTION_URL + PAGE_URL + CONSUMER_KEY)
        .then((response) => {
          return response.json()
        })
        .then((data) => {
          return resolve(data)
        })
        .catch((error) => {
          return reject(error)
        })
      }
    })
  }

  _onMomentumScrollEnd(e, state, context) {
    const photoPage = Math.floor(state.index / 10) + 1
    const statePage = this.state.page.current
    console.log('Current page: ' + photoPage)
    console.log('State page: ' + statePage)


    if (photoPage !== statePage) {
      this._renderNewPage(photoPage)
    }
  }

  _renderNewPage(page) {
    let newPage = this.state.page
    newPage.current = page
    this.setState({ page: newPage })
  }
}

export default ImageScreen

【问题讨论】:

  • 错误信息对我来说似乎很清楚:调用this.setState 的辅助函数之一必须在调用render() 时调用。这是不好的。不要在渲染时调用的函数中调用this.setState
  • @FelixKling 我不知道这可能发生在哪里 - 你介意再看看我更新的代码吗?
  • 你需要看Swiper的实现。如果您传递给它的任何函数(renderNewItemsrenderNewPagerenderPagination)在Swiperrender 方法内被调用,那么您不能在这些方法中调用setState。道具名称以render 开头的事实似乎表明您不应该在相应的方法中调用setState。如果您只想知道渲染了哪个页面,我相信Swiper 提供了一种传递回调以获取这些更改通知的方法。
  • Looking at the source code, Swiper 在其render 方法中调用this.props.renderPagination。您作为renderPagination 属性传递的函数间接调用setState,这就是您收到该错误的原因。 According to the issues on the repo,您可以使用onMomentumScrollEnd 获得页面更改通知。
  • @FelixKling 这就是修复!我将所有内容移至_onMomentumScrollEnd 方法,并且不再从renderPagination 调用该状态。感谢您对此进行调查。

标签: javascript reactjs react-native


【解决方案1】:

使用应该更新

shouldComponentUpdate();

如果上面的方法不行,你也可以试试这个。

this.forceUpdate();

【讨论】:

  • 你建议我用哪种方法调用它?
  • 对于更新状态,您可以使用这些方法。有时我们遇到这些错误,然后我们需要强制更新状态(如果有必要)。
【解决方案2】:

不确定,但我认为问题出在这一行:

renderPagination = { this._renderPagination }

你忘了bind这个事件,这是创建loop,因为你在这个里面使用this.setState。试试这个:

renderPagination = { this._renderPagination.bind(this) }

原因:每当你再次使用this.setStateReactrender 整个component 时,如果你没有bind 任何方法,那将在期间被调用每个rendering,除非您在该函数中不使用setState,否则不会产生任何问题,但如果您在其中使用setState,那么它将创建loop,它将再次调用render@ 987654337@.......

【讨论】:

  • 他为所有其他方法定义了binding,但他忘记了bind这个:_renderPagination,请检查。
  • 你是对的。但这并不能解释问题中的错误。我看不到如何不绑定函数“正在创建loop”。如果函数没有绑定,this 将是undefined 并且会抛出一个完全不同的错误。
  • 请检查这个jsfiddle的控制台,这就是我想说的:jsfiddle.net/o3czw0L2
  • 但这不适用于这里。 OP 使用renderPagination = { this._renderPagination } 而不是renderPagination = { this._renderPagination() }(即他们没有直接调用this._renderPagination)。虽然需要.bind() 是正确的,但它并不能解决实际问题。
  • 哦,明白了,对不起我的错误,将删除答案:)
猜你喜欢
  • 2017-05-31
  • 2017-01-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-20
  • 2017-05-16
  • 2016-09-20
  • 1970-01-01
相关资源
最近更新 更多