Загрузка данных из REST API
- четверг, 27 апреля 2017 г. в 03:14:41
Хочу поделиться ещё одним маленьким велосипедом — в первую очередь, чтобы получить бесценные советы. Дополнительные примеры можно посмотреть в исходниках фан-проекта на GitHub.
Почти все страницы в проекте обернуты компонентом Page:
const MyPage = () => (
<Page>
Hello World
</Page>
)
Для загрузки внешних данных, у компонента Page есть три передаваемых свойства (props):
Пример с применением Redux:
// components/Post/PostViewPage.js
const PostViewPage = ({ post, ...props }) => (
<Page {...props}>
<Post {...post} />
</Page>
)
const mapStateToProps = (state) => ({
post: state.postView,
isNotFound: isEmpty(state.postView),
})
const mapDispatchToProps = (dispatch, ownProps) => ({
onMounted: () => {
const id = parseInt(ownProps.match.params.id, 10)
dispatch(actions.read(id))
}
})
export default connect(mapStateToProps, mapDispatchToProps)(PostViewPage)
Обратите внимание, флаг isLoading не передаётся в props явно, он привязывается через mapStateToProps в компоненте Page (что будет продемонстрировано ниже по тексту).
Если у вас возникают вопросы по выражениям:
// деструктуризация и рест-параметры
{ post, ...props }
// спред
{...props}
… то можно обратиться к справочнику MDN:
Сайд-эффект actions.read(id) обеспечивает redux-thunk:
// ducks/postView.js
const read = id => (dispatch) => {
// установить флаг state.app.isLoading
dispatch(appActions.setLoading(true))
// сбросить значение state.postView
dispatch(reset())
// флаг о завершении таймаута
let isTimeout = false
// флаг о завершении загрузки
let isFetch = false
setTimeout(() => {
isTimeout = true
if (isFetch) {
dispatch(appActions.setLoading(false))
}
}, 500) // демонстрировать state.app.isLoading не менее 500 мс
axios(`/post/${id}`)
.then(response => {
const post = response.data
// записать данные в state.posts
dispatch(postsActions.setPost(post))
// записать данные в state.postView
dispatch(set(post))
isFetch = true
if (isTimeout) {
dispatch(appActions.setLoading(false))
}
})
.catch(error => {
isFetch = true
if (isTimeout) {
dispatch(appActions.setLoading(false))
}
dispatch(appActions.setMainError(error.toString()))
})
}
Когда данные загружаются слишком быстро, то возникает неприятый визуальный эффект мигания индикатора загрузки. Чтобы этого избежать, добавил таймер на 500 мс и логику на флагах isTimeout и isFetch.
Компонент Page, если отбросить прочие украшательства, обеспечивает процесс загрузки внешних данных:
// components/Page/Page.js
class Page extends React.Component {
_isMounted = false
componentDidMount() {
this._isMounted = true
const { onMounted } = this.props
if (onMounted !== void 0) {
onMounted()
}
}
render() {
const { isNotFound, isLoading, children } = this.props
if (this._isMounted && !isLoading && isNotFound) {
return <NotFound />
}
return (
<div>
<PageHeader />
<div>
{this._isMounted && !isLoading
?
children
:
<div>Загрузка...</div>
}
</div>
<PageFooter />
</div>
)
}
}
const mapStateToProps = (state, props) => ({
isLoading: state.app.isLoading
})
export default connect(mapStateToProps)(Page)
Как это работает? Первый проход render выполнится с выключенным флагом _isMounted — отображение индикатора загрузки. Далее выполнится lifecycle-метод componentDidMount, где включится флаг _isMounted и запустится функция обратного вызова onMounted; внутри onMounted мы вызываем сайд-эффект (например, actions.read(id)), где мы включаем флаг state.app.isLoading, что вызовет новый render — по прежнему отображение индикатора загрузки. После асинхронного вызова axios (или fetch) внутри нашего сайд-эффекта, мы выключаем флаг state.app.isLoading, что вызовет новый render — теперь, вместо отображения индикатора загрузки, выполнится render вложенного компонента (children); но если отработает включение флага isNotFound, то вместо render-а для вложенного компонента (children), выполнится render компонента <NotFound />.