nanohtml icon indicating copy to clipboard operation
nanohtml copied to clipboard

[feature] add thunking API

Open yoshuawuyts opened this issue 7 years ago • 7 comments

In yo-yo and choo it's a best practice to have element functions in an application return the same element given the same input. This mechanism is commonly referred to as thunking.

How would we feel about adding a light wrapper that exposes a thunk function so that elements can be thunked with little effort. I was thinking of an API along these lines:

// elements/my-button.js
const thunk = require('bel/thunk')
const html = require('bel')

module.exports = thunk(createMyButton)

// create a cool button that has a different color
function createMyButton (color) {
  return html`
    <button style="background-color: ${color}">
      Click me! :D
    </button>
  `
}

In a choo app this could then be consumed as:

const html = require('choo/html')
const choo = require('choo')

const myButton = require('./elements/my-button')

const app = choo()
app.router([ '/', myView ])

app.model({
  state: { color: 'blue' }
})

const tree = app.start()
document.body.appendChild(tree)

function myView (state, prev, send) {
  return html`
    <main>
      ${myButton(state.color)}
    </main>
  `
}

What do you think? Would this be a useful addition to bel, or should we continue to point people to use an external thing / write it themselves? I was thinking of including this into choo, but given that we strongly encourage people to use bel for standalone components (yay, reusability) I figured it might make more sense to include it here first, and then have it propagate upwards through yo-yo to choo. Thanks!

See Also

  • https://github.com/Raynos/vdom-thunk

yoshuawuyts avatar Aug 03 '16 14:08 yoshuawuyts

Hmm let me give it some more thought. Initially it feels like this should be in the diffing library or yo-yo as bel doesn't know about previous elements.

shama avatar Aug 03 '16 22:08 shama

Yeah, doesn't thunking only become relevant when you're executing the element creation over and over again (a la morphdom)?

timwis avatar Aug 04 '16 21:08 timwis

@timwis yeah you're right, but this type of rendering is quickly becoming the default; e.g.

  • react
  • yo-yo, choo
  • ember
  • mithril, mercury, etc.

Probably the only hot new one around that doesn't do virtual-dom diffing would be Angular 2, but that's about it.

yoshuawuyts avatar Aug 05 '16 09:08 yoshuawuyts

Just an update that I've been experimenting with ideas but nothing really to show for yet. But wanted to mention using a feature like this for handling <canvas> updates.

Currently <canvas> tags don't work well in the idiomatic flow because the tag never indicates itself as changed. The simple fix would be to always assume a <canvas> has changed when morphing, replacing with the new one.

But it would interesting if we let the element author choose when to invalidate. Basically the thunk just remembers the last passed arguments/element and appends to the function. Then any element not marked or first time through the thunk, gets updated. Any previous that are marked will always be ignored:

const html = require('yo-yo')
const thunk = require('yo-yo/thunk')

/* prev arg gets added by thunk to the end of the function call */
module.exports = thunk(function createCanvas (data, prev) {
  let canvas = prev.element
  if (prev.args[0].width !== data.width || prev.args[0].height !== data.height) {
    canvas = html`<canvas width="${data.width}" height="${data.height}" />`
  }
  const ctx = canvas.getContext('2d')
  ctx.fillRect(data.x, data.y, data.width, data.height)
  return canvas
})

/* ... */

let data = {x:0,y:0,width:100,height:100}
raf(function loop () {
  data.x += .1
  if (data.x > 100) data.width *= 2
  yo.update(root, canvas(data))
  raf(loop)
})

This might simplify the validation check too as we wouldn't need to traverse objects and arrays checking if the arguments have changed. We just let the element author choose the parameters that decide that. Which most of the time would be 1 or 2 if statements.

Or we could provide that API for convenience to the author to apply as needed:

const html = require('yo-yo')
const thunk = require('yo-yo/thunk')
module.exports = thunk(function createMyButton (data, prev) {
  let button = prev.element
  if (!button || prev.hasChanged(data)) {
    button = html`<button>${data.label}</button>`
  }
  return button
})

shama avatar Aug 09 '16 03:08 shama

That looks awesome @shama! Thanks for the writeup. prev being an object with element and args properties (rather than just the previous version of data) was confusing at first but I don't have a better idea. Maybe if it were called cache or something...

If I recall correctly, the way react (or at least redux) handles this is by "mapping state to props" when connecting the store to the component, so that react knows whether to update that specific component on a given state change. Right?

timwis avatar Aug 09 '16 10:08 timwis

@yoshuawuyts https://github.com/yoshuawuyts/cache-element/ can be used to thunk element, or am I confused?

roobie avatar Feb 06 '17 22:02 roobie

Yup, though I recommended nanocomponent these days - iterated on the interface a bit

On Mon, Feb 6, 2017, 23:15 Björn Roberg [email protected] wrote:

@yoshuawuyts https://github.com/yoshuawuyts https://github.com/yoshuawuyts/cache-element/ can be used to thunk element, or am I confused?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/shama/bel/issues/40#issuecomment-277831819, or mute the thread https://github.com/notifications/unsubscribe-auth/ACWlerG7SCfNAjUvrJ1sYvee_HIQ_er7ks5rZ5uTgaJpZM4Jbsbi .

yoshuawuyts avatar Feb 07 '17 04:02 yoshuawuyts