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


  1. comerc
    26.04.2017 13:52

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


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

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

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

    Но для production quality — неаккуратненько.


  1. andreylat
    26.04.2017 14:19
    +1

    Спасибо за статью,
    очень актуально что в ней подробно «разжевывается» вопрос для тек, кто «в танке»,
    с пояснениями и ссылками, где почитать подробнее.
    И, хотя, еще не вся картина мира react-redux ясно видится, но создается впечатление, что еще чуть-чуть и ее можно будет понять


  1. 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>


    1. comerc
      26.04.2017 18:21

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


      1. aon24
        26.04.2017 19:07

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


        1. comerc
          26.04.2017 22:37

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


          1. aon24
            26.04.2017 22:46

            мигает, собака. Но редко и не раздражает. Это лучше, чем железные полсекунды (имхо).


      1. aon24
        26.04.2017 22:07
        +1

        Для отладки даже написал диалоговый промис :)

        function msgBoxYesNo(s) {
            return new Promise((yes, no) => {
                confirm(s) ? yes() : no();
            });
        }
        


        1. comerc
          26.04.2017 22:38

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


        1. comerc
          26.04.2017 22:44

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


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


    1. comerc
      28.04.2017 14:29

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



  1. superyarik
    28.04.2017 14:29
    +1

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