javascript

Загрузка данных из REST API

  • четверг, 27 апреля 2017 г. в 03:14:41
https://habrahabr.ru/post/327422/
  • Разработка веб-сайтов
  • ReactJS
  • JavaScript


Хочу поделиться ещё одним маленьким велосипедом — в первую очередь, чтобы получить бесценные советы. Дополнительные примеры можно посмотреть в исходниках фан-проекта на 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 />.