Don't re-render siblings in Treeview with scalajs-react

231 Views Asked by At

I've built a simple TreeView with scalajs-react. Each node contains a text field.

I write some text into child 1.1:

enter image description here

Now, if I add new child 1.2 below 1, the text disappears because node 1 with all its children gets re-rendered:

enter image description here

When adding a child in this Javascript-Redux TreeView, the siblings are not re-rendered. How can I achieve that with scalajs-react?

See my code below or a minimal example project on GitHub.

case class Node(text: String, children: Vector[Node])

object TreeView {
  val childNode = Node("1.1", Vector())
  val parentNode = Node("1", Vector(childNode))

  val rootNode = ScalaComponent.builder[Unit]("Node")
    .initialState(parentNode)
    .renderBackend[NodeBackend].build

  class NodeBackend($ : BackendScope[Unit, Node]) {

    def addChild =
      $.modState(
        _.copy(children = $.state.runNow().children :+ Node("1.2", Vector())))

    def render(node: Node): VdomElement = {
      val children =
        if (node.children.nonEmpty)
          node.children.toVdomArray(child => {
            val childNode = ScalaComponent.builder[Unit]("Node")
              .initialState(child)
              .renderBackend[NodeBackend].build
            childNode.withKey(child.text)()
          })
        else EmptyVdom

      <.div(
        node.text, <.input(), <.button("Add child", ^.onClick --> addChild),
        children
      )
    }
  }

  def apply() = rootNode()
1

There are 1 best solutions below

4
On BEST ANSWER

This is more along the lines of how to do it:

case class Node(label: String, text: String, children: Vector[Node])

object TreeView {
  val childNode = Node("1.1", "", Vector.empty)
  val parentNode = Node("1", "", Vector(childNode))

  val NodeComponent = ScalaComponent.builder[Node]("Node")
    .initialStateFromProps(identity)
    .renderBackend[NodeBackend]
    .build

  class NodeBackend($: BackendScope[Node, Node]) {

    def addChild =
      $.modState(s =>
        s.copy(children = s.children :+ Node("1.2", "", Vector.empty)))

    val onTextChange: ReactEventFromInput => Callback =
      _.extract(_.target.value)(t => $.modState(_.copy(text = t)))

    def render(node: Node): VdomElement = {
      val children =
        node.children.toVdomArray(child =>
          NodeComponent.withKey(child.label)(child))

      val input =
        <.input.text(
          ^.value := node.text,
          ^.onChange ==> onTextChange)

      <.div(
        node.label, input, <.button("Add child", ^.onClick --> addChild),
        children
      )
    }
  }

  def root = NodeComponent(parentNode)
}

Changes

  • Don't create a new component per Node, create a new instance of the same component. Amongst other reasons, React will always think it's got something different and redraw it, losing state for stateful components
  • Add value and onChange to input so that React tracks the editor contents, else editor changes only appear to work but React will wipe them whenever it feels like it.
  • Don't call .runNow() on Callback - it's an escape hatch for fringe situations and should always be avoided

That'll do. This still concerns me because you're using stateful components which are dangerous because they're the high-level version of a big mutable variable. You might find that React makes the wrong call and throws your state away at runtime in certain situations. You can achieve the same goals in a safer way statelessly, check out https://japgolly.github.io/scalajs-react/#examples/state-snapshot for one of the ways it can be done. Hope that helps.