Хочу поделиться ещё одним маленьким велосипедом — в первую очередь, чтобы получить бесценные советы. Дополнительные примеры можно посмотреть в исходниках фан-проекта на GitHub.
Почти все страницы в проекте обернуты компонентом Page:
const MyPage = () => (
<Page>
Hello World
</Page>
)
Для загрузки внешних данных, у компонента Page есть три передаваемых свойства (props):
- Функция обратного вызова onMounted вызывается внутри метода жизненного цикла компонента componentDidMount; согласно документации React-а, именно в этом месте рекомендуется загружать внешние данные.
- Флаг isLoading мы передаём перед загрузкой внешних данных — true, и после завершения этой операции — false.
- Флаг isNotFound мы передаём, если загрузка внешних данных не увенчалась успехом.
Пример с применением 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 />.
Комментарии (13)
andreylat
26.04.2017 14:19+1Спасибо за статью,
очень актуально что в ней подробно «разжевывается» вопрос для тек, кто «в танке»,
с пояснениями и ссылками, где почитать подробнее.
И, хотя, еще не вся картина мира react-redux ясно видится, но создается впечатление, что еще чуть-чуть и ее можно будет понять
aon24
26.04.2017 14:23+1Это не велосипед, — обычное решение задачи.
У меня сделано примерно также (нет смысла выкладывать), только без редукса (строчек меньше и все в одном файле) и флаг «загрузка» зажигается через 500мс (у меня не веб-сайт, а приложение для работы с БД, — там почти все кэшируется).
В учебнике по CSS есть крутящийся кружочек вместо слова «загрузка»:
<div style="
margin: 10% auto;
border-bottom: 6px solid #fff;
border-left: 6px solid #fff;
border-right: 6px solid #c30;
border-top: 6px solid #c30;
border-radius: 100%;
height: 100px;
width: 100px;
-webkit-animation: spin .6s infinite linear;
-moz-animation: spin .6s infinite linear;
-ms-animation: spin .6s infinite linear;
-o-animation: spin .6s infinite linear;
animation: spin .6s infinite linear;
></div>comerc
26.04.2017 18:21А если загрузка занимает меньше, чем 500 мс, то индикатор загрузки не показываем? Тогда, если загрузка занимает 750 мс, потребуется ожидать 1000 мс: 500 мс до индикатора + 500 мс с индикатором. Правильно я понял?
aon24
26.04.2017 19:07Я включаю индикатор через 500мс, если загрузка не закончилась. В 90% он вообще не включается. Все пользователи в ЛС, справочники и формы в кэше браузера. По сети бегают только данные в небольших объемах. Повторю: у меня не веб-сайт.
aon24
26.04.2017 22:07+1Для отладки даже написал диалоговый промис :)
function msgBoxYesNo(s) { return new Promise((yes, no) => { confirm(s) ? yes() : no(); }); }
comerc
26.04.2017 22:44У меня тоже есть подобная поделка :)
export const sleep = (ms, reason = null) => new Promise((resolve, reject) => setTimeout(() => { if (reason) { reject(reason) return } resolve() }, ms) )
superyarik
28.04.2017 14:29+1с длинными например раскрывающимися списками будет некрасиво — контент будет прыгать при перерендере, мы делаем лоэдэр овэрлеем, который рисуется над контентом.
comerc
Поступило мнение вне Хабра: