What is the correct way to pass state down the tree?

After working with imba for a little over 14ish hours now, I’m at a point where I feel mostly comfortable. Though, since I’ve started, I’ve had a burning question . . .

What is the best way to pass state to children that are far down the tree? ‘Lifting the state up’ or hard-wiring the path makes the GUI inflexible and tightly-coupled; also lifting the state up adds a lot of noise. To solve this problem in other frameworks, I’ve used dependency injection in Flutter (using dependency injection over their build context), and Svelte had a “Context” API. These allowed me to essentially pass state down the tree directly to whatever asked for that specific state. It removes the noise and the tight-coupling.

The closest things I’ve thought of for Imba without reinventing the wheel was using global state or just falling back to tightly coupling the state-path to the components. While global state made a lot of sense at first, it ended up not being practical for scenarios where sub-tags that used the same name but were looking for unique data would be in conflict. Otherwise, I was running into trouble understanding the component data-flow and update/rendering cycles that prevented me from making my own context API.

Any ideas or suggestions? Thanks!!

1 Like

Do you have a specific example? To access a global state (on the client side) I’ve often just defined a method on all elements:

extend tag element
    def api
        someGlobalState

Then you can access it from any tag anywhere in your app. Hopefully you can provide some sort of example so I can think more about it and help out :slight_smile:

2 Likes

Thanks for the reply, SomeBee!

To put it more concretely,

I’m trying to figure out a way to pass information down the tree to a component’s descendants with the following requirements:

Abides to these rules

  1. The node that sets the data does not need to know about its children
  2. The node that retrieves the information retrieves it “dynamically” (or via “injection”)
  3. Information set at level 1 of the tree is accessible at level x+1, AKA whoever queries for it.
  4. State cascades. Nodes may change the state for its children at any point in the tree, and it doesn’t effect the state above it, but below it.

Theoretical Imba example

tag list-view
  def render
    <self>
      <context lists=[]>
        for todo in context@lists # queries for outer context
          # anything here wont have access to lists from outer context
          # BUT it does have access to "read-only" from outer context


tag todoRoot
  def render
    <self>
      <context lists=[todo.new] read-only=... >
        <list-view>
      <context otherData=[....]>
        <other-node> # doesn't know the parent context's sibling context

Hopefully this explains it better! I know this example is trivial, but hopefully it puts the idea to light.

Even though I can make a prototype, if something like this already exists natively in Imba that’d be my preference. If that doesn’t exist, but there is some base functionality that makes this possible, I’d like to hear more about that.

I’ve made a working prototype, but could it be improved? I don’t like that it requires using arrays . . . but I don’t think dynamic properties exist.

Base prototype classes

# A context class for setting data that's retrievable by children
tag context 
	prop _ctx default: {}, watch: yes

	def add key, val
		console.log key, val
		_ctx[key] = val

	def setSet arr
		add(arr[0], arr[1])
		self
	
	def get key
		return _ctx[key] if _ctx[key]
		let parentCtx = @parentContext 
		return parentCtx.get key if parentCtx
		return null

	def getParentContext
		let par = @owner_
		while par
			if par.@_ctx != null
				return par
			par = par.@owner_

	def render
		<self>

### Make context available to all tags
extend tag element
	def context
		return getContext

	def getContext parent
		let par = @owner_
		while par
			if par.@_ctx != null
				return par
			par = par.@owner_

General usage

tag app-view-root
	def render 
		<self> 
			<context set=['boards', data.boards] set=['name', 'test']>
				<app-view[data].{app}>

# further down the tree...
tag dock-view
	def render 
		<self.{dock}>
			<ul> 
				<li> <a route-to='/all-boards'> 'All boards'
				for board in context.get('boards')
                                       ....

Demo Scrimba

Thank you for the example. I’ll be thinking about it today. In imba 2 we theoretically have all the information about the parent tree even before children are actually inserted, so it should be easier to do this in an elegant way there.

2 Likes

Worked on this today, and have implemented decent support for this (imho) in imba 2. That is, within tags you have access to a private field #context. Whenever you access properties on this object it will traverse up the parent/context chain and return the value from the first parent that has this key defined.

tag app-view-root
	def render 
		<self boards=(...)> 
			<app-view> <dock-view>


# further down the tree...
tag dock-view
	def render 
		<self>
			<ul> 
				<li> <a route-to='/all-boards'> 'All boards'
				for board in #context.boards
				    <li> <a href=board.link> ...

You can see another example here: https://github.com/imba/imba/blob/master/test/apps/examples/context-state.imba

The hash-prefixed variables is an imba 2 feature inspired by ESnext (see private fields https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Class_fields). Will create a small cast showing the context stuff as soon as we’ve updated the imba alpha on scrimba :slight_smile:

Btw, imba-router doesnt work with imba 2 at all just yet, so I guess you should stick to v1 still, but just know that these things will work nicely in v2 and that we’re working hard to get it out into production!

2 Likes

And here is a scrimba playground: https://scrimba.com/c/ckPN7VtD
I’m still contemplating whether to call it context or something else. Could maybe even consider a special type of syntax - like a special prefix. So lets say we used @@, then @data would access data on self (this.data) while @@data would access *thisOrClosestParentWithData*.data. I guess it could be useful.

2 Likes

Wow! That was by far the best of outcomes I could ever hope for; having a suggestion be added into the language at such a native level so quickly - that’s unheard of :grin:!

Accessing up the tree sounds great! I’m not against it since that would be the ultimate level of terseness and directness - but I’m wondering how it would fare from a “maintainability” and “scalibility”. Since the parent chain doesn’t need to be explicit about which properties descendants will be specifically piggy-backing off of. Maybe the feature could introduce an additional feature to handle scope and accidental name conflicts? Or would an explicit syntax that says ‘this will shadow down the tree’ be sufficient? I think that would be my only two questions! I will also be thinking about this for the rest of the day haha…

I think @@data could have some important repercussions - because if we’re using only one property then the individual keys on the data objects from earlier in the chain would be lost during access unless those are merged in retrieval – otherwise that could be pretty good! Or perhaps I’ve misunderstood how @@data would work

Thanks so much for your work!

After thinking for a while, I think my vote goes for what is the simplest, and this is what I think is the simplest.

@@parentProvidedProp(/Func?) makes a lot of sense and is the most simple. It already offers a lot of flexibility and is terse and readable. If someone wanted to make a scoped context tree they could define their own specially named context property that’s attached to the root and then reused by all subsequent components of that domain.

I also think that @@<symbol> should be a shortcut for #context.<symbol>. So whatever is possible from @@<symbol> should, in theory, be accessible from #context.. … At least I think so!

Edit:
Although unintentional property shadowing, for the parent property tree, will exist, I’m not quite convinced anymore it will be a real problem. I’m assuming the typical candidates for being shadowed won’t usually be properties anyone is trying to reach for anyways.

Sounds like a good idea. We should maybe wait with the special syntax until 2.1 or something - and use #context.prop. for a while first. Right now #context.prop will start lookup at the current node, but I guess it makes more sense to always start lookup at the parent in these cases. Otherwise this wouldnt work:

tag avatar
    get user
        #context.user // currently results in an infinite loop of lookups

So, if context lookup starts at the parent we can even easily set the default value of fields - unless a specific value is sent in.

tag avatar
    @user = #context.user
    
    def render
        <self> <img src=@user.avatar-url>

Thanks for all you rinput xanazoid! It is really a useful feature, and the inclusing of a predictable tracking of parent context was also needed for SSR (which I’ll jump on next week).

4 Likes

That’s awesome, I completely agree with everything you wrote. And thank you too, Sindre! I appreciate your work - I honestly can’t wait to use Imba 2!! :smile:

look forward to trying this out tomorrow.

We will hopefully do a cast on the context changes tomorrow :smile:

@somebee can you follow up on doing a cast on the context changes?

Thanks.

2 Likes

I can do one when I’m back at the office tomorrow.

1 Like

For those reading this in the future, there is now a cast on the context API

1 Like