Help us understand the problem. What is going on with this article?

rxjs-hooksで楽しむReact HooksとRxJS

More than 1 year has passed since last update.

16.8で正式にリリースされるReact Hooks。

rxjs-hooksを使うとHooksでRxJS使えていい感じだったのでまとめたい

1. 検索機能を実装する

以前にredux-observableでやったことのあった検索機能をhooksでやってみる。

前提

ひとまず前回と同じくAPIとコンポーネントをざっくり作る

// api.ts

import fetchJsonp from "fetch-jsonp"
import qs from "querystring"

export const searchApi = (word) => {
  const baseURL = "https://ja.wikipedia.org/w/api.php"
  const params = {
    action: "opensearch",
    format: "json",
    search: word
  }
  const url = `${baseURL}?${qs.stringify(params)}`
  return fetchJsonp(url)
    .then((response) => response.json())
    .then((json) => json[1])
}
const Search = ({ word, changeInput }) => (
  <div>
    <input value={word} onChange={(e) => changeInput(e.target.value)} />
  </div>
)

const Result = ({ result }) => (
  <div>
    <h3>Result</h3>
    <ul>
      {result.map((r, i) => (
        <li key={i}>{r}</li>
      ))}
    </ul>
  </div>
)

ここから検索につながる部分を作っていく

パターンA: useStateuseObservableを使うパターン

おそらくこっちが適切であろうパターン。

import React, { useState } from "react"
import { useObservable } from "rxjs-hooks"
import { map, bufferTime, distinctUntilChanged, switchMap } from "rxjs/operators"

const useSearch = (word) =>
  useObservable(
    (word$) => // 渡されるwordのstream
      word$.pipe(
        debounceTime(400),
        distinctUntilChanged(),
        switchMap((word) => searchApi(word)),
        map((result) => (result !== undefined ? result : []))
      ),
    [], // 結果の初期値
    [word] // input。配列にして渡す必要があることに注意
  )

export const App = () => {
  // word自体はuseStateで取り出す
  const [word, setWord] = useState("")
  // これを独自実装したuseSearchへ渡す
  const result = useSearch(word)
  return (
    <div>
      <Search changeInput={setWord} word={word} />
      <Result result={result} />
    </div>
  )
}

この場合は検索は責務を絞ることができるので、下記のように分離することもできる

const SearchAndResult = (word) => {
  let result = useSearch(word)
  return <Result result={result} />
}

export const App2 = () => {
  const [word, setWord] = useState("")
  return (
    <div>
      <Search changeInput={setWord} word={word} />
      <SearchAndResult word={word} />
    </div>
  )
}

パターンB: useEventCallbackを使うパターン(イマイチ)

もう一つやり方があるので記述する。「どうしても全部RxJSに任せたいんだ!」という場合は良いかもしれないがおそらくこの場合においてはデメリットのほうが大きいだろう

import React from "react"
import { useEventCallback } from "rxjs-hooks"
import { map, startWith, distinctUntilChanged, debounceTime, switchMap } from "rxjs/operators"

export const App = () => {
  const initial: [string, string[]] = ["", []]
  const [keyboardCallack, [word, result]] = useEventCallback(   (event$) =>
      combineLatest(
        event$, // 入力を即時反映させるために、素のstremを流す必要がある
        event$.pipe(
          startWith([]), // 初期値をstatWithで指定
          debounceTime(400),
          distinctUntilChanged(),
          switchMap((word) => searchApi(word)),
          map((result) => result || [])
        )
      ),
    initial
  )
  return (
    <div>
      <Search changeInput={keyboardCallack} word={word} />
      <Result result={result} />
    </div>
  )
}

useEventCallbackは結果の1つ目がコールバック、二つ目がStreamの結果が返ってくる。

ただSearchのinputを入力に正しく同期するために、注1のように入力をそのまま流してcombineLatestを使ったり、startWithを使って入力の度にstreamが来るようにする工夫が必要になる。

また、入力のコールバックに強く依存してしまう部分も難点だろう。

2. useEventCallbackで秒間クリック数を取る

上記はinputに対する入力だったためuseObservableのほうが都合が良かったが、クリックなどマウスイベント関連なら有効になる。

例示として秒間クリックを取得して出すようなことを考えてみるとこんな具合になりそうだ(そんなユースケースがあるかどうかは不明)

import { useEventCallback } from "rxjs-hooks"
import { map, bufferTime, distinctUntilChanged, tap } from "rxjs/operators"

const Button = ({ updateCount }) => {
  const [eventCallback, result] = useEventCallback(
    (event$) =>
      event$.pipe(
        bufferTime(1000), // 1秒間bufferする
        map((v) => v.length), // 受け取ったイベント=クリック数
        distinctUntilChanged()
        tap((result) => updateCount(result))
      ),
    0
  )
  return (
    <div>
      <button onClick={eventCallback}>Click!</button>
    </div>
  )
}

export const App = () => {
  // 状態自体はstateに収める
  const [count, setCount] = useState(0)
  return (
    <div>
      <Button updateCount={setCount} />
      <div>{count} click / second</div>
    </div>
  )
}

3.(おまけ) ↑↑↓↓←→←→BAを検出する

有名なコマンドが入力されたら発火するようなイースターエッグを仕込みたいときに使えるかもしれない。

import { useObservable } from "rxjs-hooks"
import { map, filter, scan } from "rxjs/operators"
import { fromEvent, pipe } from "rxjs"

const arrowMap = {
  ArrowUp: "",
  ArrowDown: "",
  ArrowLeft: "",
  ArrowRight: ""
}

const convertVisible = (key) => {
  if (arrowMap[key]) return arrowMap[key]
  if (key.length === 1) return key.toUpperCase()
  return null
}

// keydownイベントのストリームを配列化
export const useKeyCommnads = (length) =>
  useObservable(
    () =>
      fromEvent(document, "keydown").pipe(
        pluck("key"),
        map(convertVisible),
        filter((value) => !!value),
        scan<string>((curr, next) => [...curr, next], []),
        map((cmd: string[]) => cmd.slice(-1 * length))
      ),
    []
  )

// 流れてきたコマンドが任意のものかどうかを検証する
export const useTargetCommand = (targetCommand) => {
  const keyeventLog = useKeyCommnads(targetCommand.length)
  return {
    keyeventLog,
    konami: targetCommand.join("") === keyeventLog.join("")
  }
}

export const App = () => {
  const command = ["", "", "", "", "", "", "", "", "B", "A"]

  const { keyeventLog, succeed } = useTargetCommand(command)
  return (
    <div>
      <div>please key type: {keyeventLog}</div>
      <div>{succeed ? <Success /> : null}</div>
    </div>
  )
}

斜め入力まで考慮したい場合のサンプルは下記においたので興味があればご参照を
https://codesandbox.io/s/mzxx7wy318?module=%2Fsrc%2Findex.tsx

他のRxJS + Hooks手法

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした