Во время недавней полной сборки с React я столкнулся с несколькими проблемами, которые заставили меня долго ломать голову. Некоторые из них были обычными проблемами, а некоторые были характерны только для приложения, которое я создал. Я хочу поделиться некоторыми из этих зависаний, чтобы другие могли учиться и расти на моем разочаровании. Все эти проблемы были решены путем отладки кроличьей норы, а также проверенного и надежного поиска в Google.

Вопросы, которые я хочу обсудить, следующие:

  1. useEffect и асинхронные функции
  2. createRef, scrollIntoView, stopPropagation
  3. google-maps-react и ReactDOM.render
  4. отправка токенов JWT при каждом запросе, требующем авторизации
  5. передача свойств и глобального состояния (Redux), хуки

Вы заметите, что некоторые из них не относятся к проблемам React, а на самом деле представляют собой чистые темы JavaScript и Rails. Чтобы обеспечить некоторый контекст, приложение, которое я создал, называется «Trailblaze». Это геосоциальное приложение, разработанное, чтобы помочь людям найти новых друзей, с которыми можно отправиться в приключения на свежем воздухе. Внешний интерфейс был построен на React, а внутренний — на Rails в качестве API. Приложение подключается к трем внешним API, чтобы обеспечить информативность и удобство использования.

Проблема №1 — useEffect и асинхронные функции

useEffect — это хук React, который позволяет нам запускать функции в определенное время жизненного цикла компонента. Этот хук имеет аналогичную функциональность методам жизненного цикла componentDidMount и componentDidUpdate. Проблема, с которой я столкнулся, заключалась в том, что разные вещи обновлялись в разное время в DOM. С помощью useEffect я смог обеспечить структуру при отображении новых результатов. В первом фрагменте в функции handleSubmit выполняются два запроса API (getUsers и getTrails), а также обновление хранилища Redux (updateQuery). Проблема заключалась в том, что при выполнении двух запросов API queryData (глобальное состояние Redux) имеет устаревшие значения, поскольку отправка в updateQuery еще не разрешена.

const handleSelect = async selection => {
 setAddress(selection)
 const response = await geocodeByAddress(selection)
 const results = await getLatLng(response[0])
}
const handleSubmit = () => {
 dispatch(updateQuery({ ...results, city: address }))
 dispatch(getUsers(queryData))
 dispatch(getTrails(queryData))
}

Второй фрагмент устраняет эту проблему, реализуя useEffect для запросов API. Этот хук useEffect будет запускаться всякий раз, когда изменяется queryData, что происходит при выполнении функции handleSubmit.

const handleSelect = async selection => {
 setAddress(selection)
 const response = await geocodeByAddress(selection)
 const results = await getLatLng(response[0])
}
useEffect(() => {
 dispatch(getUsers(queryData))
 dispatch(getTrails(queryData))
}, [queryData, dispatch])
const handleSubmit = () => {
 dispatch(updateQuery({ ...latlng, city: address }))
}

Проблема №2 — createRef, scrollIntoView, stopPropagation

В социальном приложении, таком как Trailblaze, функция чата почти необходима. Но с чатом / обменом текстовыми сообщениями приходится много прокручивать. Большинство людей не хотят видеть старые сообщения и вместо этого предпочитают видеть самые последние сообщения по умолчанию. Первое изображение показывает начало истории обмена сообщениями между двумя людьми. На втором изображении показаны самые последние сообщения, где представление должно отображаться по умолчанию при доступе к этой странице.

Для этого я использовал React Ref. Ссылки предоставляют нам доступ к DOM всякий раз, когда вам это нужно, но их следует использовать с осторожностью.

const messageInput = React.createRef();
return (
 <div className="newMessageForm" ref={messageInput}
  <form onSubmit={handleSubmit}>
    <input type="text" value={message.text}
      onChange={handleChange}  className="message-input"/>
    <div className="submit-container">
     <input type="submit"
      value="&#8593;" className="message-submit" />
    </div>
  </form>
 </div>

Вы можете видеть выше, что мы используем React.createRef() и устанавливаем ссылку, куда мы в конечном итоге хотим, чтобы наша прокрутка по умолчанию переходила. Поскольку поле ввода сообщения находится внизу страницы, я установил ссылку на элемент div, содержащий это поле ввода. Теперь, когда у нас есть ссылка, мы можем использовать useEffect, чтобы перейти к этой ссылке, потому что useEffect выполняется всякий раз, когда компонент отрисовывается.

useEffect(() => {
 messageInput.current.scrollIntoView();
})

Проблема №3 — google-maps-react и ReactDOM.render

Эта проблема очень специфична для моего проекта, а также для пакета google-maps-react. Изображение ниже иллюстрирует проблему. Информационное окно внутри карты Google предоставляет информацию пользователю. При щелчке маркера открывается информационное окно. Теперь, из-за того, как был написан google-maps-react, событие onClick не может быть создано внутри информационного окна. Поскольку все содержимое информационного окна (включая кнопку «Добавить в избранное») написано внутри компонента InfoWindow, кнопки, использующие onClick, никуда не денутся.

Чтобы решить эту проблему, я создал фиктивный div внутри компонента InfoWindow и передал функцию onOpen, как показано ниже:

<InfoWindow
 marker={markerInfo.activeMarker}
 visible={markerInfo.showInfo}
 onOpen={e => {renderButtonInfoWindow()}}
>
  ... my div with text content here...
 <div id="unique-placeholder" />
</InfoWindow>

Функция renderButtonInfoWindow вызывается всякий раз, когда щелкают маркер. Код внизу этой функции использует ReactDOM.render точно так же, как когда мы визуализируем наш компонент ‹App /› в div с идентификатором #root в нашем файле index.js. Это помещает нашу кнопку (имя переменной «div») именно там, где мы хотим, внутри нашего фиктивного div.

const renderButtonInfoWindow = () => {
 if (loggedIn()) {
  const div = (
   <button onClick={handleClickTrail} className="user-submit">
    Add Favorite
   </button>
  )
  ReactDOM.render(
  React.Children.only(div),
   document.getElementById("unique-placeholder")
  )
 }
}

Проблема № 4: sокончание токенов JWT при каждом запросе, требующем авторизации

Trailblaze — это одностраничное приложение с функцией аутентификации пользователя. Существует несколько способов аутентификации пользователей, но метод, который я реализовал, заключался в использовании веб-токенов JSON (JWT) и сохранении токена во внешнем интерфейсе в localStorage. Это не самый идеальный и не самый безопасный метод, но он работает для целей этой сборки.

То, что я узнал после настройки серверной части и успешного получения внешнего интерфейса для сохранения данных пользователя через его токен, заключается в том, что каждый запрос к серверной части требует новой строки в заголовках запросов на выборку (даже запросы GET). Это имеет смысл, так как пользователь хранит свой токен во внешнем интерфейсе, поэтому для достижения серверного действия, требующего авторизации, некоторая информация (а именно этот токен) должна быть отправлена ​​​​в серверную часть вместе с типичной информацией.

Пример запроса на выборку показан ниже. Если токен localStorage существует, мы вводим блок запроса на выборку. Разница между обычным запросом и запросом с внешней JWT-аутентификацией заключается в том, что мы включаем третью пару ключ/значение в headers.

componentDidMount = async () => {
 const token = localStorage.token
 if (token) {
  const response = await fetch(`${API_ROOT}/conversations`, {
   method: 'GET',
   headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'Authorization': `Bearer ${token}`
   }
  })
  const conversations = await response.json();
  const userConvos = conversations.filter(convo =>
   convo.author.id === this.props.currentUser ||
   convo.receiver.id === this.props.currentUser)
   this.setState({ ...this.state, conversations: userConvos })
 }
}

Наш бэкэнд Rails проверит, существует ли заголовок авторизации, и если да, то он декодирует токен и найдет текущего пользователя в базе данных на основе его user_id, полученного из декодированного токена.

def decode_token
 if auth_header
  token = auth_header.split(' ')[1]
  begin
   JWT.decode(token, 'application_secret', true, algorithm: 'HS256')
  rescue JWT::DecodeError
   nil
  end
 end
end

Проблема № 5: присваивание реквизитов по сравнению с глобальным состоянием (Redux), хуки

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

В моем приложении я изначально устанавливаю глобальное состояние для поискового запроса и текущего пользователя. Я знал, что мне понадобится доступ к поисковому запросу в нескольких компонентах, потому что все данные, которые делают это приложение полезным, проистекают из запроса. И поскольку существуют учетные записи пользователей, имеет смысл иметь глобальное состояние только для текущего пользователя, поскольку информация о пользователе требуется во многих компонентах для целей авторизации. В конце концов я добавил массивы следов и результатов поиска в глобальное состояние. Хотя я мог бы сделать их локальными состояниями, я знаю, что в будущем я буду добавлять новые функции, которые потребуют, чтобы они были доступны глобально.

const initialState = {
 currentUser: {},
 trails: []
 results: [],
 query: {
  lat: '',
  lng: '',
  radius: 10,
  agemin: '',
  agemax: '',
  gender: 'any'
 },
}

Вот пример использования локального состояния. Этот компонент отображает компонент Favorite с несколькими переданными свойствами, включая функции.

А вот пример глобального состояния. При применении поискового фильтра глобальное состояние поискового запроса обновляется. Это позволяет создавать более чистый код, который не поддерживает постоянное сверление.

Спасибо, что прочитали!

Репозиторий Github для этого приложения можно найти по ссылке ниже:

https://github.com/dougschallmoser/trailblaze-react-app