Первое нажатие на кнопку имеет странное поведение

Я работаю над приложением списка задач, используя

  • scalajs,
  • кошки (бесплатные монады) и
  • scalajs реагируют

Когда я использую простую модель, подобную приведенному ниже, все работает, как и ожидалось.

class TodoModel() {
  private object State {
    var todos = Seq.empty[Todo]

    def mod(f: Seq[Todo] => Seq[Todo]): Callback = {
      val newTodos = f(todos)
      Callback(todos = newTodos)
    }
  }

  def add(t: Todo): Callback = State.mod(_ :+ t)
  def todos: Seq[Todo] = State.todos
}

Как только я использую бесплатные монады от кошек, у меня странное поведение. Первый щелчок всегда вставляет две записи задачи. Каждый клик впоследствии работает как ожидалось. Смотрите фотографии ниже.

Что здесь не так?

import cats.free.Free
import cats.free.Free.liftF
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.html_<^._
import org.scalajs.dom

case class Todo(text: String)

sealed trait TodoModelOp[A]
case class Add(todo: Todo) extends TodoModelOp[Unit]
case class Todos() extends TodoModelOp[Seq[Todo]]

object FreeTodoModelOps {
  // type alias for lifted TodoModelOp
  type TodoModelOpF[A] = Free[TodoModelOp, A]

  def add(Todo: Todo): TodoModelOpF[Unit] = liftF[TodoModelOp, Unit](Add(Todo))
  def todos: TodoModelOpF[Seq[Todo]] = liftF[TodoModelOp, Seq[Todo]](Todos())
}

object StateInterpreter {
  import cats.arrow.FunctionK
  import cats.{ Id, ~> }

  val interpet: TodoModelOp ~> Id = new (TodoModelOp ~> Id) {
    val todos = scala.collection.mutable.ArrayBuffer.empty[Todo]

    def apply[A](fa: TodoModelOp[A]): Id[A] = fa match {
      case Add(todo) => todos += todo; ()
      case Todos() => todos.toSeq
    }
  }

}

class TodoModel() {
  import cats.instances.list._
  import cats.syntax.traverse._
  import FreeTodoModelOps._

  def add(t: Todo): Callback = {
    def program: TodoModelOpF[Unit] = for {
      _ <- FreeTodoModelOps.add(t)
    } yield ()

    Callback(program.foldMap(StateInterpreter.interpet))
  }

  def todos: Seq[Todo] = {
    def program: TodoModelOpF[Seq[Todo]] = for {
      n <- FreeTodoModelOps.todos
    } yield n

    program.foldMap(StateInterpreter.interpet)
  }
}

object TodoPage {

  case class Props(model: TodoModel)

  case class State(todos: Seq[Todo])

  class Backend($: BackendScope[Props, State]) {
    val t = Todo("a new todo")

    def onSubmit(e: ReactEventFromInput) =
      e.preventDefaultCB >>
        $.modState(s => State(s.todos :+ t)) >>
        $.props.flatMap(P => P.model.add(t))

    def render(S: State) =
      <.div(
        <.form(
          ^.onSubmit ==> onSubmit,
          <.button("Add #", S.todos.length + 1)),
        <.ul(S.todos.map(t => <.li(t.text)): _*))

  }

  val component = ScalaComponent.builder[Props]("Todo")
    .initialStateFromProps(p => State(p.model.todos))
    .renderBackend[Backend]
    .build

  def apply(model: TodoModel) = component(Props(model))
}

object Test {
  val model = new TodoModel()

  def main(args: Array[String]): Unit = {
    TodoPage.apply(model).renderIntoDOM(dom.document.getElementById("mount-node"))
  }
}

пусто, нет нажатия на кнопку
пустой

первый клик по кнопке
первый клик

второй клик по кнопке
второй клик

1 ответ

Решение

В вашем первом фрагменте есть ошибка:

Здесь у вас есть переменная todos (inpure), к которому вы обращаетесь в чистом контексте:

def mod(f: Seq[Todo] => Seq[Todo]): Callback = {
  val newTodos = f(todos)
  Callback(todos = newTodos)

Примеси должны быть в Callback, Даже чтение переменной вне Callback небезопасно, поэтому оно должно быть:

def mod(f: Seq[Todo] => Seq[Todo]): Callback =
  Callback(todos = f(todos))

(См. Пример Scalajs-реагировать на Ref.scala для безопасной работы с переменной.)

Во-вторых, что касается вашего большего фрагмента, scalajs-реагировать очень дружелюбно к FP, но это очень нетрадиционный способ его использования и имеет ряд существенных проблем:

  • StateInterpreter.interpet не является ссылочно-прозрачным; есть общее глобальное состояние, лежащее в основе этого. Не проходит тест FP. Перестает быть законным естественным преобразованием.
  • Вы отслеживаете два набора идентичных состояний по отдельности: состояние компонента и состояние в TodoModel (нечистый, не проходит тест FP). Этот подход не только избыточен и создает риск рассинхронизации двух состояний, но также делает компонент менее пригодным для повторного использования; представьте, что вы решили нарисовать его дважды на одном экране для одних и тех же данных - они будут синхронизированы. Лучше всего сохранить компонент без сохранения состояния и чистоты.
  • Если вы собираетесь преобразовать свободную структуру в эффект компонента, лучше всего превратить ее в монаду состояния, см. Пример здесь.

Это действительно круто, что ты узнаешь о бесплатных монадах и скалайс-реакции. FP сделает вашу программу по-настоящему, очень легко рассуждать и предотвращать непонятные сюрпризы в поведении, но вам не нужно обрезать углы и следить за тем, чтобы весь ваш код оставался чистым. Любая примесь сделает весь стек полностью вплоть до нечистоты точки входа и удалит эти хорошие надежные свойства FP из этих слоев. Я бы предложил сделать все максимально чистым, используя приведенные выше пункты в качестве отправных точек, и тогда я думаю, вы обнаружите, что ошибка просто исчезает или, по крайней мере, ее очень легко обнаружить. ура

Другие вопросы по тегам