Обновить отдельный дочерний компонент

Я использую Diode 1.0.0 с scalajs-реагировать на 0.11.1.

Случай использования:

  • Родительский компонент со списком дочерних компонентов
  • Фрагмент детской модели содержит Pot для асинхронно извлеченного изображения
  • Дочерний компонент извлекает изображение при подключении и Pot является Empty, обновляя свой фрагмент модели

При наивном подходе это приводит к следующему сценарию (порядок событий может быть другим):

  1. Родитель оказан.
  2. Ребенок 1 отображается.
    1. Ребенок 1 отправляет ее GetImageAction, Фрагмент модели Pot обновляется до Pending,
    2. Модель обновлена, в результате чего родительский объект будет перерисован.
    3. Все дети перевоплощены.
    4. Дети 2 … п еще есть EmptyPot, поэтому они вызывают их GetImageActionСнова
  3. Теперь ребенок 2 отображается.
    1. Модель обновлена, в результате чего родительский объект будет перерисован.
    2. И т.п.

Это вызывает огромное дерево GetImageAction вызовы и перерисовки.

Некоторые вопросы:

  1. Неправильно ли использовать модель для этой цели? Было бы лучше использовать состояния компонентов?
  2. Как можно избежать повторного рендеринга родителя, когда только ребенок нуждается в обновлении? Я не мог понять, если / как я могу использовать shouldComponentUpdate для этого.

Обновление 1

Теперь я добавляю ключ React для каждого дочернего компонента. Это избавило от предупреждения React относительно уникальных ключей, но, к сожалению, не решило проблему выше. Дети перевоплощаются, даже если их shouldComponentUpdate метод возвращает false,

От ParentComponent.render():

  items.zipWithIndex.map { case (_, i) =>
    proxy.connector.connect(
      proxy.modelReader.zoom(_.get(i)), s"child_$i": js.Any).
      apply(childComponent(props.router, _))
  }

Обновление 2

Я попытался реализовать функциональность слушателя в родительском компоненте, но, к сожалению, потомки все еще не подключены и не смонтированы. Вот код моего родительского компонента:

package kidstravel.client.components

import diode.data.{Empty, Pot}
import diode.react.ModelProxy
import diode.react.ReactPot._
import diode.{Action, ModelR}
import japgolly.scalajs.react.extra.router.RouterCtl
import japgolly.scalajs.react.vdom.prefix_<^._
import japgolly.scalajs.react.{BackendScope, ReactComponentB, _}
import kidstravel.client.KidsTravelMain.Loc
import kidstravel.client.services.{KidsTravelCircuit, RootModel}

case class TileProps[T](router: RouterCtl[Loc], proxy: ModelProxy[T])

/**
  * Render sequence of models as tiles.
  */
trait Tiles {

  // The type of the model objects.
  type T <: AnyRef

  /**
    * Override to provide the action to obtain the model objects.
    * @return An action.
    */
  def getAction: Action

  /**
    * Returns the tile component class.
    * @return
    */
  def tileComponent: ReactComponentC.ReqProps[TileProps[T], _, _, _ <: TopNode]

  case class Props(router: RouterCtl[Loc], proxy: ModelProxy[Pot[Seq[T]]])

  class Backend($: BackendScope[Props, Pot[Seq[T]]]) {

    private var unsubscribe = Option.empty[() => Unit]

    def willMount(props: Props) = {
      val modelReader = props.proxy.modelReader.asInstanceOf[ModelR[RootModel, Pot[Seq[T]]]]
      Callback {
        unsubscribe = Some(KidsTravelCircuit.subscribe(modelReader)(changeHandler(modelReader)))
      } >> $.setState(modelReader())
    }

    def willUnmount = Callback {
      unsubscribe.foreach(f => f())
      unsubscribe = None
    }

    private def changeHandler(modelReader: ModelR[RootModel, Pot[Seq[T]]])(
        cursor: ModelR[RootModel, Pot[Seq[T]]]): Unit = {
      // modify state if we are mounted and state has actually changed
      if ($.isMounted() && modelReader =!= $.accessDirect.state) {
        $.accessDirect.setState(modelReader())
      }
    }

    def didMount = $.props >>= (p => p.proxy.value match {
      case Empty => p.proxy.dispatch(getAction)
      case _ => Callback.empty
    })

    def render(props: Props) = {
      println("Rendering tiles")
      val proxy = props.proxy
      <.div(
        ^.`class` := "row",
        proxy().renderFailed(ex => "Error loading"),
        proxy().renderPending(_ > 100, _ => <.p("Loading …")),
        proxy().render(items =>
          items.zipWithIndex.map { case (_, i) =>
            //proxy.connector.connect(proxy.modelReader.zoom(_.get(i)), s"tile_$i": js.Any).apply(tileComponent(props.router, _))
            //proxy.connector.connect(proxy.modelReader.zoom(_.get(i))).apply(tileComponent(props.router, _))
            //proxy.wrap(_.get(i))(tileComponent(_))
            tileComponent.withKey(s"tile_$i")(TileProps(props.router, proxy.zoom(_.get(i))))
          }
        )
      )
    }
  }

  private val component = ReactComponentB[Props]("Tiles").
    initialState(Empty: Pot[Seq[T]]).
    renderBackend[Backend].
    componentWillMount(scope => scope.backend.willMount(scope.props)).
    componentDidMount(_.backend.didMount).
    build

  def apply(router: RouterCtl[Loc], proxy: ModelProxy[Pot[Seq[T]]]) = component(Props(router, proxy))

}

1 ответ

Скорее всего это связано с звонком connect внутри метода рендеринга. Это заставит размонтировать / перемонтировать все дочерние компоненты. Лучше позвонить connect например, когда родительский компонент монтируется и затем использует результат в рендере.

В качестве альтернативы вы можете пропустить connect в целом и реализуйте прослушиватель изменений внутри родительского компонента напрямую. Когда коллекция элементов изменяется, обновите состояние, которое вызывает повторную визуализацию, обновляя все компоненты, которые были изменены. С помощью shouldComponentUpdate позволяет React определить, какие компоненты действительно изменились.

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