ReactHooksを使ってみる

この記事は[Nextremer Advent Calendar 2018](https://qiita.com/advent-calendar/2018/nextremer)の9日目の記事です。

弊社の対話システムである[minarai](https://www.minarai.io/)のフロントをReactHooksを使って作ってみました。

# はじめに
ReactHooksで提供されるAPIのうち
- useState
- useContext
- useMemo
の３つを使っています。
また各APIの詳細な説明は他にいくらでも良記事が転がっているのでこの記事では割愛します。

# 作ってみる
## 準備
### npmモジュールのインストール
いつもの
今回は楽をしたいのでモジュールバンドラーにparcelを使ってます。parcel超便利 :sparkles: 

```bash
$ mkdir react-hooks-minarai-sample && cd $_
$ npm init -y
$ npm i -S react@next react-dom@next
$ npm i -D parcel-bundler
```

minaraiを動かすために必要なものをインストール
minaraiではSocket.IOを使っているので、Socket．IOクライアントとminarai用のSDKをインストール

```bash
$ npm i -S socket.io-client minarai-client-sdk-js-socket-io
```

あとデザイン周り

```bash
$ npm i -S styled-components @material-ui/core
```

## minarai用のContextを作る
Socket.IOでminaraiの各種イベントを受信するためのContextを作ります。

まずはMinaraiContextを定義

```javascript:src/js/contexts/minarai.js
const MinaraiContext = createContext()
```

カスタムProviderを作成。
このContextでは、 送信メッセージ、受信メッセージ、送信メソッドの3つを提供します。

```javascript:src/js/contexts/minarai.js
const MinaraiContextProvider = ({ children }) => {
  const [outgoingMessage, setOutgoingMessage] = useState('')
  const [incomingMessage, setIncomingMessage] = useState('')
  const send = msg => {}
  return (
    <MinaraiContext.Provider value={{ outgoingMessage, incomingMessage, send }}>
      {children}
    </MinaraiContext.Provider>
  )
}
```

今回は単純に送信イベント、受信イベントのみをウォッチし、それぞれのメッセージ用のローカルステートを更新するようにしています。

```javascript:src/js/contexts/minarai.js
const MinaraiContextProvider = ({ children }) => {
  const [outgoingMessage, setOutgoingMessage] = useState('')
  const [incomingMessage, setIncomingMessage] = useState('')

  // minarai-clientの作成
  const cli = new MinaraiClient({/* 略 */})
  cli.on('sync', data => setOutgoingMessage(data.body.message))
  cli.on('message', data => setIncomingMessage(data.body.messages[0].utterances[0].text))
  cli.init()

  const send = msg => cli.send(msg)

  return (
    <MinaraiContext.Provider value={{ outgoingMessage, incomingMessage, send }}>
      {children}
    </MinaraiContext.Provider>
  )
}
```

これでコンテキストは完成。。。

と思っていたのですが、この実装だとProviderが呼び出されるたびにイベントがバインドされるので、送受信のたびにイベント発火回数が増えてしまいます。

MinaraiClientの生成、イベントバインドの処理を初回のみ行うように修正します。
ReactHooksの`useMemo`を使うと簡単に出来ますね :sparkles: 

```diff:src/js/contexts/minarai.js
  export const MinaraiContextProvider = ({ children }) => {
    const [outgoingMessage, setOutgoingMessage] = useState('')
    const [incomingMessage, setIncomingMessage] = useState('')

  // minarai-clientの作成
+   const minaraiClient = useMemo(() => {
      const cli = new MinaraiClient({/* 略 */})
      cli.on('sync', data => setOutgoingMessage(data.body.message))
      cli.on('message', data => setIncomingMessage(data.body.messages[0].utterances[0].text))
      cli.init()
+     return cli
+   }, [])

-   const send = message => cli.send(message)
+   const send = message => minaraiClient.send(message)

    return (
      <MinaraiContext.Provider value={{ outgoingMessage, incomingMessage, send }}>
        {children}
      </MinaraiContext.Provider>
    )
  }
```

`useMemo`の第二引数に空配列を渡すことで初回のみ動作させることが出来ます。
同様の処理は`useEffect`を使うことでも可能ですが、今回はクライアントインスタンスを返却したかったので`useMemo`を使用しています。

## minaraiContextを組み込む
まずはルートコンポーネントでContextProviderでラップしてやります。

```diff:src/js/components/App.js
+ import { MinaraiContextProvider } from '../contexts/minarai'

  return (
+   <MinaraiContextProvider>
      <Main>
        略
      </Main>
+   </MinaraiContextProvider>
  )
```

あとは使うコンポーネントで適宜`useContext`してやればOKです。

```diff:src/js/components/Form.js
+ import { MinaraiContext } from '../contexts/minarai'

  export default () => {
    const [text, setText] = useState('')
+   const { send } = useContext(MinaraiContext)

    return (
      <Wrapper>
        <Input value={text} onChange={e => setText(e.target.value)} />
-       <SendButton color="primary">
+       <SendButton color="primary" onClick={() => send(text)}>
          Send
        </SendButton>
      </Wrapper>
    )
  }
```

```diff:src/js/components/Messages.js
+ import { MinaraiContext } from '../contexts/minarai'

  export default () => {
+   const { outgoingMessage, incomingMessage } = useContext(MinaraiContext)
    return (
      <Wrapper>
        <MessageBalloon align="left">
-         <OutgoingMessage>test</OutgoingMessage>
+         <OutgoingMessage>{outgoingMessage}</OutgoingMessage>
        </MessageBalloon>
        <MessageBalloon align="right">
-         <IncomingMessage>test</IncomingMessage>
+         <IncomingMessage>{incomingMessage}</IncomingMessage>
        </MessageBalloon>
      </Wrapper>
    )
  }
```

作ったものはこちらにあります。
https://github.com/k-tada/react-hooks-minarai-sample