wavesoft / dot-dom

.dom is a tiny (512 byte) template engine that uses virtual DOM and some of react principles
Apache License 2.0
806 stars 53 forks source link

Introducing lifecycle methods #12

Closed wavesoft closed 7 years ago

wavesoft commented 7 years ago

This PR introduces a lifecycle method support that can be used to handle DOM mounts, unmounts and updates. This tries to be as close as possible to the react.js equivalent lifecycle methods, but be aware that they are more DOM-oriented in their arguments.

styfle commented 7 years ago

Is this just onComponentUpdate or does it include all of React's life cycle methods?

wavesoft commented 7 years ago

Hmm good point.. onComponentUpdate is actually a misleading name. I changed it.

Due to the severe lack of space, this is going to be a multi-functional function. The react equivalents would be:

React Function .dom Equivalent
componentDidMount onDOMUpdate( domInstance )
componentWillUnmount onDOMUpdate( undefined )
wavesoft commented 7 years ago

I am working also on an alternative syntax, but unfortunately it's taking too much space.

React Function .dom Equivalent
componentDidMount onDOMUpdate( newElement, 1 )
componentWillUnmount onDOMUpdate( previousElement, 0 )
componentDidUpdate onDOMUpdate( newElement, previousElement )

I am really open for discussion. This is not concrete yet and I would really like some feedback from all of you out there 😄

styfle commented 7 years ago

Instead of using a bunch of parameters for each lifecycle method, could you instead do something like the following:

const MyComponent = (props, state, setState, hook) {
  // Hook into life cycle events
  hook.componentDidMount(() => ready());
  hook.componentWillMount(() => almostReady());
  hook.componentWillReceiveProps(nextProps => changing(nextProps));
  hook.shouldComponentUpdate((nextProps, nextState) => false);

  // This is still your basic render
  return div( ... )
}

This would allow you to gradually implement life cycle hooks and better match React's lifecycle.

The goal of this project is different then React obviously but I think trying to maintain a similar API might make it gain more traction.

wavesoft commented 7 years ago

I like the hook idea. Perhaps a Proxy element can do it's magic there. I will investigate 👍 But from the looks of it, lifecycle methods would most probably land on ~0.2.3~ 0.3.0

styfle commented 7 years ago

@wavesoft Very cool! Keep up the great work! 💯 👍

wavesoft commented 7 years ago

I eventually ended up going with your idea @styfle . Unfortunately due to size limitations the naming had to be changed, so I used mnemonic letters instead:

.domReact.js equivalent
.mcomponentDidMount
.ucomponentWillUnmount
.dcomponentDidUpdate
styfle commented 7 years ago

Very cool. I was thinking of making a thin React Compatibility layer so that existing React components could be utilized in DotDom projects. I haven't given it much thought beyond assigning window.React = { createElement: H }

wavesoft commented 7 years ago

I would say it's more or less what you said:

window.React = { createElement: H };
window.ReactDOM = { render: R };

Assuming that no other core API from React.js is used, nor class components 🤔

But I wouldn't spend too much time there. I would focus a bit more on writing a JSX transformation for .dom, or figuring out other ways to reduce the size footprint of an application 😄

styfle commented 7 years ago

JSX transformation would be good if your app is only using .dom and don't expect to use 3rd party components, because 3rd party components are likely already compiled from JSX to JS.

For comparison, there is a lib called Preact which has a similar API to React, They have an optional preact-compat lib to convert an existing React project to Preact. It might be nice to do the same for .dom because optional doesn't have to be included in the size of the core lib.

wavesoft commented 7 years ago

Ah. I see your point. Yeah, that makes sense. Let's do this dot-dom-compat! We might need to implement some trickery if we want to convert classes into methods if we want to support Component instances.

Can you open an issue with your ideas so we can track the progress there? It's getting a bit out of the scope of this PR.

styfle commented 7 years ago

Makes sense. I created #26 👍

wavesoft commented 7 years ago

A problem that I see with this implementation of lifecycle methods is that it's not easy to create higher order components easily, if they also have to listen for lifecycle events.

For instance:

const A = (props, state, setState, hooks) => {
  hooks.m = () => alert ( 'mounted' );
  return div ('hello');
}

const AlertHi = (wrapComponent) =>
  (props, state, setState, hooks) => {
    hooks.m = () => alert ('hi');
    return wrapComponent(props, state, setState, hooks);
  }

const B = AlertHi(A);

// This will alert 'mounted'
R (H (B), document.body);
wavesoft commented 7 years ago

Note to self: unmount is not triggered when components change yet they return the same first-level vnode

SilentCicero commented 7 years ago

@wavesoft what is the status on lifecycle methods? I want to say that while higher order components are important, basic onload and unload is probably even more important and pressing for the module. That would make it very useful for me. If these hooks are functional, I would say, we should push those and figure out higher order problems later.

SilentCicero commented 7 years ago

Also I would prefer this notation, but I realize it may add boilerplate:

const MyComponent = (props, { state, setState, hook }) {
  // Hook into life cycle events
  hook.componentDidMount(() => ready());
  hook.componentWillMount(() => almostReady());
  hook.componentWillReceiveProps(nextProps => changing(nextProps));
  hook.shouldComponentUpdate((nextProps, nextState) => false);

  // This is still your basic render
  return div( ... )
}

Same effect, but I can only grab what I need from the injected object. What if I dont need state or setState. I find this notation far better.

SilentCicero commented 7 years ago

I really need the onload and unload lifecycle hooks lol.

wavesoft commented 7 years ago

I also tried to create a playground project so I can try this in a real world example and yeah, I felt the pain of higher order components @SilentCicero 😄

This branch contains a functional state of the lifecycle method implementation, so you can have a look, but I am still looking for a good solution to help creating higher-order components. If you have any idea, I am all ears 😄

SilentCicero commented 7 years ago

@wavesoft I'll think about the higher order component design. How can I reach you btw, gitter?

SilentCicero commented 7 years ago

The only other way would be to have a register method for the hooks, then when the hooks are triggerend all registered hooks are fired. Probably more bytes though..

So you just make m an array, m.push(() => {}), then it fires all m methods during life cycle changes.

Also, this comparison seems sketchy.. have you fully tested this, comparing objects with a single comparison is not necessarily a good idea: _child.a != _hooks.a..

You could also just fix the problem by policy:

const A = (props, state, setState, hooks) => {
  hooks.m = hooks.m || (() => alert ( 'mounted' ));
  return div ('hello');
}

const AlertHi = (wrapComponent) =>
  (props, state, setState, hooks) => {
    hooks.m = () => alert ('hi');
    return wrapComponent(props, state, setState, hooks);
  }

const B = AlertHi(A);

// This will alert 'mounted'
R (H (B), document.body);

That fixes the problem of overrides. You just make a policy of.. this is how you should use lifecycle methods. However, not all hooks get fired then.


You can also try this:

let createElement...
,z=(a=[],b,c)=>a.map(e=>e(b,c))

**approx. 31 byte increase above

_hooks={a:vnode.$,m:[],u:[],d:[]},

**approx. 15 byte increase above

       z(
          _child
            ? _child.a != _hooks.a
              ? (z(_child.u), _hooks.m)
              : _hooks.d
            : _hooks.m
        ,_new_dom, _child);

**approx. 9-12 byte savings above..

while (_children[_c]) {                                           // The _c property keeps track of the number of
                                                                      // elements in the VDom. If there are more child
                                                                      // nodes in the DOM, we remove them.

      z(_children[_c].u)
                                                                      // elements that will be removed

      render(                                                         // Remove child an trigger a recursive child removal
        [],                                                           // in order to call the correct lifecycle methods in our
        dom.removeChild(_children[_c])                                // deep children too.
      )
    }

**approx. 4-5 byte savings above..

Here we assume the life cycle hooks are actually arrays of methods to be fired. This way, when we set the methods, we can set a push potentially. However, the problem becomes, pushing the same hook multiple times. Although, I suppose that wouldn't happen as the function should only be called on re-render any way. But you would have to remove something like Proxy to make enough space for this (which I would be in favor of).

it would result in something like this, which would work I believe (in theory lol):

const A = (props, state, setState, hooks) => {
  hooks.m.push(() => alert ( 'mounted' ));

  return div ('hello');
}

const AlertHi = (wrapComponent) =>
  (props, state, setState, hooks) => {
    hooks.m.push(() => alert ('hi'));

    return wrapComponent(props, state, setState, hooks);
  }

const B = AlertHi(A);

// This will alert 'mounted'
R (H (B), document.body);

Yep.. just tested this.. works like a charm =D Both console logs fire. @wavesoft my solution above.

wavesoft commented 7 years ago

@SilentCicero what about making the hooks callable? That can spare a few client-side bytes. Like:

hooks.m(() => console.log('mounted'));

But I am not sure how much we can fit in 6 bytes that we have left 😄

wavesoft commented 7 years ago

I think I am satisfied with this. We are going to have lifecycle methods in 0.3.0 🎉

SilentCicero commented 7 years ago

@wavesoft wooo!