dyo / dyo

Dyo is a JavaScript library for building user interfaces.

Home Page:https://dyo.js.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

No issue - just an idea that may be discussed: Splitting Dyo core from Dyo non-essentials

mcjazzyfunky opened this issue · comments

The following is just about an idea, hope someone has time for a little discussion about that.
Primary note: Like always, it's perfectly fine not to like the following idea and to speak frankly if you do not like it ... it's just a little discussion about a little idea ...

I always asked myself whether it could be a good idea for Dyo version >= 4 or whatever to split the essential core part of Dyo from the non-essential part.

"Core" could be: createElement, Fragment, Boundary, render etc.
"Non-essential": Children, hooks, state management, context managment
(not really sure whether it would be a good idea to separate the
context management from the core as it seems quite "essential", but anyway...)

I personally would even consider to put the core part into an extra project, let's call it "ui-core" for now.
Dyo would then be just one possible API (out of possibly many) on top of "ui-core".
Some other UI library authors could provide a completely diffent API on top of "ui-core" if they like (different component state mangement or a different hooks API like for example the one that the "ivi" UI library is using or maybe even some template-engine based stuff or whatever) and the components would still be interchangeable as long as those different UI libraries are all based on "ui-core".
Of course it's already possible to provide different API's on top of Dyo, but that would lead in larger bundle sizes and slightly worse performance, compared to use a separate optimized core.

Even if not splitted into different packages, splitting within the same package (called "dyo") may increase tree-shakeablilty.

One possibility (out of many) to split the core from the hooks, the state managment and maybe even the context management could be the following:
Add (to the core) a public singleton object called ExcecMgr that has a method getCurrentComponentController as described below and all Dyo base hooks (including state management) could be implemented in userland by using that execution manager.
Also the context management could then be implemented in userland (if really useful).

It's important to mention that the "normal" UI developer will never get in touch with that execution manager directly, it will only be used for some base functions in userland (for example the base hooks).

// type of the public, singleton execution manager
type ExecutionManager = {
  // will return the controller if Dyo is currently
  // processing the component function, otherwise it will
  // return null
  getCurrentComponentController(): ComponentController | null

  // some other methods may follow in future...
}

type ComponentController = {
  // I don't think that the current props should be accessible
  // by the component controller but this would be
  // necessary to implement some hooks the way they
  // are implemented in Dyo - for example: useState(props => ...)
  getCurrentProps(): Record<string, any>,

  // redundant but practical
  isMounted(): boolean,
  
  // needed for example to allow a "useState" implementation
  // in userland
  forceUpdate(): void,

  // This one will return the path to the component instance.
  // For example "13.1.4.2.6" where "13" represents the root 
  // component, "13.1" is the first child component of the root,
  // component "13.1" is the parent component of "13.1.4"
  // and "13.1.4.2.6" is a sibling of "13.1.4.2.4"  
  // 
  // The idea comes from "deku.js". It could be used for example
  // to implement context management in userland. 
  // Maybe a linked list instead of a string would be better for
  // performance reasons, anyway....
  getComponentPath(): string,
 
  // lifecycle
  afterMount(subscriber: Subscriber): Unsubscribe,
  beforeUpdate(subscriber: Subscriber): Unsubscribe,
  afterUpdate(subscriber: Subscriber): Unsubscribe,
  beforeUnmount(subscriber: Subscriber): Unsubscribe,
  // etc.
  
  // plus maybe some more
}

type Subscriber = () => void
type Unsubscribe = () => void

Even if not splitted into different packages, splitting within the same package (called "dyo") may increase tree-shakeablilty

Just to comment on this point dyo is currently very tree shakeable, pretty much everything listed in the following:

"Non-essential": Children, hooks, state management, context management.

Is tree-shakeable: that includes, Children helpers, all the hooks including useContext and all the special components like Context are tree-shakeable. That is to say if you don't import them any reasonable bundler should be able to exclude them from your bundle.

Perhaps a concrete goal would help in API design.
For example: What API(s) need to be exposed in order to support ivi-like components

import { component, invalidate } from "custom-dyo-ui";
import { h, render } from "dyo";

const Counter = component((c) => {
  let counter = 0;

  const ticker = useEffect(c, (interval) => {
    const id = setInterval(() => {
      counter++;
      invalidate(c);
    }, interval);
    return () => clearInterval(id);
  });

  return (interval) => (
    ticker(interval),

    h('div','x', `Counter: ${counter}`),
  );
});

render(
  Counter(1000),
  document.getElementById("app"),
);

Thanks a lot for the clarifications regarding the tree-shakeability of Dyo.
Means, obviously I am doing something wrong in my build configuration.
Something like:

// File: index.js
export { Fragment } from 'dyo'

results in a 15 kB bundle (unzipped) with my (apperenty wrong) rollup configuration.

If someone has 5 minutes, could (s)he do me a big favor and run the following commands and check out what's wrong with my rollup.config.js, please?
Many thanks in advance.

git clone https://github.com/mcjazzyfunky/test-treeshaking.git
cd test-treeshaking
npm install
npm run build
ls -lh dist

@mcjazzyfunky I think the hooks and .Children helpers are tree-shaken with that bundle, because it should be ~22kb(unzipped) with everything included.

Thanks @thysultan. Mmmh, but 15 kB for exporting a null value seems to indicate that rollup really should shake that tree a little bit more. Actually I have never scrutinized how good rollup's and webpack's tree-shaking is really working.
Interesting fact: If I use the same build scripts for export { Fragment } from 'preact' then I'll get a 140 B output file by rollup. Webpack's output is 8,6 kB unzipped.
Strange ... anyway ....

Please just ignore that the implementation of the following example is a bit quick'n'dirty and not really complete.
I really like that ivi pattern component(c => props => vdom-tree) as a base for describing stateful components in an alternative way. Frankly, the way you have to apply hooks in ivi is not necessarily my first choice - but this is just a matter of taste, I think.
Please find here my interpretation of some ivi patterns (be aware this is NOT a clone of the original ivi API):

https://codesandbox.io/s/dyo-ivi-like-demo-l5q7v

What's called component(...) in ivi is called statefulComponent(displayName, c =>...) here.
That c argument for statefulComponent is basically of type ComponentController as described above (yet not 100% implemened).
If you open file statefulComponent.js you see that (of course) it uses Dyo hooks and also provides some kind of Pub/Sub mechanism (called notifier there). Both is in theory not really necessary as Dyo is already using some Pub/Sub feature internally. And the Dyo's hooks functions are only needed in function statefulComponet itself as there is currently no other way to interact with Dyo's component lifecylce. Both means unnecessarily larger bundle sizes.

Regarding the type of that ComponentController:
Basically it really needs some counterparts for at least:

  • forceUpdate: for example needed for state management and updates in general
  • afterMount (could be combined with afterUpdate if desired): reasons are obvious, I think
  • beforeUpdate: Will be used for example to update state values before the render function is called
  • afterUpdate: the reasons are obvious
  • beforeUnmount: reasons are obvious.
  • beforeLayout: to implement a counterpart to useLayout
  • also something needed for context managment (something different than my propsal above, as this is not really a good solution)
  • etc. (maybe for example something to implement a counterpart of useResouce - not really sure whether this could be implemented in userland)

BTW: A different hook pattern (differnt to Dyo and different to ivi) is the one that I use for example in an experimental webcomponents library (I think you've already seen a little demo the other day regarding some other issue):
https://codesandbox.io/s/js-elements-demo-1z7im
But I'm quite sure most people will not like that pattern as objects like props and state are mutable or proxies there (depending on implementation). Anyway, nevertheless the implementation there is also based on something like that ComponentController described above and could also be based on that statefulComponent function.

Mmmh, maybe that idea is a bit vague at the moment. Perhaps I should ask for a discussion again when it will be a bit further evolved some day in future.
Moreover, frankly my preferred solution would be to force that the non-pureness of component functions and hooks is reflected in the (TypeScript) type of the the component and hook functions.
If a component function has an arity/length of 1 it's pure function. If a component function has an arity/length of two then it's a non-pure function and the UI system will pass something like the above mentioned ComponentController (should be renamed to Ctrl for practical reasons) as second argument.
I've never liked it that the types do not tell anything about the effectfulness of components and hooks, especially when you write functions that return hook functions or components.
But this would not be compatible with React or Dyo and I'm quite sure the community would not like it and would call it "too verbose".

Something like the following:

function Counter({
  label = 'Counter'
  initialValue = 0,
  onChange
}: CounterProps, c: Ctrl) {
   const [count, setCount] = useState(c, initialValue) 

   ...
})

Okay, I'll close this issue now.
Maybe I'll come back in future with some more details.