Make it to make it

いろいろ作ってアウトプットするブログ

Promise再入門(2)

Promiseのthen()メソッドをコールすることは、既存のpromiseにハンドラをアサインするだけでなく新しいpromiseを作ることである。

const newPromise = promise.then(handleSuccess, handleRejection)

Promiseチェーン

successまたはerrorハンドラがpromiseを返すとき、then()メソッドはそのpromiseがresolveするまで待ち、渡された値を返却されたpromiseに対して使う。詳しくは以下の例を見れば一目瞭然。

const addOneByOne = data => (
  new Promise(resolve => {
    setTimeout(
      () => resolve(data + 1),
      Math.random() * 1000
    )
  })
)

addOneByOne(1)
  .then(result => {
    console.log('1st then handler:', result)  // 1st then handler: 2
    return addOneByOne(result)
  })
  .then(result => {
    console.log('2nd then handler:', result)  // 2nd then handler: 3
    return addOneByOne(result)
  })
  .then(result => {
    console.log('3rd then handler:', result)  // 3rd then handler: 4
  })

setTimeoutで実行タイミングをズラしたとしても、順番通りに処理が行われていることがわかる。

これは、thenオブジェクトでreturnを行わなくても同じ結果になる。

addOneByOne(1)
  .then(result => {
    console.log('1st then handler:', result)  // 1st then handler: 2
  })
  .then(result => {
    console.log('2nd then handler')  // 2nd then handler
  })
  .then(result => {
    console.log('3rd then handler')  // 3rd then handler
  })

CallbackをPromiseにリファクタする例

作った

github.com

Promise再入門(1)

モダンなJavaScriptでは、非同期処理においてpromiseとasync/awaitが主流となっている。

  • Webpackはコードスプリッティングをpromiseによって簡便化している
  • ブラウザにはビルトインファンクションであるfetchが存在し、resultをawaitできる
  • ReactではReact.Suspenseをpromiseで実装している

ここでは従来のcallbackベースのファンクションからpromise, async/awaitまでをサンプルを通してカバーする。

Promiseとは何か?

callbackはファンクション、promiseはオブジェクト

callbackはあるイベントに対してレスポンスするかたちで実行されるファンクション。どんなファンクションでもcallbackになることができ、どんなcallbackでもファンクションになることができる。

promiseはオブジェクトであり、あるイベントがまだ起こっていない状態(pending)を表現する。あるイベントが起きたかどうかに関する情報、起きた場合の出力の情報を保持している。

callbackは引数として渡され、promiseは返却される

callbackは独立して定義され、あるファンクションに引数として渡され、必要なタイミングまで保持され、関連するイベントが起きたタイミングで実行される。

promiseは非同期の処理を開始させるファンクションによって作られて返却される。関連するイベントが起きたとき、その処理は実行結果をpromiseに格納し、promise側でsuccessまたはfailureハンドラを実行できる。

callbackはsuccessとfailureを扱い、promiseは何も扱わない

callbackはある処理が成功したかどうかの情報とともに使用され、成功時と失敗時のハンドラを用意しておかないといけない。

promiseはデフォルトでは何も扱わないが、successまたはfailureハンドラは後々にアタッチされる。

callbackは複数のイベント扱うことができるが、promiseは最大1つ

callbackはそれらを受け取るファンクションによって複数回実行することができる

promiseは一つのイベントだけを表現することができ、それは成功時または失敗時のものである

CallbackをPromiseで書き換える一例

ログを一定時間ごとに出力するサンプルを下記に示す。

Callback

const log = (message = '') => {
  console.log(message)
}

log('When the raining is ')
setTimeout(() => {
  log('blowing ')
  setTimeout(() => {
    log('in your face,')
  }, 1000)
}, 2000)

Promise

const log = (message = '') => {
  console.log(message)
}

// delay promise
const delay = ms => {
  return new Promise(resolve => {
    setTimeout(resolve, ms)
  })
}

async function logMessage() {
  log('When the raining is ')
  await delay(2000)
  log('blowing ')
  await delay(1000)
  log('in your face,')
}

logMessage()

Thenメソッド

Promiseはまだ起きていないイベントの処理結果を表すものである。 処理結果の取得可能時にcallbackファンクションを自分で登録して、promiseのthenメソッドで渡す必要がある。

// Call the first argument once the promise has successfully resolved
promise.then(value => {
    // ...
});

// Call the second argument if the promise is rejected
promise.then(null, error => {
    // ...
});

// You can handle both cases by passing in two functions
promise.then(
    value => { /* ... */ },
    error => { /* ... */ }
);

Rejectionハンドラ

滅多にないと思うが、もしsuccessハンドラでエラーが起きた場合、当然ながらrejectionハンドラには進まない。

ただし、catchを書いている場合はそちらの処理が実行される。

ここは以外な盲点なので、下記にサンプルを含めて載せておく。

const samplePromise = new Promise((resolve, reject) => resolve('Success!'))

samplePromise.then(
  success => {
    console.log('success handler', success)  // これが出力される
    throw new Error('here is an error!')  // これが出力される
  },
  error => {
    console.log('rejection handler', error)
  }
)
const samplePromise = new Promise((resolve, reject) => resolve('Success!'))

samplePromise.then(
  success => {
    console.log('success handler', success)  // これが出力される
    throw new Error('here is an error!')
  },
  error => {
    console.log('rejection handler', error)
  }
)
.catch(error => {
  console.log('catch handler', error)  // これが出力される
})

上記の例ではcatch内でエラーをログするようにしているが、そのようにエラーハンドリングしっかりとしてあげることでデバッグができることを念頭においておく。

const samplePromise = new Promise((resolve, reject) => reject('Failed!'))

samplePromise.then(
  success => {
    console.log('success handler', success)
    throw new Error('here is an error!')
  },
  error => {
    console.log('rejection handler', error)  // これが出力される
  }
)
.catch(error => {
  console.log('catch handler', error)
})

MongoDB起動時にエラーが発生したときの対処法

macOS 10.15 CatalinaにアップデートしてMongoDBを起動しようとしたときに直面したエラーの対処法。

エラー内容

MongoDB exit code 100

Read-only system volume in macOS Catalina

macOS Catalina runs in a read-only system volume, separate from other files on your Mac. When you upgrade to Catalina, a second volume is created, and some files may move to a Relocated Items folder.

macOS CatalinaではOSが専用のread-onlyボリュームで動き、OSシステムファイルを誤っていじってしまわないようにするという大きな変更点が入っている。

read-onlyシステムボリュームとデータボリュームに分けられ、ユーザーのデータやアプリケーションはデータボリュームの方に振り分けられる。

確認方法

Disk Utilityを開くと、Macintosh HDとMacintosh HD - Dataに分けられていることが確認できる。

対処方法

MongoDBの場合、デフォルトのMongoDB用フォルダは/data/dbとなっているので、当然ながら書き込みはできない。

そこで、MongoDBのデータ用のフォルダをホーム下に変更して対処した。

mongod --dbpath ~/data/db

Reduxのことをわかりやすく

Reduxイントロ

Reduxとはアプリケーションの状態(state)を管理するためのJSライブラリ。 ではアプリケーションの状態とは具体的に何かというと、いろんな目的で後に使用する情報を保持しておくためのグローバルオブジェクト。

小さな例ではページロード時のローディング表示の管理から、大きな例ではSNSアプリでのユーザー情報や投稿などなど。 何を保持するかは自由だが、格納できるデータはシリアライズできるものでなければいけない。

It is highly recommended that you only put plain serializable objects, arrays, and primitives into your store. It's technically possible to insert non-serializable items into the store, but doing so can break the ability to persist and rehydrate the contents of a store, as well as interfere with time-travel debugging.

Reduxが従うルール

Single source of truth

データを保持するための場所(store)は一箇所のみである。

one app — one store — one state

もちろんコンポーネントごとの各々のstateは存在し、全ての状態をこのStoreで管理する必要はない。

Immutability

stateオブジェクトとそのプロパティを直接修正してはいけない。 その代わり、新しいオブジェクトを作り、再度新しいアプリケーションの状態を計算し、その新しいオブジェクトで上書きして再計算する。 つまり、アップデート前の古い状態オブジェクトには一切の変更が加わらないようにする。

Reduxの3つのビルディングブロック

Store

Storeはアプリケーションの状態(state)を格納しておくところ。 Storeはクラスではなくオブジェクトである。 アプリケーションのstateだけでなく、ファンクションやその他のオブジェクトも含んでいる。

Storeのアップデートに対してイベントをlistenすることができる(subscription)。

Storeはオブジェクトで別名state treeと言及される。どんな深さにもネストすることができる。

Actions

ActionsはプレーンなJSオブジェクトであり、何(What)が起きたのかを表現するが、どのように(How)stateが変わったのかは表現しない。 Storeをアップデートする際に、Storeインスタンスにdispatch (send) するものである。その他はReducerによって扱われる。

Actionオブジェクトではtypeフィールドが必須のものであり、これによってどんなActionがdispatchされるのかが表現され、typeにはexportされた定数が入る。 type以外のフィールドはオプショナルで自由に設定してよい。

Actionの一例

{ type: ADD_NOTE, title: 'Some Title', content: 'This is an action object' }

Action Creators

その他に頻出するのがAction creatorsであり、これはプレーンなJSオブジェクトを返すファンクションである。 Action creatorsはactionにダイナミックデータを挿入するために使われる。

Action creatorsの一例

function addNote(title, content) {
  return { type: ADD_NOTE, title: title, content: content };
}

Reducers

ActionsがWhatを担うならば、ReducersはHowを担う部分である。 Reducersはpure functionsであり、アプリケーションの新しいstateを再計算をする役割を持つ。

ActionsをStoreにdispatchする際に、ActionsはReducersに渡される。 Reducerは2つの引数を受け取る。一つは前のアプリケーションのstateであり、もう一つはdispatchされたactionである。そして新しいstateを返す。

(previousState, action) => newState

つまり、Reducerはdispatchされたactionのタイプに応じてアプリケーションの新しい状態を再計算する。

実際のアプリケーションではReducerは複雑化することになる。 このためによりシンプルなReducerに細かく分けてから、後にReducerのヘルパーファンクションであるcombineReducersで合体させる。 メインのReducerはRoot Reducerと呼ばれる。

ReduxでのData flow

f:id:sungjoon512:20190911232249p:plain

ユーザーによってあるイベントがトリガーされてアプリケーションのstateがアップデートされたとする。 そのプロセスでは以下のようなことが起きている。

  1. ボタンのクリックハンドラファンクションがactionをstoreにdispatchする。store.dispatch()メソッド
  2. Reduxがdispatchされたactionをreducerに継承する
  3. storeがreducerによってreturnされた新しいstateを保存する
  4. ここまででstoreにsubscribeしたので、書いたファンクションがcallされ、それによってUIが都度アップデートする

Context APIによるデータの受け渡し

HOCとAnt Designとやってきたので、それらのコンセプトを頭に置きながらContext APIというものを学んでみる。 コンポーネントが増えてアプリケーションが大きくなればなるほど、stateやpropsの受け渡しは大変になる。 そんなときに使える手段の一つとしてContext APIというのが用意されている。VueでいうEvent busに似ているかもしれない。

Context API

いつ使うか

コンポーネントツリーのどこからでも所望のデータにアクセスしたいとき

活用例

  • アプリケーションのテーマを変えるとき
  • 言語設定などを変更するとき

使い方

React.createContext()

import React from 'react'

const ThemeContext = React.createContext()

export default ThemeContext

Provider

Contextを読み込んでから、Providerを通してvalueにデータをわたす。 このデータはstateに格納してわたすことでリアクティブにできる。

import React from 'react'
import ThemeContext from './Contexts'

function App () {
  return(
    <ThemeContext.Provider value={this.state}>
      (...snip)
    </ThemeContext.Provider>
  )
}

export default App

Consumer

Providerでvalueに渡されたデータをConsumerで受け取る。

import React from 'react'
import ThemeContext from './Contexts'

function Popovers () {
  return (
    <ThemeContext.Consumer>
      {state => (
        (...snip)
      )}
    </ThemeContext.Consumer>
  )
}

export default Popovers

Ant Design + Context API + Children Propsの活用例

Higher Order Components (HOC)

例えば、複数のコンポーネント間で同じファンクションやロジックを使用している場合、繰り返しが発生していることになりDRYではない。そんなときに使えるのがHigher Order Components (HOC)である。 実はVanillaJSにもHigher Order Functionsが存在していて、普段実装をするときに使っていたりする。

Higher Order Functions

引数にわたすファンクションがcallbackであり、callbackを含むファンクションをHigher Order Functionsと呼ぶ。

下記にTSで実例を示した。

const taxIncluded = (price: number, taxRate: number) => {
  return price * taxRate * 0.01
}

const higherOrderFunction = (x: number, callback: (arg1: number, arg2: number) => number) => {
  return callback(x, 10)
}

const getTaxValue = higherOrderFunction(4500, taxIncluded)

console.log(getTaxValue)  // 450

Higher Order Components (HOC)

ReactでのHOCの場合は引数にわたすのがReact componentであり、返却されるのは新しいReact componentであり、引数にわたしたReact componentに何かしらの機能を付け足して返却できる。

ボタンの種類がいくつか存在し、各ボタンクリック時にそのボタンのタイプをログするような機能をHOCで付与する例を示す。 ちなみに、実装の際はAnt Designを用いた。

App.jsx

import React from 'react'
import { ButtonPrimaryWithClick, ButtonDashedWithClick, ButtonDangerWithClick, ButtonLinkWithClick } from './components/Buttons'
import './App.css'

function App () {
  return (
    <div className='App'>
      <div className='container' style={{ paddingTop: '3rem' }}>
        <ButtonPrimaryWithClick typeValue='primary' />
        <ButtonDashedWithClick typeValue='dashed' />
        <ButtonDangerWithClick typeValue='danger' />
        <ButtonLinkWithClick typeValue='link' />
      </div>
    </div>
  )
}

export default App

WithClickHoc.jsx

import React from 'react'

function withClick (Component, propKey, propName) {
  return class WithClick extends React.Component {
    constructor (props) {
      super(props)
      this.state = {
        [propName]: this.props[propName]
      }
    }

    handleClick = () => {
      console.log(`You clicked ${propKey} ${this.state[propName]}!`)
    }

    render () {
      const props = {
        [propName]: this.state[propName]
      }

      return (
        <div onClick={this.handleClick}>
          <Component {...props} />
        </div>
      )
    }
  }
}

export default withClick

Buttons.jsx

import React from 'react'
import PropTypes from 'prop-types'

import { Button } from 'antd'
import withClick from './WithClickHoc'

const ButtonPrimary = ({ typeValue }) => <Button type='primary'>Primary</Button>
const ButtonDashed = ({ typeValue }) => <Button type='dashed'>Dashed</Button>
const ButtonDanger = ({ typeValue }) => <Button type='danger'>Danger</Button>
const ButtonLink = ({ typeValue }) => <Button type='link'>Link</Button>

ButtonPrimary.propTypes = {
  typeValue: PropTypes.string
}
ButtonDashed.propTypes = {
  typeValue: PropTypes.string
}
ButtonDanger.propTypes = {
  typeValue: PropTypes.string
}
ButtonLink.propTypes = {
  typeValue: PropTypes.string
}

export const ButtonPrimaryWithClick = withClick(ButtonPrimary, 'button', 'typeValue')
export const ButtonDashedWithClick = withClick(ButtonDashed, 'button', 'typeValue')
export const ButtonDangerWithClick = withClick(ButtonDanger, 'button', 'typeValue')
export const ButtonLinkWithClick = withClick(ButtonLink, 'button', 'typeValue')