Как сделать поиск пользователей по GitHub без React + RxJS 6 + Recompose

  • Tutorial
Эта статья является ответом на статью-перевод «Как сделать поиск пользователей по GitHub используя React + RxJS 6 + Recompose», которая буквально вчера научила нас как надо использовать React, RxJS и Recompose вместе. Что ж, предлагаю теперь посмотреть, как это можно реализовать без оных инструментов.




Disclaimer
Многим может показаться, что данная статья содержит элементы троллинга, написана впопыхах и по фану… Так вот, это так.


Так как это ответная статья, я решил построить ее в том же пошаговом формате, что и оригинал. Кроме того, статья также призвана сравнить оригинальную реализацию с описанной здесь. Поэтому в ней присутствует обилие отсылок и цитат из оригинала. Приступим.

Эта статья рассчитана на людей имеющих опыт работы с React и RxJS. Я всего лишь делюсь шаблонами, которые я посчитал полезными для создания такого UI.

Эта статья рассчитана на людей имеющих опыт работы с Javascript (ES6), HTML и CSS. Кроме того, в своей реализации я буду использовать «исчезающий» фреймворк SvelteJS, но он настолько прост, что вам не обязательно иметь опыт его использования, чтобы понимать код.

Делаем мы все ту же штуку:



Без классов, работы с жизненным циклом или setState.

Да, без классов, работы с жизненным циклом или setState. А также без React, ReactDOM, RxJS, Recompose. И кроме того без componentFromStream, createEventHandler, combineLatest, map, startWith, setObservableConfig, BehaviorSubject, merge, of, catchError, delay, filter, map, pluck, switchMap, tap, {another bullshit}… Короче вы поняли.

Подготовка


Все что нужно лежит в моем REPL примере на сайте SvelteJS. Можете потыркать там, либо локально, скачав исходники оттуда (кнопка с характерной иконкой).

Для начала создадим простой файлик App.html, который будет являться root-компонентом нашего виджета, со следующим содержанием:

<input placeholder="GitHub username">

<style>
  input {
    font-size: 20px;
    border: 1px solid black;
    border-radius: 3px;
    margin-bottom: 10px;
    padding: 10px;
  }
</style>


Здесь и далее, я использую стили из оригинальной статьи. Обратите внимание, что уже прямо сейчас они в scope, т.е. применяются только к данному компоненту и можно смело использовать имена тегов там где это актуально.

Стили писать буду прямо в компонентах, потому что SFC, а также потому что REPL не поддерживает вынос CSS/JS/HTML в разные файлы, хотя это легко делается с помощью препроцессоров Svelte.

Recompose


Отдыхаем…

Поточный компонент


… загораем…

Конфигурирование


… пьем кофе…

Recompose + RxJS


… пока другие…

Map


… работают.

Добавляем обработчик событий


Не совсем обработчик конечно, просто биндинг:

<input bind:value=username placeholder="GitHub username">


Ну и определим значение username по-умолчанию:

<script>
  export default {
    data() {
      return {
	username: ''
      };
    }
  };
</script>


Теперь, если вы начнете вводить что-то в поле ввода, значение username будет меняться.


Проблема яйца и курицы


Ни куриц, ни яиц, ни проблем с другими животными, мы же не RxJS используем.

Связываем вместе


Все уже реактивно и связано. Так что дальше пьем кофе.

Компонент User


Этот компонент у нас будет отвечать за отображение пользователя, имя которого мы будет ему передавать. Он будет получать value из компонента App и переводить его в AJAX запрос.

«Воу-воу-воу, полегче» ©

У нас этот компонент будет тупым и просто отображать красивую карточку юзера по заранее известной модели. Мало ли откуда могут приходить данные и/или в каком месте интерфейса мы захотим показать эту карточку.

Примерно так будет выглядить компонент User.html:

<div class="github-card user-card">
  <div class="header User" />
    <a class="avatar" href="https://github.com/{login}">
      <img src="{avatar_url}&s=80" alt={name}>
    </a>
    <div class="content">
      <h1>{name || login}</h1>
      <ul class="status">
        <li>
          <a href="https://github.com/{login}?tab=repositories">
	    <strong>{public_repos}</strong>Repos
	  </a>
	</li>
	<li>
	  <a href="https://gist.github.com/{login}">
	    <strong>{public_gists}</strong>Gists
	  </a>
	</li>
	<li>
	  <a href="https://github.com/{login}/followers">
	    <strong>{followers}</strong>Followers
	  </a>
	</li>
    </ul>
  </div>
</div>

<style>
  /* стили */
</style>


JSX/CSS


CSS просто добавили в компонент. Вместо JSX у нас HTMLx встроенный в Svelte.

Контейнер


Контейнером выступает любой родительский компонент для компонента User. В данном случае это компонент App.

debounceTime


Дебоунсить ввод текста не имеет смысла, если операция лишь перезаписывает значение в модели данных. Так что в самом биндинге нам это не нужно.

pluck


filter


map


Подключаем


Вернёмся в App.html и импортируем компонент User:

import User from './User.html';


Запрос данных


GitHub предоставляет API для получения информации о пользователе:

Для демонстрации, просто напишем маленький файлик api.js, который будет абстрагировать получение данных и экспортировать соответствующую функцию:

import axios from 'axios';

export function getUserCard(username) {
  return axios.get(`https://api.github.com/users/${username}`)
              .then(res => res.data);
}


И точно также импортируем эту функцию в App.html.

Теперь сформулируем задачу более предметным языком: нам нужно при изменении одного значения в модели данных (username) изменять другое значение. Назовем его соответственно user — данные о юзере, которые мы получаем из API. Реактивность во всей красе.

Для этого, напишем вычисляемое свойство Svelte, используя следующую конструкцию в App.html:

<script>
  import { getUserCard } from './api.js';
  ...
  export default {
    ...
    computed: {
      user: ({ username }) => username && getUserCard(username)
    }
  };
</script>


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

В оригинальной статье эта проблема решается встроенной в RxJS функцией debounceTime, которая не дает нам слишком часто запрашивать данные. Для нашей же реализации можно воспользоваться standalone решением, типа debounce-promise или любым другим подходящим, благо есть из чего выбрать.

<script>
  import debounce from 'debounce-promise';
  import { getUserCard } from './api.js';
  ...
  const getUser = debounce(getUserCard, 1000);
  ...
  export default {
    ...
    computed: {
      user: ({ username }) => username && getUser(username)
    }
  };
</script>


Итак, эта либа создает debounce-версию переданной ей функции, которую мы потом используем в вычисляемом свойстве.

switchMap


ajax


RxJS предоставляет собственную реализацию ajax которая прекрасно работает со switchMap!

Так как мы не используем RxJS и тем более switchMap, мы можем использовать любую библиотеку для работы с ajax.

Я использую axios, потому что он удобный для меня, но вы можете использовать что угодно и сути дела это не меняет.

Пробуем


Для начала нам нужно зарегистрировать компонент User для использования его в качестве тега шаблона, так как сам по себе импорт не добавляет компонент в контекст шаблона:

<script>
  import User from './User.html';
  ...
  export default {
    components: { User }
    ...
  };
</script>

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

Далее, значение user — это не сами данные, а промис на эти данные. Потому что мы халявщики и не хотим делать вообще никакой работы, только пить кофе с печеньками.

Для того, чтобы передать реальные данные в компонент User, мы можем воспользоваться специальной конструкцией для работы с промисами:

{#await user} <!-- тут это еще промис -->
{:then user} <!-- а здесь уже объект с данными. имя можно дать любое. -->
  {#if user}
  <User {...user} />
  {/if}
{/await}

Имеет смысл проверить объект с данными на существование, прежде чем передавать его в компонент User. Spread-оператор здесь позволяет «расщепить» объект на отдельные пропсы при создании экземпляра компонента User.



Короче работинг.

Обработка ошибок


Попробуйте ввести несуществующее имя пользователя.

Наше приложение сломано.

Ваше наверное да, но наше точно нет))) Просто ничего не произойдет, хотя это конечно не дело.

catchError


Добавим дополнительный блок для обработки rejected-промиса:

{#await user}
{:then user}
  {#if user}
  <User {...user} />
  {/if}
{:catch error}
  <Error {...error} />
{/await}


Компонент Error


<div class="error">
  <h2>Oops!</h2>
  <b>{response.status}: {response.data.message}</b>
  <p>Please try searching again.</p>
</div>

Сейчас наш UI выглядит гораздо лучше:

И не говорите, а главное никаких усилий.



Индикатор загрузки


Короче там дальше вообще ересь началась, со всякими там BehaviorSubject-ами и иже с ними. Мы же просто добавим индикатор загрузки и не будем доить слона:

{#await user}
  <h3>Loading...</h3>
{:then user}
  {#if user}
  <User {...user} />
  {/if}
{:catch error}
  <Error {...error} />
{/await}

Результат?

Два крошечных logic-less компонента (User и Error) и один управляющий компонент (App), где наиболее сложная бизнес-логика описана в одну строку — создание вычисляемого свойства. Никаких обмазываний observable-объектами с ног до головы и подключений +100500 инструментов, которые вам не нужны.

Интерактивная демка

Пишите простой и понятный код. Пишите меньше кода, а значит меньше работайте и проводите больше времени с семьей. Живите!

Всем счастья и здоровья!

Все :)


FYI


Если вы уже посмотрели пример в REPL, то наверное обратили внимание на тост с warning'ом слева внизу:
Compiled, but with 1 warning — check the console for details

Если вы не поленитесь открыть консоль, то увидите вот такое сообщение:
Unused CSS selector
.user-card .Organization {
background-position: top right;
}

Статический анализатор Svelte сообщает нам, что некоторые стили компонентов не используются. Кроме того, по вашему желанию или повелению дефолтных настроек компилятора, неиспользуемые стили буду удалены из итогового css-бандла без вашего участия.

P/S


Читайте и другие статьи про Svelte, а также заглядывайте в русскоязычный телеграм канал SvelteJS. Будем рады вам!
Поделиться публикацией
Комментарии 20
    +3
    Спасибо за статью.
    Svelte действительно интересный фреймворк.
    Но, справедливости ради, надо заметить, что цель статьи, на которую вы отвечаете, была не в том чтобы максимально просто решить задачу, а показать как пользоваться такими инструментами как React, RxJS 6 и Recompose.
      +4
      Спасибо, рад что вы заинтересовались.))

      По второй части, согласен с вами. Однако считаю, что оригинальная статья своей цели не достигла. Кейс, который был выбран, ни в коей мере не показывает зачем использовать подобную связку инструментов. После прочтения возникает ощущение нецелесообразности происходящего и несоответствия решения задаче. Считаю, что автору оригинальной статьи следовало найти более подходящий кейс, где подобная связка имела бы смысл. Уверен, что такой кейс существует.

      Целью моей статьи также не является максимально простое решение задачи. Простота и лаконичность — это скорее побочный эффект использования простых подходов и технологий, таких как Svelte. Если же изначально выбирать сложные инструменты, такие как RxJS в данном случае, то ожидать простого решения не приходится.

      0
      Спасибо за обзор. Фреймворк похож на vue.js. Можете подсказать отличия и преимущества, если знакомы с vue?
        0
        Фреймворк похож на vue.js.

        Визуально да, похож. Хотя смотря с какой стороны посмотреть. Скорее уж Vue похож на Svelte. По крайней мере те подходы, которые перекочевали с Vue 1.0. Сейчас там полно из React и даже Angular.

        Можете подсказать отличия и преимущества, если знакомы с vue?

        Конечно знаком, пишу на нем с 2015 года. Главное отличие в том, что Svelte — это «исчезающий» фреймворк, то есть фрейморк без рантайма. Кроме того это еще статический анализатор и компилятор в ванилу.

        Более подробно можете почитать об этом в других статьях про Svelte.

        0
        Тоже неплохо
          +2
          Просто отлично! Всегда раздражают статьи про реакт, куда все стараются напихать КУЧУ умных слов, зависимостей и концепций… Просто чтобы сделать вот также, как получилось у вас, только ТРУ-функционально и умно. Ваш подход мне нравится гораздо больше! :)
            0
            Спасибо.))) Разделяю ваше мнение.
            +1
            Что-то $mol остаёт. Раньше товарищ Дмитрий любил реализовывать любые вещи на своём моле. Энтузиазм пропал, но равновесие не нарушено, так как теперь у нас есть Svelte!) Который, к слову, выглядит более адекватным)
              0
              Что-то $mol остаёт. Раньше товарищ Дмитрий любил реализовывать любые вещи на своём моле.

              Да, я раньше даже специальные Disclaimer писал по этому поводу))))

              Энтузиазм пропал, но равновесие не нарушено, так как теперь у нас есть Svelte!) Который, к слову, выглядит более адекватным)

              Рад что вы это заметили) Вообще, равновесие еще долго будет нарушено, потому что кол-во статей о фреймворках из «Больной Тройки» нам еще долго не уравновесить))

              Но признаюсь честно, написал эту статью в том числе из корыстных побуждений… В следующей статье буду интегрировать этот Svelte компонент, аля виджет, в React приложение))) Давно обещаю ребятам в чате рассказать как это делается.
              +1

              Обалденно, я уже влюбился в простоту фреймворка, хотя почти ничего не знаю о нём. Но за статью спасибо, подход просто эпичный по своей простоте и одновременному решению всех проблем.

                0
                Спасибо, рад что вам понравилось. Постараюсь рассказывать о нем дальше, по возможности.
                0

                Да, а весь юмор статьи в том, что использовалась непонятная хрень в которой никто не будет разбираться. Был бы ванильный JS, то тогда было бы другое дело.

                  0
                  5 лет назад реакт был «непонятной хренью в которой никто не будет разбираться». А числа предлагалось складывать с помощью специального jQuery-плагина )))))

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

                  А вообще во все времена были и будут люди, которые не хотят разбираться с чем-то новым. Однако прогресс это не остановило, как видите)))

                  0
                  А как решается проблема «накладывания» промисов? Имею ввиду гипотетический сценарий, когда(даже с учетом дебаунса) отправляется несколько запросов с небольшим интервалом, и более ранний завершается позже, таким образом показываются устаревшие данные.
                    0
                    Решается поиском в npm по ключевым словам «debounce promise». )))))
                      –1
                      Ок, объясню подробнее.
                      Юзер начинает что-то вводить, через 100мс идет первый запрос, он продолжает ввод, через 200 мс сработает второй запрос. Возможна ситуация, что первый запрос закончится позже второго и будут показаны устаревшие данные.
                      См. диаграмму потоков:
                      time(ms): 0---------100-------200-------300-------400-------500-------
                      user
                      input:    "q"--"qu"--"qua"--"quan"--"quant"---------------------------
                      promise:  -----------P---------------P--------------------------------
                                           ("qua")         ("quant")
                                           \               \
                                            \               \_GET____
                                             \                       \
                      request:                \________GET____________\___________
                                                                       \          \
                      resolve:  ---------------------------------------"quant"------"qua"---
                      

                      Код, симулирующий такую ситуацию: jsfiddle.net/mikolalex/fvau6s51/6
                      Ситуация, кстати, не гипотетическая, а очень часто встречающаяся на практике(медленный интернет), сталкивался на многих сайтах, например на Яндекс.Расписаниях(«запаздывающий» автокомплит названия станции).
                        0
                        Мне показалось или вы на полном серьезе объясняете мне что такое «гонки»? )))) Право, не стоит, вы слишком любезны. ))))

                        По существу вопроса, при используемом подходе «гонок» быть не может по определению, потому что вне зависимости от последовательности завершения асинхронных вызовов, стартуют они всегда в строгом порядке и синхронно. Промис созданный на последний введенный пользователем символ всегда будет последним промисом в серии вызовов. Все остальные промисы и их результаты просто отбрасываются, вне зависимости от того, в какой последовательности они завершились:

                        svelte.technology/repl?version=2.9.11&gist=85e212d7065e1d1c076ab2d6f341bf9b
                          0
                          Ну вы же с первого раза не поняли вопроса, вот пришлось рассусоливать)
                          Спасибо, ответ исчерпывающий.
                            0
                            Да, не понял, потому что для этого есть специальный термин — «race condition» или на жаргоне «гонки». Это их частный случай. Ваш термин я не уловил, вероятно, прочитал недостаточно внимательно. Рад, что мы разобрались.
                          0
                          Достаточно запоминать последнее успешное завершение запроса в серии однотипных запросов, чтобы суметь проигнорировать «опоздавшие» результаты:
                          jsfiddle.net/fvau6s51/13

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

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