yysun/apprun

Stateful components

Closed this issue · 14 comments

Just started looking at AppRun last night, and really like the design/concept! Very elegant đź‘Ť

I noticed in the documentation that stateful components are "work in progress" - I was wondering if you can elaborate a little bit?

I'm trying to figure out if this feature (or a planned future feature) satisfies my use-case.

As far as my understanding, components in AppRun are very different from components in, say, React - I think the best way I can explain my understanding of the conceptual difference is, AppRun components are "components of the application", while React components are "components of the UI".

While components in AppRun provide a useful abstraction of a self-contained section of the UI state and view, presumably stateful components will provide an abstraction of UI "controls", e.g. components that have an internal state?

I recently explained it to someone like this:

There is application state, and there is control state.

Control state really pertains only to a control while it exists, and becomes completely irrelevant the moment the control disappears from the user's view: whether a date-picker or drop-down was open, where the cursor was located in an input, and so on - no longer relevant once the control is gone.

We don't need or expect or want such state to persist in the model - most of the time, we don't even care that it exists, for example, we don't typically care if a drop-down is open, we just want a notification when a selection is made.

Am I right to think this is the purpose of stateful components in AppRun?

Will we be able to instanciate these components via JSX, similarly to how all components are instanciated in for example React?

Or where do you see this feature heading?

Thanks :-)

yysun commented

Thank you for the comments. I really like you summarized the AppRun components are “components of applications”. I usually call them mini-applications. They react to events and render the elements they are mounted to.

Stateful component is a child mini-application inside a parent mini-application. The child runs and refreshes itself. Just like your example of date-picker, the parent does not need refresh when child refreshes.

The tricky part is when parent freshes. It needs to restore the same child. Current implementation is to cache and restore the child by an Id. If Id is not presented, AppRun creates a new instance of child component, which means the state of the previous child would lost.

Stateful components can be instanciated just like React now with a requirement that it needs an Id.

If we can figure out a way to auto-generate the Id / restore the same child, it will be perfect then.

hey guys,

don't mean to hijack the thread but what you're describing is literally the design of domvm's Views/Components [1]. Each holds its own microstate (can be fully private), can be embedded in other views, can be redrawn independently (whether by self or via ancestor) and can be instantiated and mounted as necessary. the ui can therefore be as monolithic or micro-composed as necessary.

The way redraw works in domvm is that each vnode has idx and parent properties, so redrawing a view in-place is simply a matter of re-slotting the new vnode into parent.body[idx]. it has worked out well and is quite performant.

[1] https://github.com/domvm/domvm

yysun commented

Thank you @leeoniya for the idx/parent hint and the reference to domvm.

The tricky part is when parent freshes. It needs to restore the same child. Current implementation is to cache and restore the child by an Id. If Id is not presented, AppRun creates a new instance of child component, which means the state of the previous child would lost.

I was a bit confused by the id-attribute in the example - if I understand correctly, this provides local identity (within the parent) like the key-attribute in React, as opposed to the id-attribute in HTML, which provides global identity throughout the document?

yysun commented

The Id is actual DOM Id. E.g., when rendering a stateful component:

<TestComponent id=“c1” />

AppRun creates a div with the Id and mount/attach the component instance to it.

<div id=“c1”></div>

Now, c1 is available in the document as a global identity. And you can retrieve the component instance from its _component property.

const component = c1._component;
yysun commented

When there is an id, everything works fine. Only in v1.x:

If Id is not presented, AppRun creates a new instance of child component, which means the state of the previous child would be lost.

In v2.x beta, we are testing a different logic.

@yysun what is the different logic that you are testing for the v2.x?

yysun commented

In V1.x, we generate a new id for components w/o id. The new id is used as the cache key well as the id for the div that the component will mount to. Every refresh the new id is not in the cache, so a new component instance will be created. The state of the old component is lost.

In V2.x, we only use the generated id for the div. We use the class as the cache. Therefore, all components w/o id share the same instance. The change is in 6550af2.

The testing is going well. Plan to merge the logic into V1.x soon.

This has changed a lot in the past year, right?

Looks like you have proper child components now?

Only, I still don't see support for any sort of local identity - you still don't have support for keyed updates?

yysun commented

Do you have examples of how do you want to use the local identity and keyed
updates?

A basic sortable table, for example - something like:

const updateTitle = (id) => (({ rows }), e) => ({ /* updated rows array... */ });

const moveRow = (id, direction) => ({ rows }) => ({ /* sorted rows array... */ });

const view = state => (
  <table>
    {state.rows.map(row => (
      <tr key={row.id}>
        <td><input type="text" value={row.title} $oninput={updateTitle(row.id)}/></td>
        <td $onclick={moveRow(row.id, -1)}>UP</td>
        <td $onclick={moveRow(row.id, +1)}>DOWN</td>
      </tr>
    )}
  </table>
);

So I have an array of row objects like { id: 123, title: "foo" } and want to be able to edit the titles and move the items up/down.

There could be many reasons for wanting a local child key rather than an id global to the page - basically, you want to be sure there are no collisions with other elements on the page that happen to use the same ID. (and sure, you could manually namespace a global id like e.g. id={"row-"+row.id} as a work-around - but then, if there's a case where you want two instances of this editor on page at the same time, you need yet another work-around...)

yysun commented

Perhaps you can define the local key as a data attribute, data-key.

Then you can query it from the element property of the component.

component.element.querySelector('[data-key="xxx"]')

To me, the main reason for using JSX and a virtual DOM library is to avoid exactly this sort of thing.

yysun commented

It does as you expected: once you updated the state, the JSX/virtual DOM engine renders only the changes to the screen.

In some cases, the state might have thousands of rows, but you only changed one row. You may want to update the DOM element directly without the virtual DOM diffing. I thought you were talking about this case and suggested to get the element to update.

I still need your help to let me understand the scenario of "local identity and keyed updates".