jsdog.dev

TypeSafeなReducerを作るライブラリ「Fluent Reducer」

こんにちは、せーぶるです

普段から技術的な発信はあまりしていないのですが、 久々にライブラリを作ってみたのでここで紹介したいと思います

[fluent-reducer](https://github.com/sable-virt/fluent-reducer)

特徴

  • TypeScriptフレンドリー
  • フレームワーク非依存
  • React/vue/Angularでも使える(Reactのときは拡張クラスを使うと便利)
  • Redux/ReduxThunkライクに非同期アクションが書ける(というかインスパイアしてる)
  • 複数のReducerを作ったときのdispatchに渡せるactionがTypeScriptでチェックされる(後述)

コードサンプル

以下はReactで利用する場合の例です

import { ReactFluentReduer } from 'fluent-reducer/react'
export interface IRootState {
  name: string
}
const RootState: IRootState = {
  name: 'unknown'
}
const rootReducer = new ReactFluentReducer<'root', IRootState>(RootState)

// SyncAction
const changeName = rootReducer.sync<string>('CHANGE_NAME', (state, name) => {
  state.name = name
})

// AsyncAction
const asyncChangeState = reducer.async<string, string, Error>('ASYNC_CHANGE_NAME', (name, dispatch, getState) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(name)
    }, 3000)
  })
}, {
  // before promise action
  started(state, params) {
    console.log('started', params)
    state.state = 'started'
  },
  // promise rejected action
  failed(state, { error }) {
    console.error(error)
  },
  // promise resolved action
  done(state, { result }) {
    state.name = result
    console.log('done')
  }
})

// React Component
export const MyExampleComponent: React.FC = () => {
  const [state, dispatch] = rootReducer.useFluentReducer()
  const _handleOnClick = useCallback(() => {
    dispatch(changeName(Date.now().toString()))
  }, [dispatch])
  return (
    <div onClick={_handleOnClick}>{state.name}</div>
  )
}

React/Vue/Angularと併用する際のサンプルはこちらにあります

https://github.com/sable-virt/fluent-reducer-examples

React/Vue/Angularで使う例を書きましたが、 ベターな書き方をしている訳ではありませんので、実装時には各々の流儀にしたがって使ってください

Reactだけ拡張されたクラスがあるのは実際にこれを使って作る際にあったらいいなというのを追加しただけなので、VueやAngularも拡張可能です

もちろんReact/Vue/Angularがなくても使うことができます

向いているとき

  • useReducerだけでは少々心もとないとき
  • TypeSafeに状態管理したいけど面倒、やり方がわからないとき
  • Reduxまで使うのは少々大げさだなと感じるとき

なぜ作ったか

  • Reduxベースでアプリケーション構築することが多くなった
  • Reduxで複数Reducer作ってやるほど大きくないものも出てきた
  • なるべく構成を近づけたいがuseReducerでTypeSafeに組むのは少々面倒
  • useReducerだけだと人によって書き方がバラバラになりそう
  • 趣味(構成をこねくりまわすのが好きなだけ)

複数useReducerしたときのdispatchの問題

そんな構造にしなければいいのかもしれないですが、そのようにしないといけない場合もありますゆえ・・・


const [state, dispatch] = useReducer(reducer1, st)
const [state2, dispatch2] = useReducer(reducer2, st)

// 本当はdispatch2にしないといけない
dispatch(changeName('sable'))

このときdispatch, dispatch2に渡すActionを間違えると意図したようにstateが書き変わりません(reducer1のstateを書き変えるアクションをreducer2に実行してしまう、など)

Dispatchに渡すアクションを間違えたとき

fluent-reducerではあえてReducerとActionを密にすることで自身のReducerから作成されたアクションでないものをdispatchしようとするとTypeScriptのチェック段階でエラーを出します

これはReducer定義時に<‘root’ とIDをつけることで各アクションにも同様のID定義が振られ、結果Actionの定義とマッチせずエラーとなります

これによって意図しないReducerに誤ってdispatchすることを防ぎます Reducerの統廃合や分割したいときに有効ではないかと思います

最後に

正直勢いでざざっと作ったので微妙なところがあるかもしれません 状態管理についてはいまだに頭を悩ませるところもあるのでなかなか大変ですね

公開後は少し更新は様子を見ながらちょっとずつやろうかなとは思ってます 本体はもちろんのこと、サンプルアプリに対してのPRお待ちしてます