Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't wire the wires? #9

Open
mindplay-dk opened this issue Aug 17, 2024 · 4 comments
Open

Can't wire the wires? #9

mindplay-dk opened this issue Aug 17, 2024 · 4 comments
Labels
enhancement New feature or request

Comments

@mindplay-dk
Copy link

I can't find a way to wire a wire. (computed signal derived from a computed signal.)

image

I'm not sure if this is supported or not - I don't see a real example in examples/kitchen-sink, and the documentation in the README doesn't seem to cover it.

I'm confused as to the intended use of the $ token provided to wire functions - in some cases, the token is passed to a signal to subscribe:

const $doubleCount = wire(($) => $count($) * 2);

that's the "textbook" example from the README, which does work.

but I would expect I'd be able to subscribe to computed signals just the same:

const $doubleDoubleCount = wire(($) => $doubleCount($) * 2);

which doesn't work, and the computed signal doesn't accept any arguments.

I don't see a basic example of subscribing to a computed signal in examples/kitch-sink, but I did see subscriptions being created to what I presume are computed signals from some of the higher order functions, which seem to use this pattern:

const $doubleDoubleCount = wire(($) => $($doubleCount) * 2);

this doesn't work in my simple example either though.

and if that is the intended usage, I think that's problematic. 🤔

I would expect signals and derived signals to be as consistent as possible - having to pass the $ token in some cases, and call it in others, would get really confusing really fast, and will almost definitely create problems when refactoring between regular and computed signals.

(the terminology is a bit confusing as well, and I wonder if we could align better with Solid here, on both counts - usage as well as naming.)

I'm sure there's an explanation, but, arriving with some familiarity with Solid and Sinuous, and many other frameworks, I gave up after about an hour of "trying stuff", so there is at least a documentation problem here.

I want to port all my usual demos, which I've been porting between frameworks for many years - at least the ones you've seen in the Sinuous README ought to be portable, but this feels like maybe I'm a bit early? 😅

@abhishiv
Copy link
Owner

abhishiv commented Aug 17, 2024

Hey @mindplay-dk, you are correct that's a major feature lacking in alfama. My plan is to add the following features to wires

  1. Allow wires to be read subscribed just like signals like you suggest

  2. Change wire signature so that to run a wire instead of calling it, you would call .run on it.

  onMount(() => {
    wire(($) => {
       // ... todo
    }).run();
  });

https://github.com/abhishiv/alfama/pull/10/files#diff-0c19821af86e6776495c9639a1b3dbfa8b1dd09fbeba143040730491d7230090

  1. Change wire definition signature to add a previousValue param
export type WireFunction<T = unknown> = {
   ($: SubToken, params: { wire: WireFactory; previousValue?: T }): T;
 };

Opened #10 to handle these features

With regards to naming, I really like the name wire because it makes it explicit that you are wiring reactivity to the dom. But I did a quick search and solid uses the effect/memo/computed terminology. I do like the fact that this differentiation makes it explicit that effects are actions & have no value, while memo has return value. Maybe alfama should also have a such a distinction and introduce effect, while keeping wire as memo?

EDIT: come to think of it, are effects without value useful? Having values make it easy to prevent multiple execution of of a effect by just having a look at previousValue. Without values you have to hoist a variable in outer scope and then remember to set it manually. Also I see in solid computed doesn't allow setting a signal. Is that really usefull?

EDIT2: How about making it more like haptic with computed signals? So you can pass a wire while creating a signal instead of making wire a function that accepts a token?

@mindplay-dk
Copy link
Author

Short answer: I would shoot for as much parity with something existing, either Solid or Sinuous.

Long answer: ... 😅

Having a single abstract "wire" concept that can be used both for effects and computed state, it's tempting, in the same way that consolidating anything is tempting - and, if it makes sense, I think it's fine to consolidate it that way internally.

But people are going to use this for two different purposes, and it's helpful to have language in the source code explaining the difference - so I would prefer having two separate functions for derived state and effects. It sounds like you're leaning that way anyhow.

From my date-picker example in Sinuous, here's an example of derived state:

export const DatePicker = ({ /* ... */ }) => {
  const offset = observable(0);

  const date = computed(() => {
    const d = new Date(value());
    d.setMonth(d.getMonth() + offset());
    return date;
  });

  const month = computed(() => months[date().getMonth()])

  // ...
}

The important difference I'm hoping to see here is explicit subscriptions:

export const DatePicker = ({ value, /* ... */ }, { observable, computed }) => {
  const offset = observable(0);

  const date = computed(($) => {
    const d = new Date(value());
    d.setMonth(d.getMonth() + offset($)); // 👈 added token
    return date;
  });

  const month = computed(($) => months[date($).getMonth()]) // 👈 added token

  // ...
}

(note that the value prop in this example expects an observable.)

The observable is explicitly tied to the component life-cycle by using dependency injection: the injected observable function is "entangled" with the component life-cycle events, ensuring cleanup.

The computed function, I don't know, does this actually require dependency injection, or is it getting the context it needs from the $ subscription handle somehow? If it isn't, you might prefer to have free-standing functions for computed and other more high-level helper functions - it might make it easier to compose and build libraries of reusable and composable computed/effect utilities; the equivalent of React hooks.

On the other hand, it might feel good to inject everything, just for consistency - but, one could argue, this might actually make things harder to understand, as it raises the question of why they're injected, and there wouldn't be a very good answer besides "feels right".

So I would lean towards free-standing functions if they are in fact dependency free.

As to naming, I have no strong feelings. Name it however you want. But there are tangible benefits to mimicking the existing ecosystem, in terms of familiarity and adoption - Solid is already well established, and other projects are already copying that nomenclature. So I would worry less about names being "technically correct" and lean more towards familiarity and consistency with the ecosystem. Path of least surprise.

Regarding effects, I would pull up another example from the date-picker:

export const DateInput = ({ value, /* ... */ }) => {
  const isOpen = observable(false);

  // ...

  subscribe(() => {
    value(); // 👈 this looks odd
    isOpen(false);
  })

  // ...
}

(again, the value prop here is an observable.)

This might answer one of your questions, in that this happens to demonstrate and effect being triggered by a change to value, but the effect has no dependency on the updated value itself.

Again, the difference I'd be interested in is the explicit subscription:

export const DateInput = ({ value, /* ... */ }, { observable, subscribe }) => {
  const isOpen = observable(false);

  // ...

  subscribe(($) => {
    value($); // 👈 explicit subscription
    isOpen(false);
  })

  // ...
}

I think it's already obvious why this is better - the free standing call to value() didn't appear to do anything, where as with the token being received and passed, you can now infer that this code is actually doing something that depends on context.

For me, this is the motivation, and why I was so interested in Haptic. 🙂

Everything else is kind of secondary and (in my opinion) not where the important differences are - so in every other respect, mainly features and terminology, I would lean towards resembling Sinuous or Solid as much as possible.

People like Solid - a few of us don't like the magic, and that's what we're trying to fix, right? I would start with something that deviates from existing libraries mainly on that point. I'd like to see the key problem solved first - questioning other details could come later, after proving that the important change is worth while.

By the way, my date picker example has been ported between many frameworks over the years - it's designed to demonstrate components and essential state management features, composition, modularity, and reuse. I've ported it to basically every framework I've ever tried, probably dozens. I've found this makes a better benchmark and answers more questions than a counter or a to-do list, both of which are more "hello world" and less "things people actually need". 😅

It doesn't look like this is all the way there yet, but I would be happy to try porting it again, at your say-so. I can't really offer to help much with the library itself, beyond discussing things, but I'd be glad to help you push the framework and see how it holds up when you think it's ready. 😁

@abhishiv
Copy link
Owner

abhishiv commented Aug 28, 2024

hey @mindplay-dk

thank you very much for the response. And sorry for the late reply - I have been busy with some other stuff.

It doesn't look like this is all the way there yet, but I would be happy to try porting it again, at your say-so. I can't really offer to help much with the library itself, beyond discussing things, but I'd be glad to help you push the framework and see how it holds up when you think it's ready. 😁

Yeah thanks a lot for your feedback. It's really useful since it helps me change the api surface based on developer experience feedback.

I have implemented 2) and 3) from my list(.run on wire, and previousValue).

For 1) I started on it and have in the meantime have added a computedSignal function(exposed in component as computedSignal and in alfama/state as createComputedSignal) which works very much like it does in haptic.

export const createComputedSignal = <T = any>(wire: Wire<T>) => {
const value = wire.run();
const signal = createSignal<T>(value);
const handler = () => {
if (signal.get() !== wire.value) signal.set(wire.value as T);
};
wire.tasks.add(handler);
return signal;
};

This is just a stopgap since it was fairly easy to implement. I still have to look into subscription for wires, and will get to it hopefully next week.

Btw, your date picker example is very good! I didn't finish, but started porting it to alfama as well. I didn't finish, but I realised that to render the row it needs to the use stores instead of signals, since only stores could use the <Each> component. I'll get to it as well next week.

@abhishiv abhishiv added the enhancement New feature or request label Aug 28, 2024
@mindplay-dk
Copy link
Author

I realised that to render the row it needs to the use stores instead of signals, since only stores could use the <Each> component

You don't need <Each> for this:

      ${() => weeks().map(week => html`
        <tr>
          ${week.map(day => html`
            <td class="date-picker-btn ${day.class}" onClick=${() => { offset(0); value(day.value) }}>${day.date}</td>
          `)}
        </tr>
      `)}

it uses regular JS array .map instead of the Sinuous map function (the Sinuous analog to <Each>) because the calendar only updates as a whole, when you navigate to a different month - you don't really benefit from granular updates here. 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants