【发布时间】: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的实现。如果您传递给它的任何函数(renderNewItems、renderNewPage、renderPagination)在Swiper的render方法内被调用,那么您不能在这些方法中调用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