japgolly / scalajs-react

Facebook's React on Scala.JS
https://japgolly.github.io/scalajs-react/
Apache License 2.0
1.64k stars 231 forks source link

Improvements exclusive to Scala 3.0 #785

Open japgolly opened 4 years ago

japgolly commented 4 years ago

Done

japgolly commented 3 years ago

Excerpt from TriStateCheckbox:

  final case class Props(state       : State,
                         setNextState: Callback,
                         disabled    : Boolean = false)

  private def render($: ScalaComponent.MountedPure[Props, Unit, Unit], p: Props) = {
    val setNext = $.props.flatMap(p => p.setNextState.unless_(p.disabled)) // Only access .setNextState inside the Callback for Reusability
      ...
  }

  implicit val reusabilityProps: Reusability[Props] =
    Reusability.caseClassExcept("setNextState") // .setNextState is never accessed outside of a Callback

Scala 3 could allow something like this (and with erased classes, without incurring any runtime cost!):

  final case class Props(state       : State,
                         setNextState: Xxxxxxxxxxx[Callback],
                         disabled    : Boolean = false)

  private def render($: ScalaComponent.MountedPure[Props, Unit, Unit], p: Props) = {
    val setNext = $.props.flatMap(p => p.setNextState.unless_(p.disabled))
      ...
  }

  implicit val reusabilityProps: Reusability[Props] =
    Reusability.caseClass

where

japgolly commented 3 years ago

Oh man, this is why it'd be good to enforce at the type-level. I just made a mistake above. Accessing the Callback in the render function would void Reusability (which is why I used $.props in the first place).

Correction:

given [A] Conversion[Xxxxxxxxxxx[A], A](using InRenderFn | InCallback)
// It should actually be this to preserve Reusability
given [A] Conversion[Xxxxxxxxxxx[A], A](using InCallback)

No wait a minute.... That's easily hacked:

def render(p: Props) = {
  val hack = Callback(p.setNextState) // wrong because `p` is fixed
}

How on earth can I have this work?

def render(p: Props) = {
  val good  = $.props.flatMap(_.setNextState)
  val bad  = Callback(p.setNextState)
}

I could just solve it really simplistically for the single BackendScope case but that feels a bit limited.

def render(p: Props) = {
  val good = $.fromProps(_.setNextState) // provide evidence directly from BackendScope
  val bad  = Callback(p.setNextState)    // reject because evidence unavailable
}

trait BackendScope[P, S] {
  // obviously rename all of this
  def fromProps[A](f: XxxxxxEvidence ?=> P => Xxxxxxxxxxx[A]): A
}

?

japgolly commented 3 years ago

Actually if I were to do something so simple, I wouldn't even need Scala 3 features. We could have this in Scala 2 today.

final case class Xxxx[A](unsafeGet: A)

trait BackendScope[P, S] {
  def fromProps[A](f: P => Xxxxxxxxxxx[A]): CallbackTo[A] =
    props.flatMap(f(_).unsafeGet)
}
japgolly commented 3 years ago