Первое нажатие на кнопку имеет странное поведение
Я работаю над приложением списка задач, используя
- 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 из этих слоев. Я бы предложил сделать все максимально чистым, используя приведенные выше пункты в качестве отправных точек, и тогда я думаю, вы обнаружите, что ошибка просто исчезает или, по крайней мере, ее очень легко обнаружить. ура