Search code examples
mithril.js

Why does the Mithril child component state change not trigger an update?


The following code works as expected: it creates two counter buttons that persist their state and update on clicking:

let Counter = function (vnode) {
  let count = 0

  return {
    view: (vnode) => {
      return m("button",
        {
          onclick: function () {
            console.log(count++)
          }
        }, "Count: " + count)
    }
  }
}

let Counters = {
  view: () => [
    m(Counter),
    m(Counter),
  ]
}

m.mount(document.body, Counters)

However, if I define the array of Counter components in a separate variable and pass that to the Counters view function, then the view stops updating. The state persists and I can see incrementing count logged to console, but nothing changes on screen. This is the updated code:

let Counter = function (vnode) {
  let count = 0

  return {
    view: (vnode) => {
      return m("button",
        {
          onclick: function () {
            console.log(count++)
          }
        }, "Count: " + count)
    }
  }
}

let counters = 
  [
    m(Counter),
    m(Counter),
  ]
let Counters = {
  view: () => counters
}

m.mount(document.body, Counters)

Why would this be happening? This is a toy example of a more complicated Mithril application that I'm working on, where I would like to arbitrarily sort the array of child components.


Solution

  • I was able to gather useful feedback in the Mithril Gitter chat and will post what I learned below.

    The reason the counters in my example were not updating was because they were defined once inside the counters array and it was returned subsequently. As the same array is returned every time the view() function of Counters is called, Counters does not see a need to update as it thinks that the elements it is returning have not changed. Even though the Counter elements inside the array update, the reference to the array remains the same.

    One way of handling this is by defining a factory function that would return a new sorted array of Counter elements each time the view() function on Counters is called. Furthermore, the application state then needs to be kept in a global location and the only parameter passed to each Counter is the index of the global counter it is referencing, like so:

    let counterData = [0, 0]
    
    let Counter = {
      view: ({
        attrs: {
          index
        }
      }) => {
        return m("button", {
          onclick: () => counterData[index]++
        }, "Count: " + counterData[index])
      }
    }
    
    let Counters = function () {
      const counters = () => {
        return counterData.sort((a, b) => a - b).map((count, i) => m(Counter, {
          index: i
        }, count))
      }
    
      return {
        view: () => counters()
      }
    }
    
    m.mount(document.body, Counters)
    

    A working JSFiddle can be found here.