I'm using Diode 1.0.0 with scalajs-react 0.11.1.
Use case:
- Parent component with list of child components
- Child's model fragment contains
Pot
for asynchronously fetched image - Child component fetches image when mounted and
Pot
isEmpty
, updating its model fragment
With a naive approach, this causes the following scenario (order of events might be different):
- Parent is rendered.
- Child 1 is rendered.
- Child 1 dispatches its
GetImageAction
. Model fragmentPot
is updated toPending
. - Model is updated, causing parent to re-render.
- All children are re-rendered.
- Children 2 … n still have an
Empty
Pot
, so they trigger theirGetImageAction
s again.
- Child 1 dispatches its
- Now Child 2 is rendered.
- Model is updated, causing parent to re-render.
- Etc.
This causes a huge tree of GetImageAction
invocations and re-renderings.
Some questions:
- Is it wrong to use the model for this purpose? Would it be better to use component states?
- How can the re-rendering of the parent be avoided when only the child needs to be updated? I couldn't figure out if / how I can use
shouldComponentUpdate
for this purpose.
Update 1
I am now adding a React key to each child component. This got rid of the React warning regarding unique keys, but unfortunately didn't solve the issue above. The children get re-rendered, even if their shouldComponentUpdate
method returns false
.
From ParentComponent.render()
:
items.zipWithIndex.map { case (_, i) =>
proxy.connector.connect(
proxy.modelReader.zoom(_.get(i)), s"child_$i": js.Any).
apply(childComponent(props.router, _))
}
Update 2
I tried implementing the listener functionality in the parent component, but unfortunately the children are still unmounted and re-mounted. Here's the code of my parent component:
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))
}