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

  • Tutorial

Хочу поделиться ещё одним маленьким велосипедом — в первую очередь, чтобы получить бесценные советы. Дополнительные примеры можно посмотреть в исходниках фан-проекта на 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))
    })
    .catch(error => {
      dispatch(appActions.setMainError(error.toString()))
    })
    .then(response => {
      isFetch = true
      if (isTimeout) {
        dispatch(appActions.setLoading(false))
      }
    })
}

Когда данные загружаются слишком быстро, то возникает неприятный визуальный эффект мигания индикатора загрузки. Чтобы этого избежать, добавил таймер на 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

    0

    Поступило мнение вне Хабра:


    Собственно, в чем главная недодумка — это использование пары флагов вместо enum’а состояний. сам че-то такое же делал. начал на флагах, потом смотрю и думаю: что за хрень, у меня же может быть куча состояний для более-менее сложных компонент. Навскидку:

    еще не начали загружаться [но хотим показать прелоадер а-ля фейсбук], собираемся загрузиться [ждем еще чего-то], загружается, не загрузилось с ошибкой, не загрузилось пустое, загрузилось пустое, загрузилось нормально, загрузили первую порцию [но сервер просит попросить еще попозже]

    То есть для чего-то супер-простого это вполне ок решение, и если оно в стиле кода вашего проекта, претензий никаких нет (хотя за отсутствие propTypes, даже если флоу, я нашим джуниорам стучу по рукам).

    Но для production quality — неаккуратненько.
      +1
      Спасибо за статью,
      очень актуально что в ней подробно «разжевывается» вопрос для тек, кто «в танке»,
      с пояснениями и ссылками, где почитать подробнее.
      И, хотя, еще не вся картина мира react-redux ясно видится, но создается впечатление, что еще чуть-чуть и ее можно будет понять
        +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>
          0

          А если загрузка занимает меньше, чем 500 мс, то индикатор загрузки не показываем? Тогда, если загрузка занимает 750 мс, потребуется ожидать 1000 мс: 500 мс до индикатора + 500 мс с индикатором. Правильно я понял?

            0
            Я включаю индикатор через 500мс, если загрузка не закончилась. В 90% он вообще не включается. Все пользователи в ЛС, справочники и формы в кэше браузера. По сети бегают только данные в небольших объемах. Повторю: у меня не веб-сайт.
              0

              Но тогда остается возможность кратковременного мигания индикатора загрузки. А мне было интересно решить этот вопрос.

                0
                мигает, собака. Но редко и не раздражает. Это лучше, чем железные полсекунды (имхо).
              +1
              Для отладки даже написал диалоговый промис :)
              function msgBoxYesNo(s) {
                  return new Promise((yes, no) => {
                      confirm(s) ? yes() : no();
                  });
              }
              
                0

                Прикольно. Забрал себе в чулан.

                  0

                  У меня тоже есть подобная поделка :)


                  export const sleep = (ms, reason = null) => new Promise((resolve, reject) =>
                    setTimeout(() => {
                      if (reason) {
                        reject(reason)
                        return
                      }
                      resolve()
                    }, ms)
                  )
                0

                Светлое завтра на Material-UI (Alpha-версия).

                  0
                  +1
                  с длинными например раскрывающимися списками будет некрасиво — контент будет прыгать при перерендере, мы делаем лоэдэр овэрлеем, который рисуется над контентом.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое