React Routerは何をしているのか？ ~history編~

## history
* React Routerのroutingに関するUtil Library
* [公式サイト](https://github.com/mjackson/history)
* React Router内のhisotryはこのLibraryのこと

## listen

```js:createBrowserHistory
const createBrowserHistory = (options = {}) => {
  ...,

  const listen = (listener) =>
    startListener(listener, false)
}
```

listenerをstartListenerに渡しているだけのようです。
startListenerを見に行きます。

```js:startListener
  const startListener = (listener, before) => {
    if (++listenerCount === 1)
      stopListener = BrowserProtocol.startListener(
        history.transitionTo
      )

    const unlisten = before
      ? history.listenBefore(listener)
      : history.listen(listener)

    return () => {
      unlisten()

      if (--listenerCount === 0)
        stopListener()
    }
  }
```

初回は`++listenerCount === 1`なのでstopListenerが作成されます。
まずは、BrowserProtocolを見てみます。

```js:BrowserProtocol
export const startListener = (listener) => {
  const handlePopState = (event) => {
    if (event.state !== undefined) // Ignore extraneous popstate events in WebKit
      listener(_createLocation(event.state))
  }

  addEventListener(window, PopStateEvent, handlePopState)

  return () =>
    removeEventListener(window, PopStateEvent, handlePopState)
}
```

htmlの`popstate`イベントにlistenerを登録しています。
まずは、listenerとして渡された、history.transitionToを見てみます。
その前に、historyとは何なのかを確認します。

```js:history
  const Protocol = useRefresh ? RefreshProtocol : BrowserProtocol

  const {
    getUserConfirmation,
    getCurrentLocation,
    pushLocation,
    replaceLocation,
    go
  } = Protocol

  const history = createHistory({
    getUserConfirmation, // User may override in options
    ...options,
    getCurrentLocation,
    pushLocation,
    replaceLocation,
    go
  })
```

ProtocolはBrowserProtocolと考えて進めます。
BrowserProtocolで定義されているmethodを取得後に、createHistoryしています。

```js:createHistory
const createHistory = (options = {}) => {
  const {
    getCurrentLocation,
    getUserConfirmation,
    pushLocation,
    replaceLocation,
    go,
    keyLength
  } = options

  ...,

  return {
    getCurrentLocation,
    listenBefore,
    listen,
    transitionTo,
    push,
    replace,
    go,
    goBack,
    goForward,
    createKey,
    createPath,
    createHref,
    createLocation
  }
}
```

定義したmethodをまとめたObjectのようです。
transitionToがあるのでこれを確認します。

```js:transitionTo
  const transitionTo = (nextLocation) => {
    if (
      (currentLocation && locationsAreEqual(currentLocation, nextLocation)) ||
      (pendingLocation && locationsAreEqual(pendingLocation, nextLocation))
    )
      return // Nothing to do

    ...,

    confirmTransitionTo(nextLocation, (ok) => {
      if (pendingLocation !== nextLocation)
        return // Transition was interrupted during confirmation

      pendingLocation = null

      if (ok) {
        if (nextLocation.action === PUSH) {
          const prevPath = createPath(currentLocation)
          const nextPath = createPath(nextLocation)

          if (nextPath === prevPath && deepEqual(currentLocation.state, nextLocation.state))
            nextLocation.action = REPLACE

        if (nextLocation.action === POP) {
          updateLocation(nextLocation)
        } else if (nextLocation.action === PUSH) {
          if (pushLocation(nextLocation) !== false)
            updateLocation(nextLocation)
        } else if (nextLocation.action === REPLACE) {
          if (replaceLocation(nextLocation) !== false)
            updateLocation(nextLocation)
        }
      } else if (currentLocation && nextLocation.action === POP) {
        const prevIndex = allKeys.indexOf(currentLocation.key)
        const nextIndex = allKeys.indexOf(nextLocation.key)

        if (prevIndex !== -1 && nextIndex !== -1)
          go(prevIndex - nextIndex) // Restore the URL
      }
    })
  }
```

Locationのactionによって処理を変更しています。
処理内のconfirmTransitionToを見てみます。

```js:confirmTransitionTo
  const confirmTransitionTo = (location, callback) => {
    loopAsync(
      beforeListeners.length,
      (index, next, done) => {
        runTransitionHook(beforeListeners[index], location, (result) =>
          result != null ? done(result) : next()
        )
      },
      (message) => {
        if (getUserConfirmation && typeof message === 'string') {
          getUserConfirmation(message, (ok) => callback(ok !== false))
        } else {
          callback(message !== false)
        }
      }
    )
  }
```

laodAsyncに３つの引数が渡されています。
loadAsyncが何をしているかを確認します。

```js:loadAsync
export const loopAsync = (turns, work, callback) => {
  let currentTurn = 0, isDone = false
  let isSync = false, hasNext = false, doneArgs

  const done = (...args) => {
    ...,
  }

  const next = () => {
    ...,
  }

  next()
}
```

ここまででlistenerが分かりました。
listenerの引数である`_createLocation`を見に行きます。

```js:_createLocation
const _createLocation = (historyState) => {
  const key = historyState && historyState.key

  return createLocation({
    pathname: window.location.pathname,
    search: window.location.search,
    hash: window.location.hash,
    state: (key ? readState(key) : undefined)
  }, undefined, key)
}
```

event.state.keyを取得して、createLocationしています。
stateのreadStateを見てみます。

```js:readState
export const readState = (key) => {
  let json
  try {
    json = window.sessionStorage.getItem(createKey(key))
  } catch (error) {
    if (error.name === SecurityError) {
      // Blocking cookies in Chrome/Firefox/Safari throws SecurityError on any
      // attempt to access window.sessionStorage.
      warning(
        false,
        '[history] Unable to read state; sessionStorage is not available due to security settings'
      )

      return undefined
    }
  }

  if (json) {
    try {
      return JSON.parse(json)
    } catch (error) {
      // Ignore invalid JSON.
    }
  }

  return undefined
}
```

まずは、createKeyを見ていきます。

```js:createKey
const KeyPrefix = '@@History/'

const createKey = (key) =>
  KeyPrefix + key
```

このkeyをwindow.sessionStorage.getItemに渡してjsonを取得しています。
おそらく、browserが持つsessionの内、履歴についてのものでしょうか。

```js:createLocation
export const createLocation = (input = '/', action = POP, key = null) => {
  const object = typeof input === 'string' ? parsePath(input) : input

  const pathname = object.pathname || '/'
  const search = object.search || ''
  const hash = object.hash || ''
  const state = object.state

  return {
    pathname,
    search,
    hash,
    state,
    action,
    key
  }
}
```

`input={...}`のObjectを渡しています。
actionにはundefinedが渡されていますが、undefinedの場合はdefault値が適用されるので、action=POPとなります。
結果は、LocationのObjectを返しています。
inputを引数に取るparsePathを見に行きます。

```js:parsePath
export const parsePath = (path) => {
  let pathname = extractPath(path)
  let search = ''
  let hash = ''

  ...,

  const hashIndex = pathname.indexOf('#')
  if (hashIndex !== -1) {
    hash = pathname.substring(hashIndex)
    pathname = pathname.substring(0, hashIndex)
  }

  const searchIndex = pathname.indexOf('?')
  if (searchIndex !== -1) {
    search = pathname.substring(searchIndex)
    pathname = pathname.substring(0, searchIndex)
  }

  if (pathname === '')
    pathname = '/'

  return {
    pathname,
    search,
    hash
  }
}
```

pathnameを返す、extractPathを見てみます。

```js:extractPath
const extractPath = (string) => {
  const match = string.match(/^https?:\/\/[^\/]*/)
  return match == null ? string : string.substring(match[0].length)
}
```

inputのstringに対して正規表現で抜き出しています。
`https://hogehoge/bar`と`http://hogehoge/foo`のようなものはmatchしますが、それ以外はstringそのままを返します。
parsePathに戻って、次にhashstringとquerystringが含まれていればそこまでの文字列をpathnameとしています。
`https://hogehoge/#bar -> { pathname: https://hogehoge/, hash: #bar }`となり、`https://hogehoge/foo?item=bar -> { pathname: https://hogehoge/foo, search: ?item=bar }`となります。

以上からまとめると、createBrowserHistory.listen(listener)が実行されると次のように処理が走ることがわかりました。
window.popstate(browserのurlが変更される)のイベントが発生->現在のstateからlocationを作成->loadAsync内の処理でok=trueならば、actionはPOPなのでupdateLocationが実行される

これでReact Routerを読み進めることができます。