Skip to content

Instantly share code, notes, and snippets.

@bryceosterhaus
Created June 23, 2017 00:14
Show Gist options
  • Save bryceosterhaus/0483e085a7404231b0b6c9ba75821ac8 to your computer and use it in GitHub Desktop.
Save bryceosterhaus/0483e085a7404231b0b6c9ba75821ac8 to your computer and use it in GitHub Desktop.
Callbacks vs. Events

Say you have three nested components. GrandParent -> Parent -> Child. You have state(foo and bar) that lives in Grandparent. That state is then changed by something that happens in Child. There are two different ways to think about how to get this state to change, Event Emitting and Callback passing. I think it would be wise of us to consider switching to a callback approach rather than an Event Emitting. Primarily because I think it reduces unneccessary boilerplate and having to write logic in the "middle man" component, which in this case is the parent.

Here is the code for the two different approaches. Let me know what you think

Event Emitting

{namespace GrandParent}

/**
 * GrandParent
 */
{template .render}
  {call Parent.render}
    {param events: [
      'childFooChange': $_handleNewFoo,
      'childBarChange': $_handleNewBar
    ] /}
  {/call}
{/template}
class GrandParent extends Component {
  _handleNewFoo(newFoo) {
    this.state.foo = newFoo;
  }

  _handleNewBar(newBar) {
    this.state.bar = newBar;
  }
}
{namespace Parent}

/**
 * Parent
 */
{template .render}
  {call Child.render}
    {param events: [
      'fooChange': $_handleFooChange,
      'barChange': $_handleBarChange
    ] /}
  {/call}
{/template}
class Parent extends Component {
  _handleFooChange(event) {
    this.emit('childFooChange', event);
  }

  _handleBarChange() {
    this.emit('childBarChange', event);
  }
}

Callback functions

{namespace GrandParent}

/**
 * GrandParent
 */
{template .render}
  {call Parent.render}
    {param onChildFooChange: $_handleNewFoo /}
    {param onChildBarChange: $_handleNewBar /}
  {/call}
{/template}
class GrandParent extends Component {
  _handleNewFoo(newFoo) {
    this.state.foo = newFoo;
  }

  _handleNewBar(newBar) {
    this.state.bar = newBar;
  }
}
{namespace Parent}

/**
 * Parent
 */
{template .render}
  {@param onChildFooChange: any}
  {@param onChildBarChange: any}

  {call Child.render}
      {param onFooChange: $onChildFooChange /}
      {param onBarChange: $onChildBarChange /}
  {/call}
{/template}
class Parent extends Component {
  // No need to emit anything anymore
}

Personal opinions

I think using callbacks are easier to debug and easier to reason about when reading through code. This is especially helpful when there are multiple components nested, otherwise you would be creating event emitters on each component up the tree. This is a common pattern used in React and a pattern we picked up and used in Loop. We found is extremely helpful and easy to work with since it follows the same paradigm as passing down state through components.

Let me know what you think!

A parrot for your time.

profit

@carloslancha
Copy link

Hi!

Since the event emitting stack is preserved and you can follow the calls to the original emitter, for me debugging events or callbacks is mostly the same.

welcome_-_liferay_dxp

For me the events way makes components feel a little more independent, but I guess that's just a matter of taste.

Also, events way gives you the option of listening some events for free, like:

{call Button.render}
    {param events: [ 
        'click': $_handleClick 
    ] /}
...
{/call}

With this you're listening the button click without needing to add any logic, callback execution or extra event emitting on button component.

But to decide this... I will go for the reason of not rewrite everything to switch to callbacks...

What do you guys think? 😃

@bryceosterhaus
Copy link
Author

bryceosterhaus commented Jun 23, 2017

Hey @carloslancha, thanks for replying.

For the example you gave of the "free events". Wouldn't you still have to emit inside that Button component or is that event applied to the component container?

For component independence, I like callbacks because it gives the idea that components are consumed and used by the parent component rather than each component living independently doing their own thing. But yes, this is also just a matter of taste.

And as far as rewriting, yeah I agree its not ideal, but I do think this would help for the long term.

@jbalsas
Copy link

jbalsas commented Jun 23, 2017

Hey @bryceosterhaus,

I just checked, and although it is not currently possible because of how metal.js EventEmitter::emit works, we could fix it to solve the ergonomics issue you point out. This way, we should be able to rewrite your first example Parent as:

{namespace Parent}

/**
 * Parent
 */
{template .render}
  {call Child.render}
    {param events: [
      'fooChanged': $emit,
      'barChanged': $emit
    ] /}
  {/call}
{/template}
class Parent extends Component {
    // No need to emit anything anymore
}

The other debate, as you both already mentioned is pretty much semantics and personal taste.

  • Once in a React-like mindset, it is hard to reason about it any other way, and it does indeed work perfectly when all your app follows that pattern. As Carlos mentioned, however, an event-driven APIs allows for multiple subscribers which are common and/or desirable for other people or in other scenarios.
  • The event approach in turn removes some burden from the component creator, that can simply fire events without the need to check for params or handle how callbacks should be invoked (of course, this is can be easily solved/abstracted).

@bryceosterhaus
Copy link
Author

Hey @jbalsas, thanks for replying. In regards to your potential fix for this, I think that is great idea and definitely would help a bit. I still want to push back and really recommend the callback approach because I think it simplifies our data flow patterns and helps developers compose components.

In response to:

event approach in turn removes some burden from the component creator

I would argue that this is actually a bad thing. Component creators should be the one defining the API of that component and how it is used. When the component creator defines the params that is takes, they are defining their API. When we use event emitting, it requires the consumer of the components to know more than just the arguments the component takes. IMO when we create components that rely on even emitting, the responsibility of the component goes from the creator and off loads it to the consumer which ultimately makes it harder to compose components together.

In the end, I am just hoping to make it easier to develop and compose components. In my experience so far, our components are feeling more like YUI modules that are great in isolation, but difficult to to piece together when creating a larger component.

And I do realize its probably not realistic to change our patterns at this point because of time constraints. But I would encourage us to at least look more into it because I think it would be a great benefit in the long run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment