pockestrap

Programmer's memo

Ovtoでコンポーネントに閉じたStateを作る

結論

実装に無理があるので、OpalからReactを使いたい。

これ何

Ovtoはhyperappをベースにしている。Ovtoではstateはappに対して1つのツリーしかない。 つまり、各コンポーネントに閉じたstateを持つことはできず、全てはグローバルなstateを触ることになる。

これが嫌になったので、コンポーネントに閉じたStateを作ってみた。

実装

module Ovto
  class LocalStateMiddleware < Ovto::Middleware('local_state')
    class State < LocalStateMiddleware::State
      item :generation, default: 0
    end

    class Actions < LocalStateMiddleware::Actions
      def redraw
        { generation: state.generation + 1 }
      end
    end
  end

  module LocalState
    def local_state(&default)
      @local_state ||= default.call
    end

    def set_local_state(state)
      @local_state = @local_state.merge state
      actions.local_state.redraw
    end
  end

  Ovto::Component.prepend LocalState
end

使い方

まずAppでLocalStateMiddlewareをuseする。

class MainApp < Ovto::App
  use Ovto::LocalStateMiddleware

  # ...
end

そうするとstateを使いたいコンポーネントlocal_stateset_local_stateメソッドが使えるようになるので、それを使ってstateの取得/更新をする。

コードは適当なので動かないかもしれない。

class C < Ovto::Component
  class State < Ovto::State
    item :counter, default: 0
  end

  def render
    s = local_state do
      # デフォルト値
      State.new
    end

    o 'div' do
      o 'span', s.counter
      o 'button', { onclick: -> (_) { set_local_state(counter: s.counter + 1) } }
    end
  end
end

実装の解説

OvtoではOvto::Componentインスタンスの寿命はだいたいrenderされているときと一致するので、インスタンス変数に適当にstateを突っ込みつつ、renderし直すためにMiddlewareのstateを更新している。

既知の問題

Reactでkeyが必要になるようなケースで、コンポーネントがrenderされる順序が変わるとOvto::Componentインスタンスが作り直されて、stateがリセットされるような気がする。 試していないので未確認。