redux-saga で DOMからアニメーションを分離した話

# DOMアニメーションをredux-sagaに乗せる

React + Redux でアニメーションといえば CSSアニメーションをマルチクラスでトリガーするものが殆どかと思いますが、要件によってはそれだけでは十分ではない場合があります。jQuery.animateでやっていた様に、インラインスタイルをゴリゴリ書き換えるものを React + Redux でも実現したいです。

しかし、多くのアニメーションライブラリはDOM参照する実装都合上、ref参照が必要になりStatelessを諦めなければいけなくなります。また、Presentational層にアニメーション終了などの action を発行する様なロジックを記述しなければいけません。

そこで、 Redux の副作用を扱う [redux-saga](https://github.com/yelouafi/redux-saga/blob/master/README_ja.md) を用いて実装したいと思います。動かすDOMは Store.state を参照するアプローチ。アニメーションライブラリに [gsap](https://www.npmjs.com/package/gsap) を使用します。

本稿サンプル通りの内容は、ひとつのコンポーネントをアニメーションさせるだけには少し大袈裟かも知れませんが、以下の得られる利点を考えると価値あるものだと考えています。

- StatelessFunctionalComponentを維持出来る
- 開始・終了をtaskで管理出来る
- アニメーションの状態をApp内共有出来る
- reducerの書き方によってはひとつのTweenで複数のコンポーネントを動かせる
- 他の非同期処理と足並みを揃えることが出来る
- 動作を分割して凝ったアニメーションを組み換え出来る

上記が全て不要な場合はCSSアニメーションで実装することをお勧めします。

# アニメーション関数を用意
redux-saga に乗せるため、Promise に wrap されたアニメーション関数を用意し、callで利用します。下記の例ではアニメーションを走らせるために`CLICK_BUTTON`が発行されるのを待ちます。

```~/actions/creators/animate.js
import types from '~/actions/types/animate'

export function updateStyle(style) {
  return { type: types.UPDATE_STYLE, style }
}

export function clickButton() {
  return { type: types.CLICK_BUTTON }
}
```

```~/sagas/animation.js
import types from '~/actions/types/animate'
import { updateStyle } from '~/actions/creators/animate'
import { dispatch } from  '~/store'

function someAnimate(duration) {
  return new Promise(resolve => {
    const currentStyle  = { opacity: 0 }
    const completeStyle = { opacity: 1 }
    TweenLite.to(currentStyle, duration / 1000,
      {
        ...completeStyle,
        onUpdate: () => { dispatch(updateStyle(currentStyle)) },
        onComplete: resolve,
        ease: Circ.easeOutBounce
      }
    )
  })
}

export function * someAnimateSaga() {
  while (true) {
    yield take(types.CLICK_BUTTON)
    yield call(someAnimate, 1000)
  }
}

```
```~/main.js
import 'gsap'
```

`TweenLite.to` とあるところが gsap のトゥイーン関数ですが、こちらのライブラリは エントリーポイントで import しておきます。これだけでGLOBALに `TweenLite` `TweenMax` `TweenPlugin` が定義されてしまうのでちょっと行儀が悪いです。（他に良いライブラリがあるよ！webpackでこのやり方があるよ！という方、ご存知であればお知らせいただけると幸いです）

`onComplete` で `Promise.resolve` された後、再度 `types.CLICK_BUTTON` を待ちます。



# ライブラリの callback で Store.dispatch

redux-saga では ActionCreator を `yield put` すれば、よしなに Store.dispatch してくれるので大変便利ですが、今回の様にGenerator関数ではない外部ライブラリなどの callback からでは yield が使えず、単純に `updateStyle(currentStyle)` しても Store.dispatch されません。（onUpdateのところ）この対策として、Store.dispatch を Store定義しているファイルから export し、`dispatch(updateStyle(currentStyle))`とすれば無事 Store.dispatch されます。

```~/store.js
const store = createStore(
  reducers,
  composeEnhancers(
    applyMiddleware(sagaMiddleware)
  )
)
export const dispatch = store.dispatch
```

# Reducer でプロパティを組み立てる

Action が発行されたら他と同様に Reducer で処理します。不要な `_gsTweenID` は削除しておきます。（initialStateのスキーマはサンプルのため簡素化しています）

```~/reducers/animate.js
export const initialState = {
  style: { opacity: 1 }
}

export default (state = initialState, action) => {
  switch (action.type) {
    ...
    case types.UPDATE_STYLE : {
      let { style } = action
      delete style['_gsTweenID']
      return Object.assign({}, state, { style })
    }
    ...
  }
}
```

直接opacityを操作する簡単なサンプルですが、冒頭で述べた通り、この実装でアニメーションの状態をApp内共有出来ます。Reducerの処理次第では複数のコンポーネントを同時に、かつ全く違うプロパティをTweenで動かせることが可能になります。

```~/reducers/animate.js
case types.UPDATE_STYLE : {
  let { style } = action
  delete style['_gsTweenID']
  const offset = style.opacity
  style = Object.assign({}, style, { transform: `scale(${offset})` })
  return Object.assign({}, state, { style })
}
```
transform は文字列で指定します。

# Component は Store.state を参照するのみ

いよいよComponentにアニメーションを適用します。以下の記述で、インラインスタイルでアニメーションするStatelessFunctionalComponentの完成です! それではみなさん、良いクリスマスを :tada:

```~/components/SomeComponent.js
export default function SomeComponent(props) {
  const { clickButton, animate } = props
  const { style } = animate
  return (
    <div>
      <div style={ style } />
      <button onClick={ clickButton }>click</button>
    </div>
  )
}
```