Как создать функцию поиска GitHub в React с помощью RxJS 6 и Recompose

1656565699 kak sozdat funkcziyu poiska github v react s pomoshhyu

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

Вот что мы строим:

1*KeoXx3EaGVrHXaZzK_QBzA

Нет классов, крючков жизненного цикла или setState.

Настройка

Все на моем GitHub.

git clone 
cd recompose-github-ui
yarn install

The master филиал имеет готовый проект, поэтому проверьте start отделение, если вы хотите следовать.

git checkout start

И запустить проект.

npm start

Приложение должно работать localhost:3000и вот наш исходный пользовательский интерфейс.

1*_XoqdpqQdmYrXs3q6_063w

Откройте проект в своем любимом текстовом редакторе и просмотрите src/index.js.

1*iQy1zXOnGQIIb5noAzYvfw

Перекомпонуйте

Если вы еще не видели его, Recompose – это отличная утилита React для создания компонентов в функциональном стиле программирования. Он имеет множество функций, и мне было бы трудно выбрать свои любимые.

Это Lodash/Ramda, но для React. Мне тоже нравится, что они поддерживают наблюдение. Цитата из документов:

Оказалось, что значительная часть компонентов API React может быть выражена в терминах наблюдаемых

Мы будем реализовать эту концепцию сегодня! ?

Трансляция нашего компонента

Прямо сейчас App является обычным компонентом React. Мы можем вернуть его через наблюдаемый с помощью функции Recompose componentFromStream.

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

Панель конфигурации

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

Однако, пока они не будут полностью реализованы, мы полагаемся на такие библиотеки как RxJS, xstream, most, Flyd и т.д.

Recompose не знает, какую библиотеку мы используем, поэтому он предоставляет a setObservableConfig чтобы превратить ES Observables в/из того, что нам нужно.

Создайте новый файл в src звонил observableConfig.js.

И добавьте этот код, чтобы сделать Recompose совместимым с RxJS 6:

import { from } from 'rxjs';
import { setObservableConfig } from 'recompose';

setObservableConfig({
  fromESObservable: from
});

Импортируйте его в index.js:

import './observableConfig';

И мы готовы!

Перекомпоновать + RxJS

Импорт componentFromStream.

import React from 'react';
import ReactDOM from 'react-dom';
import { componentFromStream } from 'recompose';
import './styles.css';
import './observableConfig';

И начать переопределение App с этим кодом:

const App = componentFromStream((prop$) => {
  // ...
});

Обратите внимание на это componentFromStream принимает функцию обратного вызова, ожидая a prop$ поток. Идея в том, что наша props становятся объектом наблюдения, и мы отображаем его в компонент React.

И если вы использовали RxJS, вы знаете идеальный оператор для этого карта ценности.

Карта

Как видно из названия, вы превращаетесь Observable(something) в Observable(somethingElse). в нашем случае Observable(props) в Observable(component).

Импортируйте map оператор:

import { map } from 'rxjs/operators';

И переопределите приложение:

const App = componentFromStream((prop$) => {
  return prop$.pipe(
    map(() => (
      <div>
        <input placeholder="GitHub username" />
      </div>
    ))
  );
});

Начиная с RxJS 5, мы используем pipe вместо операторов цепочки.

Сохраните и проверьте свой пользовательский интерфейс, результат тот же!

1*Edm3g3VL9121uIgwkzRiSA

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

Сейчас мы сделаем свое input немного более реактивный.

Импортируйте createEventHandler от Recompose.

import { componentFromStream, createEventHandler } from 'recompose';

И используйте его так:

const App = componentFromStream((prop$) => {
  const { handler, stream } = createEventHandler();

  return prop$.pipe(
    map(() => (
      <div>
        <input onChange={handler} placeholder="GitHub username" />{' '}
      </div>
    ))
  );
});

createEventHandler это объект с двумя интересными свойствами: handler и stream.

Под капотом, handler является источником событий, присылающим значение к streamкоторый доступен для наблюдения, который передает эти значения своим подписчикам.

Поэтому мы объединим stream наблюдаемый и prop$ можно наблюдать для доступа к inputтекущее значение.

combineLatest здесь хороший выбор.

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

Использовать combineLatestоднако, оба stream и prop$ должны излучаться. stream не будет излучать пока prop$ излучает, и наоборот.

Мы можем это исправить, дав stream начальное значение.

Импорт RxJS startWith оператор:

import { map, startWith } from 'rxjs/operators';

И создайте новую переменную для увлечения измененной stream.

const { handler, stream } = createEventHandler();

const value$ = stream.pipe(
  map((e) => e.target.value),
  startWith('')
);

Мы это знаем stream будет выдавать события из input‘s onChange, поэтому давайте немедленно сопоставим каждый из них event к его текстовому значению.

Кроме того, мы проведем инициализацию value$ как пустая строка — соответствующее значение по умолчанию для пустой строки input.

Сочетая все это

Мы готовы объединить эти два потока и импортировать combineLatest как метод создания, не как оператор.

import { combineLatest } from 'rxjs';

Вы также можете импортировать tap оператор для проверки значений по мере их поступления:

import { map, startWith, tap } from 'rxjs/operators';

И используйте его так:

const App = componentFromStream((prop$) => {
  const { handler, stream } = createEventHandler();
  const value$ = stream.pipe(
    map((e) => e.target.value),
    startWith('')
  );

  return combineLatest(prop$, value$).pipe(
    tap(console.warn),
    map(() => (
      <div>
        <input onChange={handler} placeholder="GitHub username" />
      </div>
    ))
  );
});

Теперь, когда вы печатаете, [props, value] регистрируется.

1*E1jAWy0UTDbWFfEh___Psg

Компонент пользователя

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

JSX/CSS

Все это основано на этом замечательном проекте GitHub Cards. Большинство вещей, особенно стилей, копируется/вставляется или перерабатывается, чтобы соответствовать React и реквизиту.

Создайте папку src/Userи вставьте этот код в User.css:

И этот код в src/User/Component.js:

Компонент просто заполняет шаблон стандартным ответом JSON GitHub API.

Контейнер

Теперь, когда «тупой» компонент исчез, давайте сделаем «умный» компонент:

Вот src/User/index.js:

import React from 'react';
import { componentFromStream } from 'recompose';
import { debounceTime, filter, map, pluck } from 'rxjs/operators';
import Component from './Component';
import './User.css';

const User = componentFromStream((prop$) => {
  const getUser$ = prop$.pipe(
    debounceTime(1000),
    pluck('user'),
    filter((user) => user && user.length),
    map((user) => <h3>{user}</h3>)
  );

  return getUser$;
});

export default User;

Мы определяем User как componentFromStreamвозвращающий a prop$ поток, отображаемый на <h3>.

debounceTime

Так как User будет получать свой реквизит через клавиатуру; мы не хотим слушать каждое излучение.

Когда пользователь начинает вводить текст, debounceTime(1000) пропускает все выбросы на 1 секунду. Этот шаблон обычно используется для предупреждения ввода.

сорвать

Этот компонент ожидает prop.user в некоторой точке. pluck захватывает userпоэтому нам не нужно деструктурировать наш props каждый раз.

фильтр

Обеспечивает это user существует и не является пустой строчкой.

карта

Пока просто поставьте user внутри а <h3> тег.

Подключение

Вернуться в src/index.jsимпортировать User компонент:

import User from './User';

И предоставить value как user опора:

return combineLatest(prop$, value$).pipe(
  tap(console.warn),
  map(([props, value]) => (
    <div>
      <input onChange={handler} placeholder="GitHub username" />
      <User user={value} />{' '}
    </div>
  ))
);

Теперь ваше значение отображается на экране через 1 секунду.

1*ti-OF_cqiKmQx1iTZZJFrA

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

Получение пользователя

API пользователя GitHub доступен здесь. Мы можем легко извлечь это во вспомогательную функцию внутри User/index.js:

const formatUrl = (user) => `

Теперь мы можем добавить map(formatUrl) после filter:

1*bdCfDgYzFP9laQAg9Y1AKw

Вы заметите, что конечная точка API отображается на экране через 1 секунду:

1*5ZTeqmDCGjnwe-MIP0H83g

Но нам нужно сделать запрос API! Вот приближается switchMap и ajax.

switchMap

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

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

Что произойдет, если пользователь введет что-нибудь новое, прежде чем результат вернется? Волнует ли нас предыдущий ответ API?

Нет.

switchMap отменит предварительную выборку и сосредоточится на текущей.

ajax

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

Использование их

Давайте импортируем оба. Мой код выглядит так:

import { ajax } from 'rxjs/ajax';
import { debounceTime, filter, map, pluck, switchMap } from 'rxjs/operators';

И используйте их так:

const User = componentFromStream((prop$) => {
  const getUser$ = prop$.pipe(
    debounceTime(1000),
    pluck('user'),
    filter((user) => user && user.length),
    map(formatUrl),
    switchMap((url) =>
      ajax(url).pipe(
        pluck('response'),
        map(Component)
      )
    )
  );

  return getUser$;
});

Переключатель от нашего input поток к an ajax поток запросов. После завершения запроса возьмите его response и map к нашим User компонент.

Есть результат!

1*NIVF7Iq9bjqremAKS2VOYQ

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

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

1*cvF0zqPlndM4VAjyGHgxsQ

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

Это плохой опыт пользователя, не правда ли?

catchError

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

Импортируйте его:

import {
  catchError,
  debounceTime,
  filter,
  map,
  pluck,
  switchMap
} from 'rxjs/operators';

И приклеить его к концу ajax цепь.

switchMap((url) =>
  ajax(url).pipe(
    pluck('response'),
    map(Component),
    catchError(({ response }) => alert(response.message))
  )
);

1*krBPGwW4tSv7FOxGaleZxQ

По крайней мере, мы получаем отзывы, но мы можем сделать лучше.

Компонент ошибки

Создайте новый компонент, src/Error/index.js.

import React from 'react';

const Error = ({ response, status }) => (
  <div className="error">
    <h2>Oops!</h2>
    <b>
      {status}: {response.message}
    </b>
    <p>Please try searching again.</p>
  </div>
);

export default Error;

Это будет хорошо отображаться response и status из нашего звонка AJAX.

Давайте импортируем его User/index.js:

import Error from '../Error';

И of из RxJS:

import { of } from 'rxjs';

Помните, наши componentFromStream обратный вызов должен возвращать наблюдаемый. Мы можем достичь этого с помощью of.

Вот новый код:

ajax(url).pipe(
  pluck('response'),
  map(Component),
  catchError((error) => of(<Error {...error} />))
);

Просто распространяйте error объекта как реквизита в нашем компоненте.

Теперь, если мы проверим наш пользовательский интерфейс:

1*OA8An4fuwA5CK4-ogDRwYw

Намного лучше!

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

Обычно нам сейчас нужна какая-нибудь форма государственного управления. Как построить индикатор нагрузки?

Но прежде чем потянуться за setStateдавайте посмотрим, может ли RxJS нам помочь.

Документы Recompose побуждали меня задуматься в этом направлении:

Вместо этого setState()объединить несколько потоков вместе.

Редактировать: Сначала использовал BehaviorSubjects, но Матти Ланкинен ответил блестящим способом упростить этот код. Спасибо, Матти!

Импортируйте merge оператор.

import { merge, of } from 'rxjs';

Когда запрос будет сделан, мы объединим наши ajax с потоком загрузочных компонентов

Внутри componentFromStream:

const User = componentFromStream((prop$) => {
  const loading$ = of(<h3>Loading...</h3>);
  // ...
});

Простой h3 индикатор загрузки превратился в наблюдаемый! И используйте его так:

const loading$ = of(<h3>Loading...</h3>);

const getUser$ = prop$.pipe(
  debounceTime(1000),
  pluck('user'),
  filter((user) => user && user.length),
  map(formatUrl),
  switchMap((url) =>
    merge(
      loading$,
      ajax(url).pipe(
        pluck('response'),
        map(Component),
        catchError((error) => of(<Error {...error} />))
      )
    )
  )
);

Мне нравится, как это кратко. При входе switchMapобъединить loading$ и ajax наблюдаемые.

Так как loading$ является статическим значением, оно будет казаться первым. Раз асинхронный ajax завершает, однако, это будет излучать и отображать на экране.

Прежде чем испытать его, мы можем импортировать delay оператора, чтобы переход не происходил слишком быстро.

import {
  catchError,
  debounceTime,
  delay,
  filter,
  map,
  pluck,
  switchMap,
  tap
} from 'rxjs/operators';

И используйте его непосредственно перед тем map(Component):

ajax(url).pipe(
  pluck('response'),
  delay(1500),
  map(Component),
  catchError((error) => of(<Error {...error} />))
);

Наш результат?

1*9ZPxZaVZt5d5TVKbPKGT9w

Мне интересно, как далеко убрать этот шаблон и в каком направлении. Пожалуйста, поделитесь своим мнением!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *